's'
This commit is contained in:
@@ -1,11 +1,12 @@
|
|||||||
"""
|
"""
|
||||||
公众号助手(com.tencent.mp)「收到XXX的推荐」通知入库。
|
公众号助手(com.tencent.mp)「收到XXX的推荐」通知入库。
|
||||||
|
|
||||||
仅需 PocketBase 一个集合:mp_recommend_events(每条通知一行)。
|
仅需 PocketBase 一个集合:mp_recommend_events(每个 mp_uid + recommender_key 一行,累计计数)。
|
||||||
|
|
||||||
- username Text 用户名(展示名,如从标题解析的 Eric)
|
- username Text 用户名(展示名,如从标题解析的 Eric)
|
||||||
- cumulative_count Number 该用户截至本条通知的累计触发次数(每次 webhook 写入时 = 旧总数 + 1)
|
- cumulative_count Number 该用户的累计触发次数(同一 mp_uid + recommender_key 每次 webhook 写入时 = 旧值 + 1)
|
||||||
- recommender_key Text 归一化键(casefold),与 username 对应,便于筛选,建议建索引
|
- recommender_key Text 归一化键(unicode 归一化 + 清洗 + casefold),与 username 对应,便于筛选,建议建索引
|
||||||
|
- mp_uid Text 用户ID(用于把同名推荐人按不同账号隔离统计)
|
||||||
|
|
||||||
字段(在 PocketBase 中新建,名称一致):
|
字段(在 PocketBase 中新建,名称一致):
|
||||||
- recommender_key Text
|
- recommender_key Text
|
||||||
@@ -26,6 +27,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import logging
|
import logging
|
||||||
import re
|
import re
|
||||||
|
import unicodedata
|
||||||
from typing import Any
|
from typing import Any
|
||||||
from urllib.parse import parse_qs
|
from urllib.parse import parse_qs
|
||||||
|
|
||||||
@@ -44,6 +46,26 @@ def _pb_escape(s: str) -> str:
|
|||||||
return (s or "").replace("\\", "\\\\").replace('"', '\\"')
|
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]:
|
def parse_form_urlencoded_like(body: str) -> dict[str, str]:
|
||||||
q = parse_qs(body, keep_blank_values=True, strict_parsing=False)
|
q = parse_qs(body, keep_blank_values=True, strict_parsing=False)
|
||||||
return {
|
return {
|
||||||
@@ -103,19 +125,35 @@ def persist_mp_recommend(
|
|||||||
timestamp_raw: str,
|
timestamp_raw: str,
|
||||||
client_ip: str,
|
client_ip: str,
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
"""写入一条事件;username、cumulative_count 写入本行(累计次数 = 同 key 已有条数 + 1)。"""
|
"""按 mp_uid + recommender_key 聚合更新累计次数(存在则 +1,不存在则创建)。"""
|
||||||
parsed = parse_mp_recommend_content(content)
|
parsed = parse_mp_recommend_content(content)
|
||||||
forward_ts = _parse_forward_ts_ms(timestamp_raw)
|
forward_ts = _parse_forward_ts_ms(timestamp_raw)
|
||||||
|
|
||||||
username = (parsed["recommender_name"] or "").strip() or "unknown"
|
username = _normalize_username(parsed.get("recommender_name") or "") or "unknown"
|
||||||
key = username.casefold() if username != "unknown" else "unknown"
|
key = _username_to_key(username if username != "unknown" else "")
|
||||||
|
mp_uid = (parsed.get("mp_uid") or "").strip()
|
||||||
|
|
||||||
pb = get_pb_client()
|
pb = get_pb_client()
|
||||||
events = pb.collection(COL_EVENTS)
|
events = pb.collection(COL_EVENTS)
|
||||||
filt = f'recommender_key = "{_pb_escape(key)}"'
|
|
||||||
lst = events.get_list(1, 1, {"filter": filt})
|
filt_parts = [f'recommender_key = "{_pb_escape(key)}"']
|
||||||
total_before = int(getattr(lst, "total_items", 0) or 0)
|
if mp_uid:
|
||||||
cumulative_count = total_before + 1
|
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] = {
|
event_payload: dict[str, Any] = {
|
||||||
"recommender_key": key,
|
"recommender_key": key,
|
||||||
@@ -124,7 +162,7 @@ def persist_mp_recommend(
|
|||||||
"package_name": parsed["package_name"],
|
"package_name": parsed["package_name"],
|
||||||
"title_line": parsed["title_line"],
|
"title_line": parsed["title_line"],
|
||||||
"summary_line": parsed["summary_line"],
|
"summary_line": parsed["summary_line"],
|
||||||
"mp_uid": parsed["mp_uid"],
|
"mp_uid": mp_uid,
|
||||||
"device_time_str": parsed["device_time_str"],
|
"device_time_str": parsed["device_time_str"],
|
||||||
"device_model": parsed["device_model"],
|
"device_model": parsed["device_model"],
|
||||||
"raw_content": parsed["raw_content"],
|
"raw_content": parsed["raw_content"],
|
||||||
@@ -134,7 +172,10 @@ def persist_mp_recommend(
|
|||||||
if forward_ts is not None:
|
if forward_ts is not None:
|
||||||
event_payload["forward_timestamp_ms"] = forward_ts
|
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)
|
event_id = getattr(rec, "id", None) or str(rec)
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
|
|||||||
Reference in New Issue
Block a user