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>
126 lines
3.2 KiB
Python
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
|