feat(gdgxk): 课程卡片显示培训年度 / 学时 / 完成状态
- list_courses 解析 showCourse 卡片里的「培训年度」「课程学时」「未完成/已完成」, 返回 section / info / status_label,前端拼成「2026年公需课 · 未完成 · 30 学时」 - 修正完成判定:以平台状态为准(需所有教学活动 100% + 试卷考核 ≥60), 不再用视频秒数够不够来推断,避免把未完成的课当已学完跳过
This commit is contained in:
+1
-1
@@ -17,7 +17,7 @@
|
||||
|
||||
### 新增
|
||||
|
||||
- 新增平台「广东公需课」(`platforms/gdgxk.py`):`jsglpt.gds.edu.cn` 账号密码(base64)登录后换 ITLMS 票进 `jsxx.gds.edu.cn`,课程列表读 `/uc/store/courseRegister`;学习只跑 `updateViewTime1` 进度心跳(服务端按 `lastViewTime` 直接记账,不需要真播)。票按 `ITLMS-xxx||ticketId` 两段交给 `jsxx.gds.edu.cn/nts/loginToStoreByAccount`;登录要过 4 位图形验证码,用 ddddocr 离线识别并自动重试。配套 `script/capture-jsglpt.py`(抓包取证)与 `script/test-gdgxk.py`(接口自测)
|
||||
- 新增平台「广东公需课」(`platforms/gdgxk.py`):`jsglpt.gds.edu.cn` 账号密码(base64)登录后换 ITLMS 票进 `jsxx.gds.edu.cn`,课程列表读 `/uc/store/courseRegister`;学习只跑 `updateViewTime1` 进度心跳(服务端按 `lastViewTime` 直接记账,不需要真播)。票按 `ITLMS-xxx||ticketId` 两段交给 `jsxx.gds.edu.cn/nts/loginToStoreByAccount`;登录要过 4 位图形验证码,用 ddddocr 离线识别并自动重试。配套 `script/capture-jsglpt.py`(抓包取证)与 `script/test-gdgxk.py`(接口自测);课程列表解析出培训年度 / 学时 / 完成状态,卡片显示成「2026年公需课 · 未完成 · 30 学时」
|
||||
- 统一日志源 `runtime.log()`:控制台、内存(`/api/log`)、SQLite(`/api/log/history`)同一份内容;新增右上角「系统日志」弹窗查看 `key=system` 的启动/设置类日志
|
||||
- 设置保存、服务启动都写入统一日志;卡片「课程日志」和「系统日志」的「清空」会同时清内存和 SQLite
|
||||
- 前端:卡片状态徽标、学习进度、课程勾选、卡片内实时日志、测验 / 设置 / 系统日志弹窗全部保留;新增平台名徽标与空态引导
|
||||
|
||||
+1
-1
@@ -180,7 +180,7 @@ GET /mod/school/wanpeng/syncSelf?courseCode=<courseCode>
|
||||
|
||||
- 登录:`POST https://jsglpt.gds.edu.cn/loginIn.do`,`loginUser.userName` / `loginUser.password` 都是 **base64**(账号是手机号),`loginUser.loginType=normal`;`captcha` 是**强制**的 4 位图形验证码,`GET /login/readCaptcha.action` 返回 `"data:image/png;base64,..."`,用 `ddddocr` 离线识别(识别错就换一张重试,最多 5 次)。登录失败时服务端把原因写回登录页 `#errorText`(如「请输入正确的验证码!」),据此区分是验证码还是密码问题。
|
||||
- 换票:`POST /teacher/ssoItlmsGxkTicket.action?verificaCode=true&page=` → `ITLMS-<hex>||<ticketId>`;把票按 `||` 切成两段,`GET https://jsxx.gds.edu.cn/nts/loginToStoreByAccount?ticket=<前段>&ticketId=<后段>` 才拿到学习平台会话(入口来自 jsglpt 首页的隐藏表单 `SSO_ITLMS_PUBLIC_STUDY_FORM`;直接带 `?ticket=` 访问 `courseRegister` 是无效的)。两个域名的 cookie 要在同一个 session 里。
|
||||
- 课程列表:`GET /uc/store/courseRegister`,页面里是 `showCourse('<课程id>','<课程名>')`。
|
||||
- 课程列表:`GET /uc/store/courseRegister`,页面里是 `showCourse('<课程id>','<课程名>')`;同一张卡片里还有 `课程学时:N`、`培训年度:YYYY`、`未完成/已完成` 徽标,已解析成 `section` / `info` / `status_label` 显示在卡片上。
|
||||
- 学习页:`GET /study/course/<课程id>?v=1`(服务端渲染、UTF-8),含 `url1`(CDN 直链 MP4)、`t`(总秒数)、`md5Uid`、`/a_<aid>/...` 上报前缀。
|
||||
- 播放注册:`POST /a_<aid>/study/video/user/updateVideoStatus1`(`_method=PUT&id=<课件id>&md5Uid=<uid>&playbackSessionId=<sid>&playbackTakeover=true`)→ `{time, md5}`。
|
||||
- 进度心跳:`POST /a_<aid>/study/video/user/<课件id>/updateViewTime1`(`_method=PUT&aid=...&isLimit=false&md5&expires&md5Uid&lastViewTime=<秒>&playbackSessionId`)。
|
||||
|
||||
@@ -6,4 +6,5 @@
|
||||
- 构建检查进 CI:`cd web && pnpm install --frozen-lockfile && pnpm typecheck && pnpm build`,
|
||||
并校验 `static/` 与源码同步。
|
||||
- 广东公需课:用一门**未学完**的课实测进度上报,确认平台端「已学时长 / 已完成」状态跟着变。
|
||||
- 广东公需课:视频弹题与课程考核未接入(第一期只做登录 / 课程列表 / 看课)。
|
||||
- 广东公需课:**试卷考核**未接入 —— 平台判定通过要「所有教学活动 100% + 试卷考核 ≥60 分」,所以视频报满后课程仍显示「未完成」。
|
||||
- 广东公需课:视频弹题未接入。
|
||||
|
||||
+1
-1
@@ -1,3 +1,3 @@
|
||||
name: study-tool
|
||||
version: 0.0.3
|
||||
version: 0.0.4
|
||||
description: 东莞市教师培训平台(jspx.dgjy.net / study.dgjy.net)多账号学习控制台
|
||||
|
||||
@@ -142,17 +142,32 @@ def profile(s):
|
||||
return m.group(1) if m else ""
|
||||
|
||||
|
||||
def _parse_courses(html):
|
||||
"""课程列表页:每个 `showCourse('id','名称')` 前面就是这张卡片的信息。"""
|
||||
out = []
|
||||
for m in re.finditer(r"showCourse\('([^']+)','([^']*)'\)", html):
|
||||
block = html[max(0, m.start() - 2500):m.start()]
|
||||
year = re.search(r"培训年度:\s*<[^>]*>(\d{4})<", block)
|
||||
hours = re.search(r"课程学时:\s*<[^>]*>(\d+)<", block)
|
||||
st = re.search(r">\s*(未完成|已完成)\s*<", block)
|
||||
out.append({
|
||||
"plan_id": m.group(1),
|
||||
"project_id": "",
|
||||
"name": m.group(2).strip(),
|
||||
"section": ("%s年公需课" % year.group(1)) if year else "",
|
||||
"status_label": st.group(1) if st else "",
|
||||
"info": ("%s 学时" % hours.group(1)) if hours else "",
|
||||
"in_study": True,
|
||||
"platform": PLATFORM_ID,
|
||||
})
|
||||
return out
|
||||
|
||||
|
||||
def list_courses(s):
|
||||
html = s.get(X + "/uc/store/courseRegister", timeout=30).text
|
||||
out, seen = [], set()
|
||||
for cid, name in re.findall(r"showCourse\('([^']+)','([^']*)'\)", html):
|
||||
if cid in seen:
|
||||
continue
|
||||
seen.add(cid)
|
||||
out.append({"plan_id": cid, "project_id": "", "name": name.strip(),
|
||||
"in_study": True, "platform": PLATFORM_ID})
|
||||
out = _parse_courses(html)
|
||||
if not out:
|
||||
if "loginBtn" in html or "showLogin" in html:
|
||||
if "logout" not in html:
|
||||
raise _expired("学习平台会话已失效,请重新登录")
|
||||
return []
|
||||
return out
|
||||
@@ -217,14 +232,21 @@ def discover(s, plan_id, project_id, name=""):
|
||||
cw["sid"] = sid
|
||||
sign = _sign(s, {"aid": cw["aid"], "id": cw["id"], "sid": sid}, cw["uid"])
|
||||
cur = _view_time(s, cw, cw["uid"], sign)
|
||||
done = cw["total"] > 0 and cur >= cw["total"]
|
||||
# 完成与否以平台状态为准:要「所有教学活动 100% + 试卷考核 ≥60」,视频秒数够不代表完成
|
||||
status = ""
|
||||
for c in _parse_courses(s.get(X + "/uc/store/courseRegister", timeout=30).text):
|
||||
if c["plan_id"] == cid:
|
||||
status = c["status_label"]
|
||||
break
|
||||
done = status == "已完成"
|
||||
return {
|
||||
"user": profile(s),
|
||||
"course": name or cw["title"],
|
||||
"progress": "%d/%d 秒" % (cur, cw["total"]),
|
||||
"progress": "%d/%d 秒%s" % (cur, cw["total"], "(课程已完成)" if done else ""),
|
||||
"info": cw["video"],
|
||||
"courseware": [{"id": cw["id"], "title": cw["title"], "href": page,
|
||||
"total": cw["total"], "viewTime": cur, "done": done,
|
||||
"status": status,
|
||||
"aid": cw["aid"], "uid": cw["uid"]}],
|
||||
"feedback": [], "test": [],
|
||||
"platform": PLATFORM_ID, "generation": "gxk",
|
||||
@@ -253,6 +275,11 @@ def study(key, s, d, **opts):
|
||||
continue
|
||||
runtime.log(key, "开始学习:%s(总长 %d 秒,已看 %d 秒)"
|
||||
% (cw["title"], cw["total"], cw["viewTime"]))
|
||||
if cw["total"] and cw["viewTime"] >= cw["total"]:
|
||||
runtime.log(key, "视频已播完,但课程仍显示「%s」:还差试卷考核或其他教学活动"
|
||||
% (cw.get("status") or "未完成"), level="warn")
|
||||
runtime.set_state(key, done=i)
|
||||
continue
|
||||
_learn(key, s, cw, speed, beat)
|
||||
runtime.set_state(key, done=i)
|
||||
runtime.set_state(key, status="done", done=total)
|
||||
|
||||
Reference in New Issue
Block a user