"""账号 / 登录 / 登录记录接口。""" import time from fastapi.responses import JSONResponse from study_tool.webapp import app from study_tool import store from study_tool.config import * from study_tool.models import * from study_tool.platforms import registry from study_tool.platforms.jspx import * from study_tool.runtime import RUNS, RUNS_LOCK, log def _log_login(username, ok, code, error, elapsed, stage=""): """每次登录尝试:统一日志 + login_log 表(登录记录页用)。""" log(username or "-", "登录%s code=%s%s %s %.1fs" % ( "成功" if ok else "失败", code, (" " + stage) if stage else "", (error or "")[:120], elapsed or 0)) try: store.login_log_add(username, ok, code, elapsed, stage, error) except Exception: pass def _login_account(b: LoginIn): """解析出一组可登录的凭据:显式传入优先,否则按账号 id 从库里取。""" acc = store.account_get(b.account_id) if b.account_id else None username = (b.username or "").strip() or ((acc or {}).get("username") or "") password = b.password or (store.account_password(b.account_id) if b.account_id else "") return acc, username, password @app.post("/api/login") def api_login(b: LoginIn): t0 = time.time() acc, username, password = _login_account(b) if not username or not password: err = "没有可用的账号密码:新账号请在「添加账号」里填密码,服务端会把它存进 SQLite" _log_login(username or b.account_id, False, "no_credential", err, time.time() - t0, "LoginIn") return JSONResponse({"ok": False, "code": "no_credential", "error": err}, status_code=200) pf = registry.get((acc or {}).get("platform") or b.platform) if pf is None or pf.login is None: err = "平台「%s」还没有实现登录,请换一个平台或先在 platforms/ 里补上实现" % ( (acc or {}).get("platform") or b.platform) _log_login(username, False, "no_platform", err, time.time() - t0, "LoginIn") return JSONResponse({"ok": False, "code": "no_platform", "error": err}, status_code=200) try: s, user = pf.login(username, password) except Exception as e: err = str(e)[:300] _log_login(username, False, "login_failed", err, time.time() - t0, type(e).__name__) if acc: store.account_upsert({"id": acc["id"], "username": acc["username"], "status": "login_failed", "lastError": err, "lastErrorAt": time.time()}) return JSONResponse({"ok": False, "code": "login_failed", "error": err}, status_code=200) warn = "" try: courses = pf.list_courses(s) if pf.list_courses else [] except Exception as e: courses, warn = [], str(e)[:200] cookies = (pf.dump_cookies or dump_cookies)(s) _log_login(username, True, "ok", warn, time.time() - t0, "user=%s" % user) if acc: store.account_save_login(acc["id"], user, cookies, courses) return {"ok": True, "user": user, "cookies": cookies, "courses": courses, "warn": warn} @app.get("/api/loginlog") def api_loginlog(tail: int = 40): """登录尝试回放:前端点「登录」后,这里能看到平台到底回了什么。""" return {"ok": True, "lines": store.login_log_tail(tail)} # ------------------------------------------------------------------ accounts @app.get("/api/accounts") def api_accounts(): """全部账号(含会话 cookie / 课程缓存 / 勾选),**不含密码**。""" return {"ok": True, "accounts": store.account_list()} @app.post("/api/accounts") def api_account_save(b: AccountIn): """新增或更新一个账号。password 不传(或传空)表示保留库里已有的密码。""" rec = b.model_dump() if not (rec.get("password") or "").strip(): rec["password"] = None if not rec.get("id") and not rec.get("username"): return JSONResponse({"ok": False, "error": "缺少 username"}, status_code=200) try: aid = store.account_upsert(rec) except Exception as e: return JSONResponse({"ok": False, "error": "写入 SQLite 失败:%s" % str(e)[:200]}, status_code=200) return {"ok": True, "id": aid, "account": store.account_get(aid)} @app.get("/api/accounts/{account_id}") def api_account_get(account_id: str): acc = store.account_get(account_id) if not acc: return JSONResponse({"ok": False, "error": "账号不存在"}, status_code=200) return {"ok": True, "account": acc} @app.delete("/api/accounts/{account_id}") def api_account_del(account_id: str): store.account_delete(account_id) with RUNS_LOCK: RUNS.pop(account_id, None) return {"ok": True}