'env'
This commit is contained in:
@@ -7,6 +7,7 @@ import json
|
|||||||
import logging
|
import logging
|
||||||
import re
|
import re
|
||||||
import time
|
import time
|
||||||
|
from datetime import datetime, timezone, timedelta
|
||||||
from urllib.parse import parse_qs, unquote, urlparse
|
from urllib.parse import parse_qs, unquote, urlparse
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
@@ -18,6 +19,12 @@ from ..config import PB_URL, PB_ADMIN_EMAIL, PB_ADMIN_PASSWORD
|
|||||||
SITE_ID = "salon"
|
SITE_ID = "salon"
|
||||||
router = APIRouter(prefix="/api/salon", tags=["salon"])
|
router = APIRouter(prefix="/api/salon", tags=["salon"])
|
||||||
DEFAULT_PASSWORD = "12345678"
|
DEFAULT_PASSWORD = "12345678"
|
||||||
|
CHINA_TZ = timezone(timedelta(hours=8))
|
||||||
|
|
||||||
|
|
||||||
|
def _now_china_str() -> str:
|
||||||
|
"""当前中国公历时间,格式 YYYY-MM-DD HH:mm:ss,用于 submitted_at_cn 上传"""
|
||||||
|
return datetime.now(CHINA_TZ).strftime("%Y-%m-%d %H:%M:%S")
|
||||||
|
|
||||||
_SALON_ADMIN_TOKEN_CACHE: tuple[str, float] | None = None
|
_SALON_ADMIN_TOKEN_CACHE: tuple[str, float] | None = None
|
||||||
_TOKEN_CACHE_TTL = 300
|
_TOKEN_CACHE_TTL = 300
|
||||||
@@ -625,6 +632,72 @@ async def join_volunteers():
|
|||||||
return await asyncio.to_thread(_fetch_volunteers_sync, token)
|
return await asyncio.to_thread(_fetch_volunteers_sync, token)
|
||||||
|
|
||||||
|
|
||||||
|
def _get_this_week_range_china() -> tuple[str, str]:
|
||||||
|
"""中国公历本周一 00:00 至周日 23:59:59 的日期字符串"""
|
||||||
|
now = datetime.now(CHINA_TZ)
|
||||||
|
days_to_monday = now.weekday() # Mon=0, Sun=6
|
||||||
|
monday = now - timedelta(days=days_to_monday)
|
||||||
|
sunday = monday + timedelta(days=6)
|
||||||
|
start = monday.strftime("%Y-%m-%d 00:00:00")
|
||||||
|
end = sunday.strftime("%Y-%m-%d 23:59:59")
|
||||||
|
return start, end
|
||||||
|
|
||||||
|
|
||||||
|
def _get_this_week_range_iso() -> tuple[str, str]:
|
||||||
|
"""中国公历本周的 UTC 时间范围,用于 created 过滤。PocketBase 要求 Y-m-d H:i:s.uZ 格式(空格非 T)"""
|
||||||
|
now = datetime.now(CHINA_TZ)
|
||||||
|
days_to_monday = now.weekday()
|
||||||
|
monday = now - timedelta(days=days_to_monday)
|
||||||
|
sunday = monday + timedelta(days=6)
|
||||||
|
monday_start = monday.replace(hour=0, minute=0, second=0, microsecond=0)
|
||||||
|
sunday_end = sunday.replace(hour=23, minute=59, second=59, microsecond=999999)
|
||||||
|
start_utc = monday_start.astimezone(timezone.utc).strftime("%Y-%m-%d %H:%M:%S.000Z")
|
||||||
|
end_utc = sunday_end.astimezone(timezone.utc).strftime("%Y-%m-%d %H:%M:%S.999Z")
|
||||||
|
return start_utc, end_utc
|
||||||
|
|
||||||
|
|
||||||
|
def _fetch_stats_sync() -> dict:
|
||||||
|
"""同步获取报名统计,按场次返回 count 和 joiners。使用 get_pb_client 与 by-wechat 一致"""
|
||||||
|
try:
|
||||||
|
from ..services import get_pb_client
|
||||||
|
pb_client = get_pb_client()
|
||||||
|
solan = pb_client.collection("solanRed")
|
||||||
|
except Exception as e:
|
||||||
|
logging.warning(f"salon join stats 初始化失败: {e}")
|
||||||
|
return {"ok": True, "stats": {"session-1": {"count": 0, "joiners": []}, "session-2": {"count": 0, "joiners": []}}}
|
||||||
|
sessions = [
|
||||||
|
{"id": "session-1", "day_keyword": "周六"},
|
||||||
|
{"id": "session-2", "day_keyword": "周日"},
|
||||||
|
]
|
||||||
|
result = {}
|
||||||
|
_get = lambda r, k, d=None: getattr(r, k, d) if hasattr(r, k) else (r.get(k, d) if isinstance(r, dict) else d)
|
||||||
|
for s in sessions:
|
||||||
|
sid = s["id"]
|
||||||
|
day_kw = s["day_keyword"]
|
||||||
|
meetupdate_filter = f'(meetupdate ~ "{day_kw}" || meetupdate = "")' if sid == "session-1" else f'meetupdate ~ "{day_kw}"'
|
||||||
|
try:
|
||||||
|
existing = solan.get_list(1, 6, {"filter": meetupdate_filter, "sort": "-created"})
|
||||||
|
items = list(existing.items) if existing.items else []
|
||||||
|
total = existing.total_items if hasattr(existing, "total_items") else len(items)
|
||||||
|
joiners = []
|
||||||
|
for r in items:
|
||||||
|
nickname = str(_get(r, "xiaohongshu_nickname") or _get(r, "nickname") or "").strip() or "匿名"
|
||||||
|
avatar = str(_get(r, "xiaohongshu_avatar") or _get(r, "media") or "").strip() or None
|
||||||
|
xh_url = str(_get(r, "xiaohongshu_url") or "").strip() or None
|
||||||
|
joiners.append({"nickname": nickname, "avatar": avatar, "xiaohongshu_url": xh_url})
|
||||||
|
result[sid] = {"count": total, "joiners": joiners}
|
||||||
|
except Exception as e:
|
||||||
|
logging.warning(f"salon join stats 查询失败 {sid}: {e}")
|
||||||
|
result[sid] = {"count": 0, "joiners": []}
|
||||||
|
return {"ok": True, "stats": result}
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/join/stats")
|
||||||
|
async def join_stats():
|
||||||
|
"""按场次返回报名人数和最近报名者"""
|
||||||
|
return await asyncio.to_thread(_fetch_stats_sync)
|
||||||
|
|
||||||
|
|
||||||
class XiaohongshuProfileRequest(BaseModel):
|
class XiaohongshuProfileRequest(BaseModel):
|
||||||
url: str | None = None
|
url: str | None = None
|
||||||
id: str | None = None
|
id: str | None = None
|
||||||
@@ -1501,11 +1574,13 @@ async def submit_join(item: dict):
|
|||||||
pb_client = get_pb_client()
|
pb_client = get_pb_client()
|
||||||
solan = pb_client.collection("solanRed")
|
solan = pb_client.collection("solanRed")
|
||||||
raw_type = item.get("type", "")
|
raw_type = item.get("type", "")
|
||||||
# PocketBase type 仅接受 session-1/session-2 等,digital-nomad/volunteer 映射为空
|
# PocketBase type 字段为 select 枚举,可能仅接受 digital-nomad/volunteer 等,session-1/session-2 会报 validation_invalid_value
|
||||||
pb_type = raw_type if raw_type in ("session-1", "session-2") else ""
|
# 场次信息存 meetupdate,stats 按 meetupdate 过滤
|
||||||
|
pb_type = ""
|
||||||
|
|
||||||
qualify = bool(item.get("qualify", False)) # 女性且小红书帖子>10 时自动通过
|
qualify = bool(item.get("qualify", False)) # 女性且小红书帖子>10 时自动通过
|
||||||
user_info_val = (item.get("userInfo") or "").strip()
|
user_info_val = (item.get("userInfo") or "").strip()
|
||||||
|
meetupdate_val = str(item.get("meetupdate", "") or "").strip()
|
||||||
pb_data = {
|
pb_data = {
|
||||||
"user_id": user_id,
|
"user_id": user_id,
|
||||||
"nickname": item.get("nickname", ""),
|
"nickname": item.get("nickname", ""),
|
||||||
@@ -1528,6 +1603,8 @@ async def submit_join(item: dict):
|
|||||||
"age_range": item.get("age_range", ""),
|
"age_range": item.get("age_range", ""),
|
||||||
"first_time": item.get("first_time", ""),
|
"first_time": item.get("first_time", ""),
|
||||||
"userInfo": user_info_val,
|
"userInfo": user_info_val,
|
||||||
|
"meetupdate": meetupdate_val,
|
||||||
|
"submitted_at_cn": _now_china_str(),
|
||||||
}
|
}
|
||||||
phone_val = item.get("phone", "")
|
phone_val = item.get("phone", "")
|
||||||
if phone_val and str(phone_val).strip():
|
if phone_val and str(phone_val).strip():
|
||||||
|
|||||||
Reference in New Issue
Block a user