From a59592bed5b22b6baa7bc41bf4298c82c1f64b03 Mon Sep 17 00:00:00 2001 From: eric Date: Thu, 2 Apr 2026 05:26:11 -0500 Subject: [PATCH] 's' --- app/services/mp_recommend.py | 65 +++++++++++++++++++++++++++++------- 1 file changed, 53 insertions(+), 12 deletions(-) diff --git a/app/services/mp_recommend.py b/app/services/mp_recommend.py index 10022bb..a62dd01 100644 --- a/app/services/mp_recommend.py +++ b/app/services/mp_recommend.py @@ -1,11 +1,12 @@ """ 公众号助手(com.tencent.mp)「收到XXX的推荐」通知入库。 -仅需 PocketBase 一个集合:mp_recommend_events(每条通知一行)。 +仅需 PocketBase 一个集合:mp_recommend_events(每个 mp_uid + recommender_key 一行,累计计数)。 - username Text 用户名(展示名,如从标题解析的 Eric) -- cumulative_count Number 该用户截至本条通知的累计触发次数(每次 webhook 写入时 = 旧总数 + 1) -- recommender_key Text 归一化键(casefold),与 username 对应,便于筛选,建议建索引 + - cumulative_count Number 该用户的累计触发次数(同一 mp_uid + recommender_key 每次 webhook 写入时 = 旧值 + 1) + - recommender_key Text 归一化键(unicode 归一化 + 清洗 + casefold),与 username 对应,便于筛选,建议建索引 + - mp_uid Text 用户ID(用于把同名推荐人按不同账号隔离统计) 字段(在 PocketBase 中新建,名称一致): - recommender_key Text @@ -26,6 +27,7 @@ from __future__ import annotations import logging import re +import unicodedata from typing import Any from urllib.parse import parse_qs @@ -44,6 +46,26 @@ def _pb_escape(s: str) -> str: return (s or "").replace("\\", "\\\\").replace('"', '\\"') +def _normalize_username(raw: str) -> str: + """ + 尽量稳定地把展示名归一化到可用于统计/过滤的形式。 + - 兼容 emoji/颜文字/特殊字母:保留字符本身,不做 ASCII 限制 + - 统一空白:压缩为单个空格 + - Unicode 归一化:NFKC(把全角等兼容形态归一) + - 大小写:仅在 key 上 casefold + """ + s = (raw or "") + s = unicodedata.normalize("NFKC", s) + s = s.replace("\u00A0", " ") + s = re.sub(r"\s+", " ", s).strip() + return s + + +def _username_to_key(username: str) -> str: + u = _normalize_username(username) + return u.casefold() if u else "unknown" + + def parse_form_urlencoded_like(body: str) -> dict[str, str]: q = parse_qs(body, keep_blank_values=True, strict_parsing=False) return { @@ -103,19 +125,35 @@ def persist_mp_recommend( timestamp_raw: str, client_ip: str, ) -> dict[str, Any]: - """写入一条事件;username、cumulative_count 写入本行(累计次数 = 同 key 已有条数 + 1)。""" + """按 mp_uid + recommender_key 聚合更新累计次数(存在则 +1,不存在则创建)。""" parsed = parse_mp_recommend_content(content) forward_ts = _parse_forward_ts_ms(timestamp_raw) - username = (parsed["recommender_name"] or "").strip() or "unknown" - key = username.casefold() if username != "unknown" else "unknown" + username = _normalize_username(parsed.get("recommender_name") or "") or "unknown" + key = _username_to_key(username if username != "unknown" else "") + mp_uid = (parsed.get("mp_uid") or "").strip() pb = get_pb_client() events = pb.collection(COL_EVENTS) - filt = f'recommender_key = "{_pb_escape(key)}"' - lst = events.get_list(1, 1, {"filter": filt}) - total_before = int(getattr(lst, "total_items", 0) or 0) - cumulative_count = total_before + 1 + + filt_parts = [f'recommender_key = "{_pb_escape(key)}"'] + if mp_uid: + filt_parts.append(f'mp_uid = "{_pb_escape(mp_uid)}"') + filt = " && ".join(filt_parts) + + lst = events.get_list(1, 1, {"filter": filt, "sort": "-updated"}) + items = list(getattr(lst, "items", []) or []) + existing = items[0] if items else None + + old_count = 0 + existing_id = None + if existing is not None: + existing_id = getattr(existing, "id", None) + try: + old_count = int(getattr(existing, "cumulative_count", 0) or 0) + except (TypeError, ValueError): + old_count = 0 + cumulative_count = old_count + 1 event_payload: dict[str, Any] = { "recommender_key": key, @@ -124,7 +162,7 @@ def persist_mp_recommend( "package_name": parsed["package_name"], "title_line": parsed["title_line"], "summary_line": parsed["summary_line"], - "mp_uid": parsed["mp_uid"], + "mp_uid": mp_uid, "device_time_str": parsed["device_time_str"], "device_model": parsed["device_model"], "raw_content": parsed["raw_content"], @@ -134,7 +172,10 @@ def persist_mp_recommend( if forward_ts is not None: event_payload["forward_timestamp_ms"] = forward_ts - rec = events.create(event_payload) + if existing_id: + rec = events.update(existing_id, event_payload) + else: + rec = events.create(event_payload) event_id = getattr(rec, "id", None) or str(rec) logger.info(