feat: revamp points/reward system for inclusive engagement
- /city /deb now free, capped at 10/day each (was 2 pts cost) - Welcome bonus +20 pts on first-ever valid message - First-message-of-day bonus +2 pts - Weekly winner point bonuses reduced (500→200, 300→100, 150→50) - Weekly participation rewards for all active users (+5 base, +15 for ≥20 pts) - Pro-day rewards for top 3 unchanged Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -2602,7 +2602,7 @@ export function AccountCenter() {
|
||||
Top 1
|
||||
</span>
|
||||
<span className="text-xs font-bold text-yellow-500">
|
||||
+500 积分 & 7天Pro
|
||||
+200 积分 & 7天Pro
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between p-3 bg-white/5 rounded-xl border border-white/5">
|
||||
@@ -2613,7 +2613,7 @@ export function AccountCenter() {
|
||||
Top 2-3
|
||||
</span>
|
||||
<span className="text-xs font-bold text-slate-300">
|
||||
+300 积分 & 3天Pro
|
||||
+100 积分 & 3天Pro
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between p-3 bg-white/5 rounded-xl border border-white/5">
|
||||
@@ -2624,7 +2624,7 @@ export function AccountCenter() {
|
||||
Top 4-10
|
||||
</span>
|
||||
<span className="text-xs font-bold text-orange-400">
|
||||
+150 积分
|
||||
+50 积分
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -2632,7 +2632,7 @@ export function AccountCenter() {
|
||||
<div className="mt-6 flex items-start gap-2 p-3 bg-black/20 rounded-xl">
|
||||
<Info size={14} className="text-slate-500 mt-0.5 shrink-0" />
|
||||
<p className="text-[10px] text-slate-500 leading-normal italic">
|
||||
积分规则:群内有效发言(自动防刷检测)。每周一零点结算并重置周积分榜。
|
||||
积分规则:群内有效发言(自动防刷检测)+ 每日首条发言额外奖励。每周一零点结算周榜,所有活跃用户均享参与奖。
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -79,6 +79,24 @@ class CommandGuard:
|
||||
self._reply_group_required(message)
|
||||
return False
|
||||
|
||||
def check_daily_query_limit(self, message: Any, query_type: str, limit: int) -> bool:
|
||||
user = message.from_user
|
||||
result = self.io_layer.db.track_query_usage(user.id, query_type)
|
||||
if result.get("allowed"):
|
||||
return True
|
||||
if result.get("reason") == "user_missing":
|
||||
self.io_layer.bot.reply_to(message, "⚠️ 用户未注册,请先在群内发言。")
|
||||
return False
|
||||
if result.get("reason") == "daily_limit":
|
||||
used = int(result.get("used") or 0)
|
||||
self.io_layer.send_query_message(
|
||||
message,
|
||||
f"❌ 今日 <b>/{query_type}</b> 免费次数已用完 ({used}/{limit})\n请明天再来,或通过群内发言赚取积分。",
|
||||
parse_mode="HTML",
|
||||
)
|
||||
return False
|
||||
return True
|
||||
|
||||
def ensure_access_and_points(self, message: Any, cost: int, command_label: str) -> bool:
|
||||
if not self.ensure_group_member(message, command_label):
|
||||
return False
|
||||
|
||||
@@ -10,7 +10,7 @@ from src.bot.command_guard import CommandGuard
|
||||
from src.bot.io_layer import BotIOLayer
|
||||
from src.bot.observability import CommandTrace
|
||||
from src.bot.services.city_command_service import CityCommandService
|
||||
from src.bot.settings import CITY_QUERY_COST
|
||||
from src.bot.settings import CITY_DAILY_FREE_LIMIT, CITY_QUERY_COST
|
||||
|
||||
def _is_city_command(message: Any) -> bool:
|
||||
command = extract_command_name(
|
||||
@@ -77,6 +77,9 @@ class CityCommandHandler:
|
||||
return
|
||||
|
||||
city_name = str(resolved.city_name)
|
||||
if not self.guard.check_daily_query_limit(message, "city", CITY_DAILY_FREE_LIMIT):
|
||||
trace.set_status("blocked", "daily_query_limit")
|
||||
return
|
||||
if not self.guard.ensure_access_and_points(message, CITY_QUERY_COST, "/city"):
|
||||
trace.set_status("blocked", "guard_rejected")
|
||||
return
|
||||
|
||||
@@ -10,7 +10,7 @@ from src.bot.command_guard import CommandGuard
|
||||
from src.bot.io_layer import BotIOLayer
|
||||
from src.bot.observability import CommandTrace
|
||||
from src.bot.services.deb_command_service import DebCommandService
|
||||
from src.bot.settings import DEB_QUERY_COST
|
||||
from src.bot.settings import DEB_DAILY_FREE_LIMIT, DEB_QUERY_COST
|
||||
|
||||
def _is_deb_command(message: Any) -> bool:
|
||||
command = extract_command_name(
|
||||
@@ -75,6 +75,9 @@ class DebCommandHandler:
|
||||
)
|
||||
return
|
||||
|
||||
if not self.guard.check_daily_query_limit(message, "deb", DEB_DAILY_FREE_LIMIT):
|
||||
trace.set_status("blocked", "daily_query_limit")
|
||||
return
|
||||
if not self.guard.ensure_access_and_points(message, DEB_QUERY_COST, "/deb"):
|
||||
trace.set_status("blocked", "guard_rejected")
|
||||
return
|
||||
|
||||
+12
-5
@@ -7,7 +7,9 @@ from typing import Any, Dict, Optional, Tuple
|
||||
from loguru import logger
|
||||
|
||||
from src.bot.settings import (
|
||||
CITY_DAILY_FREE_LIMIT,
|
||||
CITY_QUERY_COST,
|
||||
DEB_DAILY_FREE_LIMIT,
|
||||
DEB_QUERY_COST,
|
||||
MESSAGE_COOLDOWN_SEC,
|
||||
MESSAGE_DAILY_CAP,
|
||||
@@ -176,8 +178,8 @@ class BotIOLayer:
|
||||
return (
|
||||
"🚀 <b>PolyWeather 天气查询机器人</b>\n\n"
|
||||
"可用指令:\n"
|
||||
f"/city [城市名] 或 /pwcity [城市名] - 查询城市天气预测与实测 (消耗 {CITY_QUERY_COST} 积分)\n"
|
||||
f"/deb [城市名] 或 /pwdeb [城市名] - 查看 DEB 融合预测准确率 (消耗 {DEB_QUERY_COST} 积分)\n"
|
||||
f"/city [城市名] 或 /pwcity [城市名] - 查询城市天气预测与实测 (免费, 每日 {CITY_DAILY_FREE_LIMIT} 次)\n"
|
||||
f"/deb [城市名] 或 /pwdeb [城市名] - 查看 DEB 融合预测准确率 (免费, 每日 {DEB_DAILY_FREE_LIMIT} 次)\n"
|
||||
"/markets - 私聊机器人查看当前市场监控摘要\n"
|
||||
"/top - 查看积分排行榜\n"
|
||||
"/id - 获取当前聊天的 Chat ID\n\n"
|
||||
@@ -189,8 +191,9 @@ class BotIOLayer:
|
||||
"📌 <i>私有频道用于接收自动推送;手动查看市场概览请私聊机器人发送 <code>/markets</code>。</i>\n\n"
|
||||
"🔐 <i>/city 与 /deb 仅限官方群成员使用。</i>\n\n"
|
||||
"示例: <code>/city 伦敦</code> 或 <code>/pwcity 伦敦</code>\n"
|
||||
f"💡 <i>提示: 每日签到(有效发言满 {MESSAGE_MIN_LENGTH} 字)获得 <b>{MESSAGE_POINTS}</b> 积分,"
|
||||
f"每日上限 {MESSAGE_DAILY_CAP} 分。</i>"
|
||||
f"💡 <i>提示: 群内有效发言(满 {MESSAGE_MIN_LENGTH} 字)获得 <b>{MESSAGE_POINTS}</b> 积分,"
|
||||
f"每日上限 {MESSAGE_DAILY_CAP} 分。"
|
||||
f"首次发言额外奖励 <b>20</b> 积分,每日首条消息 +<b>2</b> 积分。</i>"
|
||||
)
|
||||
|
||||
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 += (
|
||||
"👤 <b>我的状态:</b>\n"
|
||||
@@ -233,7 +240,7 @@ class BotIOLayer:
|
||||
f"┣ 本周排名: <code>{weekly_rank_text}</code>\n"
|
||||
f"┣ 本周发言积分: <code>{weekly_points}</code>\n"
|
||||
f"┣ 今日发言积分: <code>{daily_points}/{MESSAGE_DAILY_CAP}</code>\n"
|
||||
f"┗ /city 消耗: <code>{CITY_QUERY_COST}</code> | /deb 消耗: <code>{DEB_QUERY_COST}</code>"
|
||||
f"┗ /city 免费 ({city_used}/{CITY_DAILY_FREE_LIMIT}) | /deb 免费 ({deb_used}/{DEB_DAILY_FREE_LIMIT})"
|
||||
)
|
||||
return rank_text
|
||||
|
||||
|
||||
+11
-2
@@ -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)
|
||||
|
||||
@@ -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"🏆 <b>PolyWeather 周榜奖励已结算 ({week_key})</b>", "────────────────────"]
|
||||
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(
|
||||
|
||||
+108
-9
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user