fix: 首次进入课程自动同步加入(syncSelf)
首次进入课程时课时树回 code=500「你还没有加入学习…」,前端把它渲染成挡路弹层, 人工点掉后 SPA 自己补发 syncSelf 才落库 —— 所以第二次进同一门课就正常。 - discover_study 返回值补 course_code(trainCourse/getById.courseCode) - 新增 study_mod_get:/mod/* 只带 cookie + Referer,不带 Bearer - 新增 study_sync_self:GET /mod/school/wanpeng/syncSelf?courseCode=… - 新增 study_sync_identity:GET /mod/gjt/roleList 补一刀身份 - ensure_study_join 改为:已加入 → student/join → syncSelf + 轮询 getById → 刷新 jspx 页 - todo.md 收尾,新增 OPTIMIZATION.md 记录待实测确认项
This commit is contained in:
@@ -1139,6 +1139,8 @@ def discover_study(client, train_id, name="", user=""):
|
||||
# 未加入培训时 study/log 会被静默丢弃,调用方要先补 join
|
||||
"is_join": str(info.get("isJoin")) == "1",
|
||||
"need_join": str(info.get("needJoin")) == "1",
|
||||
# 浏览器「刷新课程页就能加入」靠的就是它:/mod/school/wanpeng/syncSelf?courseCode=<courseCode>
|
||||
"course_code": str(info.get("courseCode") or ""),
|
||||
}
|
||||
|
||||
|
||||
@@ -1223,12 +1225,60 @@ def study_refresh_page(s, d):
|
||||
return ""
|
||||
|
||||
|
||||
def study_mod_get(client, path, params=None):
|
||||
"""调 /mod/ 这组接口(不在 /train 下,也不带 Bearer,只认 cookie 会话)。
|
||||
|
||||
浏览器课程页刷新时发的就是这组:/mod/student/currentInfo、/mod/school/wanpeng/syncSelf、
|
||||
/mod/gjt/roleList。用 StudyClient 的会话(cookie)直接 GET 就能通,
|
||||
而 /train/* 必须额外带 Authorization: Bearer <Middle-Token>。
|
||||
"""
|
||||
h = {"Accept": "application/json, text/plain, */*",
|
||||
"Referer": STUDY + "/training/trainDetails"}
|
||||
r = client.s.get(STUDY + path, params=params, headers=h, timeout=40)
|
||||
try:
|
||||
return r.json()
|
||||
except Exception:
|
||||
return {"resultCode": r.status_code, "resultMsg": (r.text or "")[:200]}
|
||||
|
||||
|
||||
def study_sync_self(client, course_code):
|
||||
"""把当前账号同步进课程 —— 浏览器刷新课程页时真正「加入培训」的那一步。
|
||||
|
||||
抓包实录(常用公文写作):
|
||||
getById -> isJoin=0 needJoin=1
|
||||
GET /mod/school/wanpeng/syncSelf?courseCode=<code> (耗时约 2s)
|
||||
getById -> isJoin=1
|
||||
前端在 isJoin != 1 时就是发这一发把学员同步进课程;
|
||||
isFreeJoin=0 的慕课课不走 student/join,只能靠它落库。
|
||||
返回 (ok, msg)。
|
||||
"""
|
||||
j = study_mod_get(client, "/mod/school/wanpeng/syncSelf", {"courseCode": course_code})
|
||||
res = j.get("result") or {}
|
||||
code = str(j.get("resultCode") or res.get("code") or "")
|
||||
return code == "200", str(res.get("msg") or j.get("resultMsg") or code or "?")
|
||||
|
||||
|
||||
def study_sync_identity(client):
|
||||
"""页面上紧随 syncSelf 的身份拉取(/mod/gjt/roleList),个别课程同步后要靠它才认人。
|
||||
|
||||
返回 True/False,失败无所谓,只当补一刀。
|
||||
"""
|
||||
try:
|
||||
j = study_mod_get(client, "/mod/gjt/roleList")
|
||||
return bool(j.get("result")) or str(j.get("resultCode") or "") == "200"
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
def ensure_study_join(s, client, d):
|
||||
"""确保这门培训已加入;返回 (ok, 说明)。
|
||||
|
||||
顺序:先看当前状态 → 直接调 join → 被拒就刷新一次课程页再复查。
|
||||
顺序(照浏览器课程页的真实行为):
|
||||
当前状态 → student/join → 被拒就 syncSelf(courseCode) 轮询复查
|
||||
→ 再不行就刷新一次 jspx 课程页复查。
|
||||
"""
|
||||
tid = d.get("train_id") or ""
|
||||
code = d.get("course_code") or ""
|
||||
|
||||
def joined():
|
||||
j = client.call("GET", "/course/front/trainCourse/getById", params={"id": tid})
|
||||
@@ -1236,13 +1286,40 @@ def ensure_study_join(s, client, d):
|
||||
|
||||
if d.get("is_join") or joined():
|
||||
return True, "已加入"
|
||||
|
||||
ok, msg = study_join_course(client, tid)
|
||||
if ok:
|
||||
return True, "加入成功"
|
||||
|
||||
# 浏览器刷新课程页走的就是这条:syncSelf 落库,再重新拉 getById
|
||||
if code:
|
||||
sync_ok, sync_msg = study_sync_self(client, code)
|
||||
for i in range(8):
|
||||
time.sleep(0.6 if i == 0 else 1.5)
|
||||
try:
|
||||
if joined():
|
||||
return True, "同步课程页后已加入(student/join 被拒:%s;syncSelf=%s)" % (
|
||||
msg or "-", sync_msg or "-")
|
||||
except SessionExpired:
|
||||
raise
|
||||
except Exception:
|
||||
pass
|
||||
if i == 2:
|
||||
# 页面在 syncSelf 之后还会拉一次身份,个别课程靠它才认人
|
||||
study_sync_identity(client)
|
||||
note = "syncSelf:%s" % ("成功" if sync_ok else "失败(%s)" % (sync_msg or "-"))
|
||||
else:
|
||||
note = "课程没有 courseCode,无法走同步接口"
|
||||
|
||||
study_refresh_page(s, d)
|
||||
if joined():
|
||||
return True, "刷新课程页后已加入(join 接口拒了:%s)" % (msg or "-")
|
||||
return False, msg or "未加入"
|
||||
try:
|
||||
if joined():
|
||||
return True, "刷新课程页后已加入(student/join 被拒:%s;%s)" % (msg or "-", note)
|
||||
except SessionExpired:
|
||||
raise
|
||||
except Exception:
|
||||
pass
|
||||
return False, "%s;%s" % (msg or "未加入", note)
|
||||
|
||||
|
||||
def study_join_course(client, train_id):
|
||||
@@ -1251,6 +1328,7 @@ def study_join_course(client, train_id):
|
||||
课程 needJoin=1 且 isJoin=0 时必须先加入,否则 study/log 照样回 200,
|
||||
但学习记录不会计数 —— 现象就是「每个课时都报成功、复查全是未完成」。
|
||||
isFreeJoin=0 的课程服务端会直接拒:「该课程无法直接加入,请联系管理员」。
|
||||
慕课课(isFreeJoin=0)别指望这个接口,要改用 study_sync_self —— 见 ensure_study_join。
|
||||
返回 (ok, msg)。
|
||||
"""
|
||||
j = client.call("POST", "/course/front/student/join", params={"courseId": train_id})
|
||||
@@ -1325,6 +1403,7 @@ RUNS_LOCK = threading.Lock()
|
||||
|
||||
def run_log(key, msg):
|
||||
line = "%s %s" % (time.strftime("%Y-%m-%d %H:%M:%S"), msg)
|
||||
print("%s [%s]" % (line, key), flush=True) # 同步打到控制台,docker compose logs -f 也能看
|
||||
with RUNS_LOCK:
|
||||
st = RUNS.get(key)
|
||||
if st is not None:
|
||||
@@ -3261,6 +3340,19 @@ def _port_open(port, host="127.0.0.1", timeout=0.6):
|
||||
return s.connect_ex((host, port)) == 0
|
||||
|
||||
|
||||
def _lan_ip():
|
||||
"""本机对外的网卡地址,用于启动提示;容器里拿到的是容器 IP,不能当访问地址,返回空。"""
|
||||
if os.path.exists("/.dockerenv"):
|
||||
return ""
|
||||
import socket
|
||||
try:
|
||||
with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as s:
|
||||
s.connect(("223.5.5.5", 80))
|
||||
return s.getsockname()[0]
|
||||
except Exception:
|
||||
return ""
|
||||
|
||||
|
||||
def _probe_self(port):
|
||||
"""端口已被占用时判断是不是本程序自己在跑,避免重复启动直接报错。"""
|
||||
import urllib.request
|
||||
@@ -3276,6 +3368,8 @@ def main():
|
||||
# 默认只监听本机;Docker / 远程管理用 HOST=0.0.0.0
|
||||
host = os.environ.get("HOST", "127.0.0.1")
|
||||
url = "http://127.0.0.1:%d" % port
|
||||
# 设了访问令牌时,打印出来的地址一律带上 ?token=:本机访问同样要校验令牌
|
||||
suffix = ("/?token=%s" % AUTH_TOKEN) if AUTH_TOKEN else ""
|
||||
|
||||
store.init() # 建库建表 + 导入旧的 settings.json / logs/*.log
|
||||
hydrate_runs() # 上次的运行状态读回来(没跑完的标成已中断)
|
||||
@@ -3284,7 +3378,7 @@ def main():
|
||||
if _port_open(port):
|
||||
info = _probe_self(port)
|
||||
if info and info.get("app") == APP_ID:
|
||||
print("服务已在运行 -> %s (PID %s)" % (url, info.get("pid")))
|
||||
print("服务已在运行 -> %s%s (PID %s)" % (url, suffix, info.get("pid")))
|
||||
print("直接打开上面的地址;要重启请先结束 PID %s。" % info.get("pid"))
|
||||
return
|
||||
print("[错误] 端口 %d 被其它程序占用(不是本程序)。" % port)
|
||||
@@ -3292,8 +3386,13 @@ def main():
|
||||
print(" 换端口: set PORT=8849 && python app.py")
|
||||
return
|
||||
|
||||
print("多账号学习控制台 -> %s" % url)
|
||||
print("IPv6 localhost 同步监听 -> http://[::1]:%d" % port)
|
||||
print("多账号学习控制台 -> %s%s" % (url, suffix))
|
||||
print("IPv6 localhost 同步监听 -> http://[::1]:%d%s" % (port, suffix))
|
||||
if AUTH_TOKEN:
|
||||
print("访问令牌 AUTH_TOKEN -> %s" % AUTH_TOKEN)
|
||||
print("远程访问地址 -> http://%s:%d%s" % (_lan_ip() or "<服务器IP>", port, suffix))
|
||||
elif host == "0.0.0.0":
|
||||
print("[提示] 已监听 0.0.0.0 但没有设置 AUTH_TOKEN:同网段谁都能打开,远程部署请设置")
|
||||
|
||||
srv4 = uvicorn.Server(uvicorn.Config(app, host=host, port=port,
|
||||
log_level="warning", access_log=False))
|
||||
|
||||
Reference in New Issue
Block a user