diff --git a/frontend/components/account/AccountCenter.tsx b/frontend/components/account/AccountCenter.tsx index cc806241..dbf82d8a 100644 --- a/frontend/components/account/AccountCenter.tsx +++ b/frontend/components/account/AccountCenter.tsx @@ -2602,7 +2602,7 @@ export function AccountCenter() { Top 1 - +500 积分 & 7天Pro + +200 积分 & 7天Pro
- 积分规则:群内有效发言(自动防刷检测)。每周一零点结算并重置周积分榜。 + 积分规则:群内有效发言(自动防刷检测)+ 每日首条发言额外奖励。每周一零点结算周榜,所有活跃用户均享参与奖。
/markets。\n\n"
"🔐 /city 与 /deb 仅限官方群成员使用。\n\n"
"示例: /city 伦敦 或 /pwcity 伦敦\n"
- f"💡 提示: 每日签到(有效发言满 {MESSAGE_MIN_LENGTH} 字)获得 {MESSAGE_POINTS} 积分,"
- f"每日上限 {MESSAGE_DAILY_CAP} 分。"
+ f"💡 提示: 群内有效发言(满 {MESSAGE_MIN_LENGTH} 字)获得 {MESSAGE_POINTS} 积分,"
+ f"每日上限 {MESSAGE_DAILY_CAP} 分。"
+ f"首次发言额外奖励 20 积分,每日首条消息 +2 积分。"
)
def build_points_rank_text(self, user: Any) -> str:
@@ -225,6 +228,10 @@ class BotIOLayer:
f"{weekly_rank}/{ranked_count}" if weekly_rank and ranked_count > 0 else "未上榜"
)
+ daily_queries_date = str(user_info.get("daily_queries_date") or "")
+ city_used = int(user_info.get("daily_city_queries") or 0) if daily_queries_date == today_str else 0
+ deb_used = int(user_info.get("daily_deb_queries") or 0) if daily_queries_date == today_str else 0
+
rank_text += "────────────────────\n"
rank_text += (
"👤 我的状态:\n"
@@ -233,7 +240,7 @@ class BotIOLayer:
f"┣ 本周排名: {weekly_rank_text}\n"
f"┣ 本周发言积分: {weekly_points}\n"
f"┣ 今日发言积分: {daily_points}/{MESSAGE_DAILY_CAP}\n"
- f"┗ /city 消耗: {CITY_QUERY_COST} | /deb 消耗: {DEB_QUERY_COST}"
+ f"┗ /city 免费 ({city_used}/{CITY_DAILY_FREE_LIMIT}) | /deb 免费 ({deb_used}/{DEB_DAILY_FREE_LIMIT})"
)
return rank_text
diff --git a/src/bot/settings.py b/src/bot/settings.py
index bb48c62f..196ed911 100644
--- a/src/bot/settings.py
+++ b/src/bot/settings.py
@@ -20,5 +20,14 @@ MESSAGE_MIN_LENGTH = _env_int("POLYWEATHER_BOT_MESSAGE_MIN_LENGTH", 3, min_value
MESSAGE_COOLDOWN_SEC = _env_int("POLYWEATHER_BOT_MESSAGE_COOLDOWN_SEC", 30, min_value=0)
# Optional per-chat override map, parsed in BotIOLayer:
# POLYWEATHER_BOT_MESSAGE_COOLDOWN_BY_CHAT="-1003586303099:10,-1003539418691:20"
-CITY_QUERY_COST = _env_int("POLYWEATHER_BOT_CITY_QUERY_COST", 2, min_value=0)
-DEB_QUERY_COST = _env_int("POLYWEATHER_BOT_DEB_QUERY_COST", 2, min_value=0)
+CITY_QUERY_COST = _env_int("POLYWEATHER_BOT_CITY_QUERY_COST", 0, min_value=0)
+DEB_QUERY_COST = _env_int("POLYWEATHER_BOT_DEB_QUERY_COST", 0, min_value=0)
+CITY_DAILY_FREE_LIMIT = _env_int("POLYWEATHER_BOT_CITY_DAILY_FREE_LIMIT", 10, min_value=1)
+DEB_DAILY_FREE_LIMIT = _env_int("POLYWEATHER_BOT_DEB_DAILY_FREE_LIMIT", 10, min_value=1)
+
+FIRST_MESSAGE_BONUS = _env_int("POLYWEATHER_BOT_FIRST_MESSAGE_BONUS", 2, min_value=0)
+WELCOME_BONUS = _env_int("POLYWEATHER_BOT_WELCOME_BONUS", 20, min_value=0)
+
+WEEKLY_PARTICIPATION_BONUS = _env_int("POLYWEATHER_BOT_WEEKLY_PARTICIPATION_BONUS", 5, min_value=0)
+WEEKLY_ACTIVE_BONUS = _env_int("POLYWEATHER_BOT_WEEKLY_ACTIVE_BONUS", 15, min_value=0)
+WEEKLY_ACTIVE_THRESHOLD = _env_int("POLYWEATHER_BOT_WEEKLY_ACTIVE_THRESHOLD", 20, min_value=1)
diff --git a/src/bot/weekly_reward_loop.py b/src/bot/weekly_reward_loop.py
index c2174960..b0f69c35 100644
--- a/src/bot/weekly_reward_loop.py
+++ b/src/bot/weekly_reward_loop.py
@@ -14,6 +14,11 @@ except Exception: # pragma: no cover - Python < 3.9 fallback
import requests
from loguru import logger
+from src.bot.settings import (
+ WEEKLY_ACTIVE_BONUS,
+ WEEKLY_ACTIVE_THRESHOLD,
+ WEEKLY_PARTICIPATION_BONUS,
+)
from src.database.db_manager import DBManager
from src.utils.telegram_chat_ids import get_telegram_chat_ids_from_env
@@ -69,11 +74,11 @@ def _compute_target_week_key(
def _reward_rule_for_rank(rank: int) -> Optional[Tuple[int, int]]:
if rank == 1:
- return 500, 7
+ return 200, 7
if rank in (2, 3):
- return 300, 3
+ return 100, 3
if 4 <= rank <= 10:
- return 150, 0
+ return 50, 0
return None
@@ -170,10 +175,12 @@ def _render_settle_report(
week_key: str,
winners: List[Dict[str, Any]],
skipped: int,
+ participation_count: int = 0,
+ active_count: int = 0,
) -> str:
lines = [f"🏆 PolyWeather 周榜奖励已结算 ({week_key})", "────────────────────"]
if not winners:
- lines.append("本周无有效活跃用户,未发放奖励。")
+ lines.append("本周无上榜用户。")
else:
medals = {1: "🥇", 2: "🥈", 3: "🥉"}
for row in winners:
@@ -186,6 +193,11 @@ def _render_settle_report(
lines.append(f"{medal} {name}: +{points_bonus} 积分{pro_text}")
if skipped > 0:
lines.append(f"(重复发放保护已生效,跳过 {skipped} 条)")
+ if participation_count > 0 or active_count > 0:
+ lines.append("────────────────────")
+ lines.append(f"🎁 参与奖 +{WEEKLY_PARTICIPATION_BONUS} 积分: {participation_count} 人")
+ if active_count > 0:
+ lines.append(f"🔥 活跃奖 +{WEEKLY_ACTIVE_BONUS} 积分 (周积分≥{WEEKLY_ACTIVE_THRESHOLD}): {active_count} 人")
return "\n".join(lines)
@@ -302,8 +314,39 @@ def _runner(bot: Any) -> None:
"winner_count": len(winners),
"skipped_count": skipped,
"winners": winners,
+ "participation_count": 0,
+ "active_count": 0,
"settled_at": datetime.now(timezone.utc).isoformat(),
}
+
+ winner_ids = {int(w.get("telegram_id") or 0) for w in winners}
+ participants = db.get_weekly_participation_candidates(week_key, winner_ids)
+ participation_count = 0
+ active_count = 0
+ for p in participants:
+ tid = int(p.get("telegram_id") or 0)
+ if tid <= 0:
+ continue
+ weekly_pts = int(p.get("weekly_points") or 0)
+ is_active = weekly_pts >= WEEKLY_ACTIVE_THRESHOLD
+ bonus = WEEKLY_PARTICIPATION_BONUS + (WEEKLY_ACTIVE_BONUS if is_active else 0)
+ if bonus <= 0:
+ continue
+ applied = db.apply_weekly_reward_payout(
+ week_key=week_key,
+ telegram_id=tid,
+ rank=0,
+ username=str(p.get("username") or f"user_{tid}"),
+ points_bonus=bonus,
+ pro_days=0,
+ )
+ if applied:
+ participation_count += 1
+ if is_active:
+ active_count += 1
+
+ summary["participation_count"] = participation_count
+ summary["active_count"] = active_count
db.mark_weekly_reward_settled(
week_key=week_key,
winners_count=len(winners),
@@ -316,7 +359,13 @@ def _runner(bot: Any) -> None:
skipped,
)
if announce and chat_ids:
- message = _render_settle_report(week_key=week_key, winners=winners, skipped=skipped)
+ message = _render_settle_report(
+ week_key=week_key,
+ winners=winners,
+ skipped=skipped,
+ participation_count=participation_count,
+ active_count=active_count,
+ )
for chat_id in chat_ids:
try:
bot.send_message(
diff --git a/src/database/db_manager.py b/src/database/db_manager.py
index b8bb67c0..f177aebc 100644
--- a/src/database/db_manager.py
+++ b/src/database/db_manager.py
@@ -286,6 +286,10 @@ class DBManager:
self._ensure_column(conn, "users", "weekly_points_week", "TEXT")
self._ensure_column(conn, "users", "supabase_user_id", "TEXT")
self._ensure_column(conn, "users", "supabase_email", "TEXT")
+ self._ensure_column(conn, "users", "welcome_bonus_claimed", "INTEGER DEFAULT 0")
+ self._ensure_column(conn, "users", "daily_city_queries", "INTEGER DEFAULT 0")
+ self._ensure_column(conn, "users", "daily_deb_queries", "INTEGER DEFAULT 0")
+ self._ensure_column(conn, "users", "daily_queries_date", "TEXT")
# Migrate legacy one-to-one binding column into mapping table.
conn.execute(
"""
@@ -785,6 +789,16 @@ class DBManager:
(int(telegram_id), wk, pts, datetime.now().isoformat()),
)
+ @staticmethod
+ def _read_bonus_config(env_key: str, fallback: int) -> int:
+ raw = os.getenv(env_key)
+ if raw is None or raw.strip() == "":
+ return fallback
+ try:
+ return max(0, int(raw))
+ except Exception:
+ return fallback
+
def _ensure_column(self, conn: sqlite3.Connection, table: str, column: str, ddl: str) -> None:
existing = {
row[1]
@@ -1253,7 +1267,8 @@ class DBManager:
)
cursor = conn.execute(
"""
- SELECT points, daily_points, daily_points_date, weekly_points, weekly_points_week, last_message_at
+ SELECT points, daily_points, daily_points_date, weekly_points, weekly_points_week, last_message_at,
+ message_count, welcome_bonus_claimed
FROM users WHERE telegram_id = ?
""",
(telegram_id,),
@@ -1334,6 +1349,7 @@ class DBManager:
remaining = max(0, daily_cap - daily_points)
points_added = min(max(0, points_to_add), remaining)
if points_added <= 0:
+ conn.commit()
return {
"awarded": False,
"reason": "daily_cap",
@@ -1341,23 +1357,38 @@ class DBManager:
"weekly_points": weekly_points,
}
+ welcome_bonus = 0
+ first_message_bonus = 0
+
+ is_first_message_of_day = daily_points == 0
+ is_new_user = int(row["message_count"] or 0) == 0 and not int(row["welcome_bonus_claimed"] or 0)
+
+ if is_new_user:
+ welcome_bonus = self._read_bonus_config("POLYWEATHER_BOT_WELCOME_BONUS", 20)
+ if is_first_message_of_day:
+ first_message_bonus = self._read_bonus_config("POLYWEATHER_BOT_FIRST_MESSAGE_BONUS", 2)
+
+ total_added = points_added + welcome_bonus + first_message_bonus
+
conn.execute("""
- UPDATE users
+ UPDATE users
SET message_count = message_count + 1,
points = points + ?,
daily_points = ?,
daily_points_date = ?,
weekly_points = ?,
weekly_points_week = ?,
- last_message_at = ?
+ last_message_at = ?,
+ welcome_bonus_claimed = MAX(welcome_bonus_claimed, ?)
WHERE telegram_id = ?
""", (
- points_added,
- daily_points + points_added,
+ total_added,
+ daily_points + total_added,
today_str,
- weekly_points + points_added,
+ weekly_points + total_added,
week_key,
now.isoformat(),
+ 1 if welcome_bonus > 0 else 0,
telegram_id,
))
conn.execute(
@@ -1372,18 +1403,56 @@ class DBManager:
conn,
telegram_id=telegram_id,
week_key=week_key,
- points=weekly_points + points_added,
+ points=weekly_points + total_added,
)
conn.commit()
return {
"awarded": True,
"reason": "ok",
"points_added": points_added,
- "daily_points": daily_points + points_added,
- "weekly_points": weekly_points + points_added,
+ "welcome_bonus": welcome_bonus,
+ "first_message_bonus": first_message_bonus,
+ "total_added": total_added,
+ "daily_points": daily_points + total_added,
+ "weekly_points": weekly_points + total_added,
"weekly_week": week_key,
}
+ def track_query_usage(self, telegram_id: int, query_type: str) -> Dict[str, Any]:
+ today_str = datetime.now().strftime("%Y-%m-%d")
+ column = "daily_city_queries" if query_type == "city" else "daily_deb_queries"
+ limit = (
+ self._read_bonus_config("POLYWEATHER_BOT_CITY_DAILY_FREE_LIMIT", 10)
+ if query_type == "city"
+ else self._read_bonus_config("POLYWEATHER_BOT_DEB_DAILY_FREE_LIMIT", 10)
+ )
+ with self._get_connection() as conn:
+ conn.row_factory = sqlite3.Row
+ row = conn.execute(
+ f"SELECT {column}, daily_queries_date FROM users WHERE telegram_id = ?",
+ (telegram_id,),
+ ).fetchone()
+ if not row:
+ return {"allowed": False, "reason": "user_missing", "used": 0, "limit": limit}
+
+ date = row["daily_queries_date"] or ""
+ used = int(row[column] or 0) if date == today_str else 0
+
+ if used >= limit:
+ return {"allowed": False, "reason": "daily_limit", "used": used, "limit": limit}
+
+ new_used = used + 1
+ conn.execute(
+ f"""
+ UPDATE users
+ SET {column} = ?, daily_queries_date = ?
+ WHERE telegram_id = ?
+ """,
+ (new_used, today_str, telegram_id),
+ )
+ conn.commit()
+ return {"allowed": True, "used": new_used, "limit": limit}
+
def spend_points(self, telegram_id: int, amount: int) -> Dict[str, Any]:
if amount <= 0:
user = self.get_user(telegram_id)
@@ -1624,6 +1693,36 @@ class DBManager:
).fetchall()
return [dict(row) for row in rows]
+ def get_weekly_participation_candidates(self, week_key: str, exclude_ids: set):
+ wk = self._safe_week_key(week_key)
+ if not wk:
+ return []
+ with self._get_connection() as conn:
+ conn.row_factory = sqlite3.Row
+ rows = conn.execute(
+ """
+ SELECT *
+ FROM (
+ SELECT
+ u.telegram_id,
+ u.username,
+ COALESCE(a.points,
+ CASE
+ WHEN u.weekly_points_week = ? THEN COALESCE(u.weekly_points, 0)
+ ELSE 0
+ END
+ ) AS weekly_points
+ FROM users u
+ LEFT JOIN weekly_points_archive a
+ ON a.telegram_id = u.telegram_id
+ AND a.week_key = ?
+ ) ranked
+ WHERE weekly_points > 0
+ """,
+ (wk, wk),
+ ).fetchall()
+ return [dict(row) for row in rows if int(row["telegram_id"] or 0) not in exclude_ids]
+
def is_weekly_reward_settled(self, week_key: str) -> bool:
wk = self._safe_week_key(week_key)
if not wk: