fix: 修复独立站日历节日去重与中文资料链接

This commit is contained in:
Jack
2026-08-17 12:58:19 +08:00
parent b1b7c13c97
commit 7af253f081
7 changed files with 15419 additions and 20828 deletions
+74 -17
View File
@@ -109,6 +109,7 @@ LOCAL_HOLIDAY_NAMES: dict[str, dict[str, tuple[str, str]]] = {
"AU": ("Boxing Day (observed)", "英语"), "GB": ("Boxing Day (observed)", "英语"),
},
"lunar-new-year": {"CN": ("春节", "简体中文"), "KR": ("설날", "韩语"), "VN": ("Tết Nguyên Đán", "越南语"), "SG": ("农历新年 / Chinese New Year", "简体中文 / 英语")},
"new-year-eve": {"DK": ("Nytårsaften", "丹麦语"), "SE": ("Nyårsafton", "瑞典语"), "PH": ("一年中的最后一天", "英语 / 菲律宾语")},
"eid-fitr": {"AE": ("عيد الفطر", "阿拉伯语"), "TR": ("Ramazan Bayramı", "土耳其语"), "ID": ("Idul Fitri", "印度尼西亚语")},
"eid-adha": {"AE": ("عيد الأضحى", "阿拉伯语"), "TR": ("Kurban Bayramı", "土耳其语"), "ID": ("Idul Adha", "印度尼西亚语")},
"assumption-day": {
@@ -118,6 +119,11 @@ LOCAL_HOLIDAY_NAMES: dict[str, dict[str, tuple[str, str]]] = {
"PT": ("Assunção de Nossa Senhora", "葡萄牙语"), "ES": ("Asunción", "西班牙语"),
},
"indonesia-independence": {"ID": ("Hari Kemerdekaan Republik Indonesia", "印度尼西亚语")},
"new-year": {
"CN": ("元旦", "简体中文"), "JP": ("元日", "日语"), "RU": ("Новогодние каникулы", "俄语"),
},
"public-holiday:JP:coming of age day": {"JP": ("成人の日", "日语")},
"public-holiday:JP:greenery day": {"JP": ("みどりの日", "日语")},
}
FESTIVAL_TARGET_COUNTRIES = {
@@ -139,6 +145,10 @@ HOLIDAY_TRANSLATIONS = {
"whit monday": "圣灵降临节星期一", "pentecost monday": "圣灵降临节星期一",
"king's birthday": "国王诞辰", "mountain day": "山之日", "marine day": "海之日",
"respect for the aged day": "敬老日", "culture day": "文化日", "children's day": "儿童节",
"new year s day": "元旦", "new year holiday": "新年假期",
"new year s eve": "跨年夜", "last day of the year": "跨年夜",
"coming of age day": "成人节", "greenery day": "绿之日",
"labour thanksgiving day": "勤劳感谢之日", "labor thanksgiving day": "勤劳感谢之日",
}
SUBDIVISION_NAMES = {
@@ -282,6 +292,12 @@ FESTIVAL_PROFILES = {
"design": ["使用新年焕新而非单一宗教表达", "按南北半球季节调整材质"],
"avoid": ["南半球正值夏季,不要统一推保暖产品"],
},
"new-year-eve": {
"title": "跨年夜", "relevance": "low", "decision": "仅用于运营排班",
"what": "公历新年前夜或年末公共休假节点,主要影响跨年营业、物流和客服排班。",
"scenes": ["跨年营业", "物流排班"], "products": [], "design": [],
"avoid": ["不要把跨年夜与元旦或连续新年假期拆成多个重复消费节"],
},
"super-bowl": {
"title": "超级碗观赛消费节点", "relevance": "high", "decision": "值得场景备货",
"what": "美国职业橄榄球年度决赛,带动家庭观赛、聚会、酒店和城市活动消费。",
@@ -371,9 +387,11 @@ FESTIVAL_PROFILES = {
GLOBAL_AGGREGATE_KEYS = {
"black-friday", "cyber-monday", "winter-holiday", "christmas", "boxing-day",
"lunar-new-year", "eid-fitr", "eid-adha", "easter", "assumption-day", "new-year", "st-stephens-day",
"lunar-new-year", "eid-fitr", "eid-adha", "easter", "assumption-day", "new-year", "new-year-eve", "st-stephens-day",
}
RANGE_AGGREGATE_KEYS = {"new-year", "eid-fitr", "eid-adha", "lunar-new-year"}
FESTIVAL_ENGLISH_TITLES = {
"back-to-school": "Back to School",
"black-friday": "Black Friday",
@@ -385,6 +403,7 @@ FESTIVAL_ENGLISH_TITLES = {
"eid-fitr": "Eid al-Fitr",
"lunar-new-year": "Lunar New Year",
"new-year": "New Year's Day",
"new-year-eve": "New Year's Eve",
"singles-day": "Singles' Day",
"st-stephens-day": "St. Stephen's Day",
"super-bowl": "Super Bowl",
@@ -393,6 +412,7 @@ FESTIVAL_ENGLISH_TITLES = {
}
GENERIC_HOLIDAY_TITLES = {"国家纪念与公共假日", "地区公共假日"}
UNSAFE_CROSS_COUNTRY_TITLES = {"独立日", "国庆日", "共和国日", "宪法日", "解放日", "胜利日", "纪念日"}
HOLIDAY_TITLE_ALIASES = {"五一劳动节": "劳动节"}
@@ -446,7 +466,9 @@ def festival_key(name: str, source_id: str = "", country_code: str = "") -> str:
return "eid-adha"
if any(token in lower for token in ("easter", "good friday", "pentecost", "whit monday", "复活节", "耶稣受难")):
return "easter"
if any(token in lower for token in ("new year's day", "new year holiday", "元旦", "新年假期")):
if any(token in lower for token in ("new year s eve", "last day of the year", "跨年夜")):
return "new-year-eve"
if any(token in lower for token in ("new year s day", "new year holiday", "new year s holiday", "元旦", "新年假期")):
return "new-year"
if any(token in lower for token in ("memorial", "remembrance", "all souls", "追悼", "纪念")):
return "memorial"
@@ -477,7 +499,12 @@ def calendar_event_type(name: str, source_id: str = "") -> str:
def holiday_title(local_name: str, english_name: str, country_code: str) -> str:
translated = HOLIDAY_TRANSLATIONS.get(english_name.lower())
normalized = _normalized_name(english_name)
if country_code == "ID" and normalized == "independence day":
return "印度尼西亚独立日"
if country_code == "PH" and normalized == "national heroes day":
return "菲律宾国家英雄日"
translated = HOLIDAY_TRANSLATIONS.get(english_name.lower()) or HOLIDAY_TRANSLATIONS.get(_normalized_name(english_name))
if translated:
return translated
if country_code in ("CN", "JP", "KR") and local_name:
@@ -650,17 +677,20 @@ def culture_reference_url(title: str) -> str:
return f"https://zh.wikipedia.org/wiki/{quote(title.replace(' ', '_'), safe='')}"
def culture_reference_cn_url(title: str) -> str:
def culture_reference_cn_url(title: str, festival_key: str = "") -> str:
"""中国大陆访客使用国内网络更易访问的百度百科。"""
return f"https://baike.baidu.com/item/{quote(title, safe='')}"
aliases = {"new-year": "元旦"}
return f"https://baike.baidu.com/item/{quote(aliases.get(festival_key, title), safe='')}"
def visual_reference_url(title: str, country_details: list[dict[str, Any]]) -> str:
def visual_reference_url(title: str, country_details: list[dict[str, Any]], title_english: str = "") -> str:
"""图片入口只搜索节日本身,不复用来源页标题。"""
terms = [title]
for detail in country_details[:2]:
terms.extend((str(detail.get("local_name") or ""), str(detail.get("country") or "")))
query = " ".join(_dedupe(terms))
detail = next((row for row in country_details if row.get("country")), {})
query_title = title_english.strip() or str(detail.get("local_name") or title).strip()
if query_title in GENERIC_HOLIDAY_TITLES or not query_title:
query_title = str(detail.get("local_name") or title).strip()
country = str(detail.get("country_english") or detail.get("country") or "").strip()
query = " ".join(_dedupe([query_title, country, "holiday"]))[:120]
return f"https://www.bing.com/images/search?q={quote(query, safe='')}&setlang=zh-hans"
@@ -668,7 +698,11 @@ def calendar_group_identity(item: dict[str, Any]) -> tuple[str, str]:
"""同日同名的明确节日跨国家聚合;国家纪念类泛称仍按国家拆分。"""
key = str(item.get("festival_key") or "public-holiday")
title = HOLIDAY_TITLE_ALIASES.get(str(item.get("title") or "").strip(), str(item.get("title") or "").strip())
if title and title not in GENERIC_HOLIDAY_TITLES and (key.startswith("public-holiday:") or key == "memorial"):
if title and title not in GENERIC_HOLIDAY_TITLES and (
key.startswith("public-holiday:")
or key == "memorial"
or (key.startswith("national:") and title not in UNSAFE_CROSS_COUNTRY_TITLES)
):
return f"named-holiday:{_normalized_name(title)}", ""
country_partition = "" if key in GLOBAL_AGGREGATE_KEYS else str(item.get("country_code") or item.get("market_area") or item.get("region") or "")
return key, country_partition
@@ -677,6 +711,8 @@ def calendar_group_identity(item: dict[str, Any]) -> tuple[str, str]:
def calendar_group_date(item: dict[str, Any]) -> str:
"""圣诞季共用一张决策卡,其他节日仍按实际日期聚合。"""
start_date = str(item["start_date"])
if item.get("festival_key") in RANGE_AGGREGATE_KEYS:
return f"@{start_date[:4]}"
if item.get("festival_key") != "christmas":
return start_date
value = date.fromisoformat(start_date)
@@ -704,8 +740,15 @@ def aggregate_calendar_insights(items: list[dict[str, Any]], now: date, poster_f
grouped.setdefault((calendar_group_date(item), group_key, country_partition), []).append(item)
insights: list[dict[str, Any]] = []
for (start_date, group_key, _), rows in grouped.items():
rows = sorted(rows, key=lambda row: (str(row.get("start_date")) != start_date, str(row.get("start_date"))))
for (group_date, group_key, _), rows in grouped.items():
range_group = group_date.startswith("@")
start_date = min(str(row.get("start_date") or group_date) for row in rows) if range_group else group_date
rows = sorted(rows, key=lambda row: (
str(row.get("start_date")) != start_date,
0 if str(row.get("country_code") or "") == "CN" else 1,
str(row.get("country_code") or ""),
str(row.get("start_date") or ""),
))
row_keys = _dedupe([str(row.get("festival_key") or "public-holiday") for row in rows])
key = row_keys[0] if len(row_keys) == 1 else f"public-holiday:{group_key.removeprefix('named-holiday:')}"
profile = profile_for_market(key, str(rows[0].get("country_code") or ""))
@@ -715,7 +758,7 @@ def aggregate_calendar_insights(items: list[dict[str, Any]], now: date, poster_f
platforms = _dedupe([value for row in rows for value in row.get("platforms") or []])
titles = [str(row.get("title") or "") for row in rows if row.get("title")]
title = str(profile.get("title") or Counter(titles).most_common(1)[0][0])
if key.startswith("public-holiday:") or key in ("national", "memorial"):
if key.startswith(("public-holiday:", "national:")) or key in ("national", "memorial"):
title = Counter(titles).most_common(1)[0][0]
english_titles = [str(row.get("title_english") or "").strip() for row in rows if row.get("title_english")]
title_english = FESTIVAL_ENGLISH_TITLES.get(key, "")
@@ -822,6 +865,9 @@ def aggregate_calendar_insights(items: list[dict[str, Any]], now: date, poster_f
"label": christmas_date_label(value, start_date) if key == "christmas" else "地区日期",
"countries": variant_countries,
})
end_date = start_date if key == "christmas" else max(str(row.get("end_date") or start_date) for row in rows)
if key == "new-year" and "RU" in country_codes:
end_date = max(end_date, f"{start_date[:4]}-01-08")
insight = {
"id": f"calendar-insight-{digest}",
"section": "calendar",
@@ -842,7 +888,7 @@ def aggregate_calendar_insights(items: list[dict[str, Any]], now: date, poster_f
"change_status": "官方日期已核验",
"confidence": "高" if official_sources else "中高",
"start_date": start_date,
"end_date": start_date if key == "christmas" else max(str(row.get("end_date") or start_date) for row in rows),
"end_date": end_date,
"related_dates": related_dates,
"date_variants": date_variants,
"date_variation_note": (
@@ -876,9 +922,9 @@ def aggregate_calendar_insights(items: list[dict[str, Any]], now: date, poster_f
"days_until": (date.fromisoformat(start_date) - now).days,
"priority_score": max(int(row.get("priority_score") or 0) for row in rows) + min(12, len(countries)),
"analysis_basis": "事实层:官方日期与适用国家;分析层:家纺经营场景推断",
"visual_reference_url": visual_reference_url(title, country_details),
"visual_reference_url": visual_reference_url(title, country_details, title_english),
"culture_reference_url": culture_reference_url(title),
"culture_reference_cn_url": culture_reference_cn_url(title),
"culture_reference_cn_url": culture_reference_cn_url(title, key),
}
translation = {
key: first[key]
@@ -889,6 +935,17 @@ def aggregate_calendar_insights(items: list[dict[str, Any]], now: date, poster_f
)
if key in first
}
if "CN" in country_codes:
translation.update({
"source_language": "zh-CN",
"needs_translation": False,
"google_translation_supported": False,
"translation_url": "",
"translation_provider": "",
"translation_fallback_url": "",
"translation_fallback_provider": "",
"translation_notice": "该节日包含中国市场,中文访客直接查看中文节日资料。",
})
insight.update(translation)
insight["poster"] = poster_for(insight["id"], "calendar", insight["culture_key"])
insights.append(insight)