Kotlin工程实践

武科大课表:从假接口到真实教务抓取

网上流传的武科大官方接口清单并不存在,这两个域名连 DNS 记录都没有。这个基于拾光课程表修改的客户端改为在浏览器会话内解析正方教务页面,并新增成绩查询与教室视图。

5 分钟阅读 约 1544 字

“没有调查就没有发言权。”这句话在武科大课表这个项目里不是口号,而是一条硬性流程:任何一个数据来源,先验证,再写代码。项目本身是面向武汉科技大学的安卓课表、成绩与教室客户端,基于开源项目拾光课程表(Apache-2.0)修改而来,保留原项目全部功能,新增成绩查询与教室视图。它用 Kotlin Multiplatform 编写,仍然是那个熟悉的安卓课表应用,只是把数据自己从教务系统里挖出来。

先证伪那份“官方 API 清单”

网上流传过一份《WUST 官方开放 API 清单》,内容包括 api.wust.edu.cn/oauth2、/edu/grades 与 /classroom/status,还配套描述了开发者平台、Python SDK 以及每天 500 次的调用配额。看上去完整得像真的,但只要做一次最基础的检查就会发现:这两个域名没有任何 DNS 记录。域名解析都不存在,所谓的接口、SDK 与配额自然无从谈起。这份清单的价值,恰恰在于提醒我们不要拿传闻当需求。

text
api.wust.edu.cn/oauth2
api.wust.edu.cn/edu/grades
api.wust.edu.cn/classroom/status

真实后端是正方教务加 CAS

WUST 真实的教学系统是正方教务,入口为 https://bkjx.wust.edu.cn/jsxsd,登录走 CAS 统一身份认证(auth.wust.edu.cn/lyuapServer/login),并没有对外开放 OAuth2 接口。所以本项目通过浏览器会话内解析教务页面的方式获取数据,跟所谓的官方接口没有任何关系。从保存下来的教务首页里,可以解出真实的学生端菜单地址:

text
/jsxsd/xskb/xskb_list.do
/jsxsd/syjx/toXskb.do
/jsxsd/kscj/cjcx_frm
/jsxsd/kscj/cjcx_list
/jsxsd/kscj/djkscj_list
/jsxsd/xsks/xsksap_query
/jsxsd/grxx/xsxx
/jsxsd/xxwcqk/xxwcqk_idxOnzh.do

这些地址才是真正要对接的东西。它们不是 REST 接口,而是页面;不是返回 JSON,而是返回 HTML。理解这一点之后,整个客户端的数据层设计就顺了。

学生端没有空教室查询

教师端有教室借用,学生端菜单里却没有“空教室查询”。这个事实直接决定了教室功能的形态:

学生端菜单里没有空教室查询,所以教室功能实现为教室维度视图,从已导入的课表聚合出每间教室的占用与空闲时段。

也就是说,教室视图不是一次网络请求,而是一次本地聚合:把已导入的课表按教室重新切分,得到“星期 × 节次”的占用网格与空闲时段。功能是真的,只是它的数据来源不是接口,而是课表本身。

仓库怎么分

整个仓库分成四块,边界相当清楚:

  • wust-schedule:App 本体,即拾光课程表的 WUST 分支,Kotlin Multiplatform。
  • wust-warehouse:适配器仓库,包含 WUST 课表、成绩适配器与生成的 school_index.pb。
  • wust-browser-tester:浏览器测试台,独立可运行形态,含扩展与离线回归。
  • docs:构建说明、环境说明、日志与校验脚本。

成绩查询与十五个字段

成绩模块从教务导入课程成绩,每条包含 15 个字段,例如学期、课程编号、课程名称、成绩、学分、学时、绩点、课程性质、考核方式等。导入后按学期分组展示,并自动汇总总学分、加权平均分与平均绩点,还支持一键清空。字段全部来自教务页面本身,不做猜测性补全。README 记录的真实快照里,成绩适配器解析出 16 条记录,汇总结果为 32.5 学分、加权 84.15、GPA 3.42。

志愿工时的两段式 SSO

志愿工时系统(volunteer.wustlinghang.cn)没有使用教务 CAS,而是“先登录武科大助手拿 token,再用 token 换志愿系统 token”的两段式 SSO。App 里的「志愿」入口采用保存学号密码、后台直连 API 的方式实现:首次填入学号与密码后,自动完成助手登录、换取志愿 token、拉取工时概览与明细,之后点「同步工时」即可刷新,全程不需要打开网页。

凭据的处理是这块最需要克制的部分:

  • 凭据只写在 App 私有的 DataStore 里;
  • 它没有被加入 BACKUP_KEYS 白名单,因此不会被 WebDAV 备份带出去;
  • 「清除凭据」可以随时删除。

由于志愿接口没有公开文档,实现上保留原始 JSON,界面做尽力解析并把原始数据折叠展示,后端字段变动也不会导致崩溃。

构建、产物与验收数字

构建入口就是一条 Gradle 命令:

powershell
$env:JAVA_HOME='C:\Program Files\Java\jdk-21'
cd D:\Documents\wust\wust-schedule
.\gradlew.bat :androidApp:assembleDebug --console=plain

gradlew :androidApp:assembleDebug 的结果是 BUILD SUCCESSFUL,产出 3 个 ABI 的 APK,各 19.9 MB,分别面向 arm64-v8a(绝大多数手机)、armeabi-v7a(老 32 位机型)与 x86_64(WSA 与模拟器)。包名为 cn.wust.timetable,因此可以与原版并存安装。

README 记录的静态验收数字还包括:课表适配器基于真实快照解析出 22 门课;仓库索引共 249 所学校并包含 WUST 两项;Room v8 迁移通过;APK 包名与内置离线仓库均已核对。README 也提醒,不要在未替换仓库地址的情况下点击「更新仓库」,因为官方仓库没有本分支的成绩适配器,更新会覆盖内置的 WUST 适配器。

调查先于发言

回到开头那句话。假接口清单的教训不是“网上都不可信”,而是判断必须建立在可复现的检查上:解析不了域名,就不要写 SDK;学生端没有菜单项,就不要造一个空教室接口;接口没有文档,就保留原始数据再尽力解析。整个项目所做的工作,无非是把每一个数据来源都实测一遍,再把结论写进代码和验收报告里。