nomadweb/backend/app/services/recommendations.py
eric ff7b721acb feat: NomadCNA community layer, social matching, payments, and ebook
Port meetups, discussions, gigs, dating/chat, VIP pay (ZPay/XorPay), MiroTalk live, digital academy, and ebook reader. Add persistent community_store, git-first deploy docs, and env template for production secrets.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-30 09:38:13 -05:00

126 lines
3.2 KiB
Python

"""Next-stop recommendation scoring — ported from NomadCNA logic, adapted to nomadro destinations."""
from __future__ import annotations
from typing import Any
def _parse_nomads_count(value: str) -> int:
digits = "".join(ch for ch in str(value) if ch.isdigit())
return int(digits) if digits else 0
def score_destination(
dest: dict[str, Any],
*,
budget: int,
internet: int,
climate: str,
tags: list[str],
priority: str = "balanced",
) -> tuple[float, list[str]]:
"""Return (score, match_reasons) for a destination dict."""
cost = int(dest.get("cost") or 0)
speed = int(dest.get("speed") or 0)
temp = int(dest.get("temperature") or 20)
rating = float(dest.get("rating") or 0)
nomads = _parse_nomads_count(dest.get("nomads_count") or "0")
highlights = dest.get("highlights") or []
tag = str(dest.get("tag") or "")
reasons: list[str] = []
score = rating * 8
# Budget fit (monthly cost in CNY-ish units)
if cost <= budget:
score += 28
reasons.append("预算内")
elif cost <= budget * 1.2:
score += 14
reasons.append("略超预算")
else:
score += max(0, 10 - (cost - budget) / 800)
# Internet
if speed >= internet:
score += 22
reasons.append("网络达标")
else:
score += max(0, 22 - (internet - speed) / 4)
reasons.append("需确认住处网络")
# Climate preference
if climate == "warm":
score += 18 if temp >= 24 else max(0, 18 - (24 - temp) * 2)
if temp >= 24:
reasons.append("气候偏暖")
elif climate == "cool":
score += 18 if temp <= 18 else max(0, 18 - (temp - 18) * 2)
if temp <= 18:
reasons.append("气候清爽")
else: # mild
score += 18 if 18 <= temp <= 26 else max(0, 18 - abs(temp - 22) * 2)
if 18 <= temp <= 26:
reasons.append("气候温和")
# Tag overlap (NomadCNA-style lifestyle tags)
dest_tags = set(highlights + [tag])
overlap = [t for t in tags if any(t in dt or dt in t for dt in dest_tags)]
if overlap:
score += min(15, len(overlap) * 5)
reasons.append(f"标签匹配:{overlap[0]}")
# Community density
if nomads >= 8000:
score += 12
reasons.append("社区活跃")
elif nomads >= 3000:
score += 6
# Priority boost
if priority == "cost":
score += (14000 - cost) / 120
elif priority == "speed":
score += speed / 5
elif priority == "community":
score += nomads / 400
if not reasons:
reasons.append("综合评分推荐")
return round(score, 1), reasons[:3]
def recommend_destinations(
destinations: list[dict[str, Any]],
*,
budget: int = 8000,
internet: int = 50,
climate: str = "mild",
tags: list[str] | None = None,
priority: str = "balanced",
limit: int = 12,
) -> list[dict[str, Any]]:
tags = tags or []
scored: list[tuple[float, dict[str, Any], list[str]]] = []
for dest in destinations:
s, reasons = score_destination(
dest,
budget=budget,
internet=internet,
climate=climate,
tags=tags,
priority=priority,
)
scored.append((s, dest, reasons))
scored.sort(key=lambda x: x[0], reverse=True)
items = []
for s, dest, reasons in scored[:limit]:
items.append({
**dest,
"match_score": s,
"match_reasons": reasons,
})
return items