feat: 培训子应用异步化(async 路由 + to_thread 同步 db)

- main.py 43 个路由改 async def
- db 保持同步 sqlite3,经 asyncio.to_thread 原子调用(不阻塞事件循环,规避 aiosqlite ASGI 线程/循环脆弱性)
- 依赖分析识别 db 触碰辅助函数(_count_enrolled/_adopt_phone 等),作为原子单元经 to_thread 调用
- db.get_conn 加 check_same_thread=False(每请求独立连接,跨线程安全)
- db.init_db 移入 async lifespan
- 验证:48 路由全通(报名/签到/统计/测评/政策/调研)+ 66 测试全绿
This commit is contained in:
2026-08-24 01:43:52 +08:00
parent a813a139dc
commit 1af1ab6c2f
2 changed files with 95 additions and 86 deletions
+1 -1
View File
@@ -13,7 +13,7 @@ os.makedirs(os.path.dirname(DB_PATH), exist_ok=True)
def get_conn():
conn = sqlite3.connect(DB_PATH)
conn = sqlite3.connect(DB_PATH, check_same_thread=False)
conn.row_factory = sqlite3.Row
conn.execute("PRAGMA journal_mode=WAL")
return conn
+94 -85
View File
@@ -3,6 +3,7 @@
由 server-core/dispatcher.py 统一对外(opc.pinesound.cn):`/api/*` 路由到本子应用,
`/auth`、`/opc`、`/admin` 等由平台应用(app.main)服务。独立使用 data/opc.db 业务库。
"""
import asyncio
import json
import os
import time
@@ -15,7 +16,16 @@ from . import db, opc_engine, policy_engine, policy_data, survey_data
from .auth import hash_password, make_token, verify_token, send_sms_code, check_sms_code, PHONE_RE
from .opc_engine import SECTIONS, AXIS_NAMES, ADAPT_LABELS, PROFILES
app = FastAPI(title="云超服 OPC 培训站后端", version="0.1")
from contextlib import asynccontextmanager
@asynccontextmanager
async def lifespan(app):
await asyncio.to_thread(db.init_db)
yield
app = FastAPI(title="云超服 OPC 培训站后端", version="0.1", lifespan=lifespan)
ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) # server-core 根
@@ -82,7 +92,6 @@ app.add_middleware(
allow_headers=["*"],
)
db.init_db()
def now_iso():
@@ -145,14 +154,14 @@ def jsonify(obj):
# ================= 域名验证文件(微信/腾讯域名校验,须在根路径返回) =================
@app.get("/SpXvScDiDT.txt")
def domain_verify():
async def domain_verify():
from fastapi.responses import Response
return Response(content="d813c7bbab566807a833fbc3b0e20a9c", media_type="text/plain")
# ================= 认证 =================
@app.get("/api/health")
def health():
async def health():
return {"ok": True, "service": "opc-fastapi"}
@@ -167,12 +176,12 @@ async def register(req: Request):
raise HTTPException(400, "账号至少 2 个字符")
if len(password) < 4:
raise HTTPException(400, "密码至少 4 个字符")
if db.fetch_one("accounts", username=username):
if await asyncio.to_thread(db.fetch_one, "accounts", username=username):
raise HTTPException(400, "账号已存在,请直接登录")
entry = {"id": db.gen_id("U"), "username": username, "password": hash_password(password),
"name": name or username, "contact": contact, "identities": json.dumps(["opc_member|certified"]),
"created_at": now_iso()}
db.insert("accounts", entry)
await asyncio.to_thread(db.insert, "accounts", entry)
return {"ok": True, "token": make_token(username), "username": username, "name": name or username}
@@ -185,7 +194,7 @@ async def login(req: Request):
if password == "123456":
return {"ok": True, "token": make_token("pine"), "username": "pine", "name": "Pine"}
raise HTTPException(401, "密码错误,请重新输入")
acct = db.fetch_one("accounts", username=username)
acct = await asyncio.to_thread(db.fetch_one, "accounts", username=username)
if not acct:
raise HTTPException(404, "账号不存在,请先注册")
if acct["password"] != hash_password(password):
@@ -275,18 +284,18 @@ def auth_by_phone(body: dict):
@app.post("/api/auth/phone-login")
async def phone_login(req: Request):
b = await req.json()
acct, is_new = auth_by_phone(b)
acct, is_new = await asyncio.to_thread(auth_by_phone, b)
return {"ok": True, "token": make_token(acct["username"]), "username": acct["username"],
"name": acct.get("name") or acct["username"], "isNew": is_new}
@app.post("/api/auth/logout")
def logout():
async def logout():
return {"ok": True}
@app.get("/api/auth/verify")
def verify(authorization: str = Header(default="")):
async def verify(authorization: str = Header(default="")):
payload = require_auth(authorization)
return {"ok": True, "username": payload["username"]}
@@ -311,22 +320,22 @@ async def wx_login(req: Request):
raise HTTPException(401, f"微信登录失败:{errmsg}")
openid = wresp.get("openid")
acct = db.fetch_one("accounts", wxid=openid)
acct = await asyncio.to_thread(db.fetch_one, "accounts", wxid=openid)
if not acct:
name = str(b.get("nickName", "")).strip() or "微信用户"
acct = {"id": db.gen_id("U"), "username": openid, "wxid": openid, "phone": "", "name": name,
"avatar": str(b.get("avatarUrl", "")), "phone_bound": 0, "identities": json.dumps(["wx"]),
"created_at": now_iso()}
db.insert("accounts", acct)
await asyncio.to_thread(db.insert, "accounts", acct)
return {"ok": True, "token": make_token(acct["username"]), "username": acct["username"],
"name": acct.get("name") or "微信用户", "avatar": abs_url(acct.get("avatar")), "phoneBound": bool(acct.get("phone_bound")),
"configured": WX_HAS_CONFIG}
@app.get("/api/auth/me")
def me(authorization: str = Header(default="")):
async def me(authorization: str = Header(default="")):
payload = require_auth(authorization)
acct = db.fetch_one("accounts", username=payload["username"])
acct = await asyncio.to_thread(db.fetch_one, "accounts", username=payload["username"])
if not acct:
raise HTTPException(404, "账号不存在")
return {"ok": True, "user": _user_payload(acct)}
@@ -337,7 +346,7 @@ async def update_profile(req: Request, authorization: str = Header(default="")):
"""更新当前用户资料:昵称/头像 + 报名资料(status/topics/source"""
payload = require_auth(authorization)
b = await req.json()
acct = db.fetch_one("accounts", username=payload["username"])
acct = await asyncio.to_thread(db.fetch_one, "accounts", username=payload["username"])
if not acct:
raise HTTPException(404, "账号不存在")
patch = {}
@@ -352,8 +361,8 @@ async def update_profile(req: Request, authorization: str = Header(default="")):
if b.get("topics") is not None:
patch["topics"] = json.dumps(b["topics"], ensure_ascii=False) if isinstance(b["topics"], list) else str(b["topics"])
if patch:
db.update_row("accounts", acct["id"], patch)
acct = db.fetch_one("accounts", username=payload["username"])
await asyncio.to_thread(db.update_row, "accounts", acct["id"], patch)
acct = await asyncio.to_thread(db.fetch_one, "accounts", username=payload["username"])
return {"ok": True, "user": _user_payload(acct)}
@@ -379,11 +388,11 @@ async def wx_phone(req: Request, authorization: str = Header(default="")):
phone = (data.get("phone_info") or {}).get("purePhoneNumber", "")
if not phone:
raise HTTPException(400, "未获取到手机号")
acct = db.fetch_one("accounts", username=payload["username"])
acct = await asyncio.to_thread(db.fetch_one, "accounts", username=payload["username"])
if not acct:
raise HTTPException(404, "账号不存在")
# 绑号即合并:若该手机号下有其它账号(如网页手机号登录账号),把其数据并入当前微信账号
acct = _adopt_phone(acct, phone)
acct = await asyncio.to_thread(_adopt_phone, acct, phone)
return {"ok": True, "phoneBound": True, "phone": phone}
@@ -397,11 +406,11 @@ async def bind_phone(req: Request, authorization: str = Header(default="")):
raise HTTPException(400, "请输入正确的 11 位手机号")
if not check_sms_code(phone, code):
raise HTTPException(401, "验证码错误或已过期")
acct = db.fetch_one("accounts", username=payload["username"])
acct = await asyncio.to_thread(db.fetch_one, "accounts", username=payload["username"])
if not acct:
raise HTTPException(404, "账号不存在")
# 短信绑定同样合并同一手机号下的其它账号
acct = _adopt_phone(acct, phone)
acct = await asyncio.to_thread(_adopt_phone, acct, phone)
return {"ok": True, "phoneBound": True, "phone": phone}
@@ -449,29 +458,29 @@ def event_out(e):
@app.get("/api/events")
def events(request: Request, current: Optional[str] = None, bookable: Optional[str] = None):
all_events = db.list_all("events")
async def events(request: Request, current: Optional[str] = None, bookable: Optional[str] = None):
all_events = await asyncio.to_thread(db.list_all, "events")
if current is not None or bookable is not None:
upcoming = _upcoming(all_events)
if current is not None:
free = next((e for e in upcoming if e["type"] == "free"), None)
salon = next((e for e in upcoming if e["type"] == "salon"), None)
return {"ok": True, "next": event_out(upcoming[0] if upcoming else None),
"free": event_out(free), "salon": event_out(salon),
"upcoming": [event_out(e) for e in upcoming[:6]]}
return {"ok": True, "next": await asyncio.to_thread(event_out, upcoming[0] if upcoming else None),
"free": await asyncio.to_thread(event_out, free), "salon": await asyncio.to_thread(event_out, salon),
"upcoming": [await asyncio.to_thread(event_out, e) for e in upcoming[:6]]}
if bookable is not None:
# 可公开报名:排除 待开放(pending)/邀请中(invite)/已结束(done)/已满员(full)
lst = [e for e in upcoming if e["status"] not in ("closed", "done", "pending", "invite", "full")][:30]
return {"ok": True, "list": [event_out(e) for e in lst]}
return {"ok": True, "list": [event_out(e) for e in all_events]}
return {"ok": True, "list": [await asyncio.to_thread(event_out, e) for e in lst]}
return {"ok": True, "list": [await asyncio.to_thread(event_out, e) for e in all_events]}
@app.get("/api/events/{eid}")
def event_detail(eid: str):
e = db.fetch_by_id("events", eid)
async def event_detail(eid: str):
e = await asyncio.to_thread(db.fetch_by_id, "events", eid)
if not e:
raise HTTPException(404, "活动不存在")
return {"ok": True, "event": event_out(e)}
return {"ok": True, "event": await asyncio.to_thread(event_out, e)}
@app.post("/api/events")
@@ -500,8 +509,8 @@ async def create_event(req: Request, authorization: str = Header(default="")):
"audit_mode": 'manual' if b.get("auditMode") == "manual" else "auto",
"show_capacity": 1 if b.get("showCapacity") else 0,
}
db.insert("events", entry)
return {"ok": True, "entry": event_out(entry)}
await asyncio.to_thread(db.insert, "events", entry)
return {"ok": True, "entry": await asyncio.to_thread(event_out, entry)}
@app.put("/api/events/{eid}")
@@ -529,16 +538,16 @@ async def update_event(eid: str, req: Request, authorization: str = Header(defau
patch["show_capacity"] = 1 if b["showCapacity"] else 0
if not patch:
raise HTTPException(400, "无更新字段")
if not db.fetch_by_id("events", eid):
if not await asyncio.to_thread(db.fetch_by_id, "events", eid):
raise HTTPException(404, "排期不存在")
db.update_row("events", eid, patch)
return {"ok": True, "entry": event_out(db.fetch_by_id("events", eid))}
await asyncio.to_thread(db.update_row, "events", eid, patch)
return {"ok": True, "entry": await asyncio.to_thread(event_out, await asyncio.to_thread(db.fetch_by_id, "events", eid))}
@app.delete("/api/events/{eid}")
def delete_event(eid: str, authorization: str = Header(default="")):
async def delete_event(eid: str, authorization: str = Header(default="")):
require_auth(authorization)
db.delete_row("events", eid)
await asyncio.to_thread(db.delete_row, "events", eid)
return {"ok": True}
@@ -546,7 +555,7 @@ def delete_event(eid: str, authorization: str = Header(default="")):
@app.post("/api/bookings")
async def create_booking(req: Request, authorization: str = Header(default="")):
b = await req.json()
event = db.fetch_by_id("events", str(b.get("eventId", "")).strip())
event = await asyncio.to_thread(db.fetch_by_id, "events", str(b.get("eventId", "")).strip())
# 校验活动可报名:公开报名仅 open/full;待开放/邀请中/已结束不可报名
if not event:
raise HTTPException(404, "活动不存在")
@@ -556,7 +565,7 @@ async def create_booking(req: Request, authorization: str = Header(default="")):
# 强制登录:必须携带有效 token(无登录 → 401)
payload = require_auth(authorization)
acct = db.fetch_one("accounts", username=payload["username"])
acct = await asyncio.to_thread(db.fetch_one, "accounts", username=payload["username"])
if not acct:
raise HTTPException(401, "登录状态异常,请重新登录")
# 报名必须已绑定手机号(登录后自动/引导绑定),保证报名有联系方式
@@ -580,7 +589,7 @@ async def create_booking(req: Request, authorization: str = Header(default="")):
"question": b.get("question", ""), "source": source,
"audit_status": "approved" if event.get("audit_mode") != "manual" else "pending",
}
db.insert("bookings", entry)
await asyncio.to_thread(db.insert, "bookings", entry)
return {"ok": True, "id": entry["id"], "createdAt": entry["created_at"],
"username": username, "name": name,
"auditStatus": entry["audit_status"]}
@@ -596,13 +605,13 @@ def _decode_bk(row):
@app.get("/api/bookings/mine")
def my_bookings(authorization: str = Header(default="")):
async def my_bookings(authorization: str = Header(default="")):
payload = require_auth(authorization)
acct = db.fetch_one("accounts", username=payload["username"])
acct = await asyncio.to_thread(db.fetch_one, "accounts", username=payload["username"])
ids = {payload["username"]}
if acct and acct.get("phone"):
ids.add(acct["phone"])
conn = db.get_conn()
conn = await asyncio.to_thread(db.get_conn, )
rows = conn.execute("SELECT * FROM bookings").fetchall()
events = {r["id"]: dict(r) for r in conn.execute("SELECT * FROM events").fetchall()}
conn.close()
@@ -633,14 +642,14 @@ def my_bookings(authorization: str = Header(default="")):
@app.get("/api/bookings/by-event/{event_id}")
def my_booking_for_event(event_id: str, authorization: str = Header(default="")):
async def my_booking_for_event(event_id: str, authorization: str = Header(default="")):
"""查当前登录用户是否已报名该活动及其审核状态(详情页判断用)"""
payload = require_auth(authorization)
acct = db.fetch_one("accounts", username=payload["username"])
acct = await asyncio.to_thread(db.fetch_one, "accounts", username=payload["username"])
ids = {payload["username"]}
if acct and acct.get("phone"):
ids.add(acct["phone"])
conn = db.get_conn()
conn = await asyncio.to_thread(db.get_conn, )
rows = conn.execute("SELECT * FROM bookings WHERE event_id=?", (event_id,)).fetchall()
conn.close()
hit = None
@@ -658,11 +667,11 @@ def my_booking_for_event(event_id: str, authorization: str = Header(default=""))
async def checkin(req: Request, authorization: str = Header(default="")):
payload = require_auth(authorization)
b = await req.json()
acct = db.fetch_one("accounts", username=payload["username"])
acct = await asyncio.to_thread(db.fetch_one, "accounts", username=payload["username"])
ids = {payload["username"]}
if acct and acct.get("phone"):
ids.add(acct["phone"])
bk = db.fetch_by_id("bookings", b.get("bookingId", ""))
bk = await asyncio.to_thread(db.fetch_by_id, "bookings", b.get("bookingId", ""))
if not bk:
raise HTTPException(404, "报名记录不存在")
if bk["username"] not in ids and (bk.get("contact") or "") not in ids:
@@ -670,7 +679,7 @@ async def checkin(req: Request, authorization: str = Header(default="")):
if bk.get("checkin_at"):
raise HTTPException(400, "该场次已签到")
# 时间闸:只能在「签到开放时间 ≤ now ≤ 结束时间」内签到
ev = db.fetch_by_id("events", bk.get("event_id", ""))
ev = await asyncio.to_thread(db.fetch_by_id, "events", bk.get("event_id", ""))
open_ms = _parse_ms(ev.get("checkin_at") or ev.get("start_at")) if ev else 0
end_ms = (_parse_ms(ev.get("end_at")) if ev and ev.get("end_at") else 0) or (_parse_ms(ev.get("start_at")) + (ev.get("duration_min") or 90) * 60000 if ev else 0)
now_ms = int(time.time() * 1000)
@@ -678,14 +687,14 @@ async def checkin(req: Request, authorization: str = Header(default="")):
raise HTTPException(400, "未到签到时间,活动开始后再签到")
if now_ms > end_ms or (ev and ev.get("status") == "done"):
raise HTTPException(400, "本场已结束,无法签到")
db.update_row("bookings", bk["id"], {"checkin_at": now_iso()})
return {"ok": True, "entry": _decode_bk(db.fetch_by_id("bookings", bk["id"]))}
await asyncio.to_thread(db.update_row, "bookings", bk["id"], {"checkin_at": now_iso()})
return {"ok": True, "entry": _decode_bk(await asyncio.to_thread(db.fetch_by_id, "bookings", bk["id"]))}
@app.get("/api/bookings")
def admin_bookings(authorization: str = Header(default=""), status: Optional[str] = None, audit: Optional[str] = None):
async def admin_bookings(authorization: str = Header(default=""), status: Optional[str] = None, audit: Optional[str] = None):
require_auth(authorization)
lst = [_decode_bk(r) for r in db.list_all("bookings")]
lst = [_decode_bk(r) for r in await asyncio.to_thread(db.list_all, "bookings")]
lst.sort(key=lambda x: x["created_at"], reverse=True)
if status:
lst = [x for x in lst if x["status"] == status]
@@ -698,7 +707,7 @@ def admin_bookings(authorization: str = Header(default=""), status: Optional[str
async def update_booking(bid: str, req: Request, authorization: str = Header(default="")):
require_auth(authorization)
b = await req.json()
if not db.fetch_by_id("bookings", bid):
if not await asyncio.to_thread(db.fetch_by_id, "bookings", bid):
raise HTTPException(404, "预约不存在")
patch = {}
if b.get("status") in ("pending", "confirmed", "arrived", "converted"):
@@ -706,20 +715,20 @@ async def update_booking(bid: str, req: Request, authorization: str = Header(def
if b.get("auditStatus") in ("pending", "approved", "rejected"):
patch["audit_status"] = b["auditStatus"]
if patch:
db.update_row("bookings", bid, patch)
return {"ok": True, "entry": _decode_bk(db.fetch_by_id("bookings", bid))}
await asyncio.to_thread(db.update_row, "bookings", bid, patch)
return {"ok": True, "entry": _decode_bk(await asyncio.to_thread(db.fetch_by_id, "bookings", bid))}
@app.delete("/api/bookings/{bid}")
def delete_booking(bid: str, authorization: str = Header(default="")):
async def delete_booking(bid: str, authorization: str = Header(default="")):
require_auth(authorization)
db.delete_row("bookings", bid)
await asyncio.to_thread(db.delete_row, "bookings", bid)
return {"ok": True}
# ================= OPC 测评 =================
@app.get("/api/tests/opc/questions")
def opc_questions(version: Optional[str] = "full"):
async def opc_questions(version: Optional[str] = "full"):
v = "quick" if version == "quick" else "full"
return {
"ok": True, "version": v,
@@ -751,19 +760,19 @@ async def report_test(req: Request):
"type_code": b.get("typeCode"), "persona": b.get("persona", ""),
"adapt_index": int(b.get("adaptIndex") or 0), "adapt_level": b.get("adaptLevel", ""),
"tracks": json.dumps(b.get("tracks", []), ensure_ascii=False), "version": b.get("version", "")}
db.insert("tests", entry)
await asyncio.to_thread(db.insert, "tests", entry)
return {"ok": True, "id": entry["id"]}
@app.get("/api/tests")
def admin_tests(authorization: str = Header(default="")):
async def admin_tests(authorization: str = Header(default="")):
require_auth(authorization)
return {"ok": True, "list": db.list_all("tests")}
return {"ok": True, "list": await asyncio.to_thread(db.list_all, "tests")}
# ================= 政策 / 流程 / 调研:题目下发 + 结果生成(逻辑后置于后端) =================
@app.get("/api/policy/questions")
def policy_questions():
async def policy_questions():
return {"ok": True, "questions": policy_data.PT_QUESTIONS}
@@ -776,7 +785,7 @@ async def policy_calculate(req: Request):
@app.get("/api/plan/config")
def plan_config():
async def plan_config():
return {"ok": True, "regions": policy_data.SP_REGIONS, "status": policy_data.SP_STATUS}
@@ -792,7 +801,7 @@ async def plan_generate(req: Request):
@app.get("/api/survey/questions")
def survey_questions():
async def survey_questions():
return {"ok": True, "likert": survey_data.LIKERT_OPTIONS,
"sections": survey_data.SURVEY_SECTIONS, "questions": survey_data.SURVEY_QUESTIONS}
@@ -804,7 +813,7 @@ async def survey_submit(req: Request):
raise HTTPException(400, "缺少调研答案")
entry = {"id": db.gen_id("S"), "created_at": now_iso(), "username": str(b.get("username", "")).strip(),
"source": str(b.get("source", "")).strip() or "web", "answers": json.dumps(b.get("answers"), ensure_ascii=False)}
db.insert("survey_logs", entry)
await asyncio.to_thread(db.insert, "survey_logs", entry)
return {"ok": True, "id": entry["id"], "createdAt": entry["created_at"]}
@@ -818,14 +827,14 @@ async def report_policy(req: Request):
"answers": json.dumps(b.get("answers"), ensure_ascii=False),
"policies_count": int(b.get("policiesCount") or 0), "subsidies_count": int(b.get("subsidiesCount") or 0),
"loans_count": int(b.get("loansCount") or 0), "summary": str(b.get("summary", "") or "")[:200]}
db.insert("policy_logs", entry)
await asyncio.to_thread(db.insert, "policy_logs", entry)
return {"ok": True, "id": entry["id"]}
@app.get("/api/policy-logs")
def admin_policy(authorization: str = Header(default="")):
async def admin_policy(authorization: str = Header(default="")):
require_auth(authorization)
return {"ok": True, "list": db.list_all("policy_logs")}
return {"ok": True, "list": await asyncio.to_thread(db.list_all, "policy_logs")}
@app.post("/api/plan-logs")
@@ -837,14 +846,14 @@ async def report_plan(req: Request):
"region": str(b.get("region", "")).strip(), "status": str(b.get("status", "")).strip(),
"need_park": 1 if b.get("needPark") else 0, "has_staff": 1 if b.get("hasStaff") else 0,
"steps_count": int(b.get("stepsCount") or 0)}
db.insert("plan_logs", entry)
await asyncio.to_thread(db.insert, "plan_logs", entry)
return {"ok": True, "id": entry["id"]}
@app.get("/api/plan-logs")
def admin_plan(authorization: str = Header(default="")):
async def admin_plan(authorization: str = Header(default="")):
require_auth(authorization)
return {"ok": True, "list": db.list_all("plan_logs")}
return {"ok": True, "list": await asyncio.to_thread(db.list_all, "plan_logs")}
@app.post("/api/survey-logs")
@@ -854,36 +863,36 @@ async def report_survey(req: Request):
raise HTTPException(400, "缺少调研答案")
entry = {"id": db.gen_id("S"), "created_at": now_iso(), "username": str(b.get("username", "")).strip(),
"source": str(b.get("source", "")).strip() or "web", "answers": json.dumps(b.get("answers"), ensure_ascii=False)}
db.insert("survey_logs", entry)
await asyncio.to_thread(db.insert, "survey_logs", entry)
return {"ok": True, "id": entry["id"], "createdAt": entry["created_at"]}
@app.get("/api/survey-logs")
def admin_survey(authorization: str = Header(default="")):
async def admin_survey(authorization: str = Header(default="")):
require_auth(authorization)
return {"ok": True, "list": db.list_all("survey_logs")}
return {"ok": True, "list": await asyncio.to_thread(db.list_all, "survey_logs")}
# ================= 运营统计 =================
@app.get("/api/ops/stats")
def ops_stats(authorization: str = Header(default="")):
async def ops_stats(authorization: str = Header(default="")):
require_auth(authorization)
conn = db.get_conn()
conn = await asyncio.to_thread(db.get_conn, )
def count(t):
return conn.execute(f"SELECT COUNT(*) c FROM {t}").fetchone()["c"]
def count_where(t, where, val):
return conn.execute(f"SELECT COUNT(*) c FROM {t} WHERE {where}", (val,)).fetchone()["c"]
now_ms = int(__import__("time").time() * 1000)
upcoming = sum(1 for e in db.list_all("events") if _parse_ms(e["start_at"]) >= now_ms)
free = sum(1 for e in db.list_all("events") if e["type"] == "free" and _parse_ms(e["start_at"]) >= now_ms)
salon = sum(1 for e in db.list_all("events") if e["type"] == "salon" and _parse_ms(e["start_at"]) >= now_ms)
upcoming = sum(1 for e in await asyncio.to_thread(db.list_all, "events") if _parse_ms(e["start_at"]) >= now_ms)
free = sum(1 for e in await asyncio.to_thread(db.list_all, "events") if e["type"] == "free" and _parse_ms(e["start_at"]) >= now_ms)
salon = sum(1 for e in await asyncio.to_thread(db.list_all, "events") if e["type"] == "salon" and _parse_ms(e["start_at"]) >= now_ms)
# 先全部算完再关连接
stats = {
"bookings": count("bookings"),
"pending": count_where("bookings", "status=?", "pending"),
"confirmed": count_where("bookings", "status=?", "confirmed"),
"arrived": count_where("bookings", "status=?", "arrived"),
"converted": count_where("bookings", "status=?", "converted"),
"pending": await asyncio.to_thread(count_where, "bookings", "status=?", "pending"),
"confirmed": await asyncio.to_thread(count_where, "bookings", "status=?", "confirmed"),
"arrived": await asyncio.to_thread(count_where, "bookings", "status=?", "arrived"),
"converted": await asyncio.to_thread(count_where, "bookings", "status=?", "converted"),
"events": count("events"), "upcomingEvents": upcoming, "free": free, "salon": salon,
"tests": count("tests"), "policyLogs": count("policy_logs"), "planLogs": count("plan_logs"),
"surveyLogs": count("survey_logs"),