虽然时间上是先开发后使用的,但是我想对于大部分读者而言如何使用比如何开发更加简单,有浅入深总归不是一件坏事。

插件的使用

油猴

如果你已经会用油猴了,那么可以直接跳过这一小节。

Tampermonkey 官网 是这样描述自己的:

Tampermonkey 允许用户自定义并增强您最喜爱的网页的功能。用户脚本是小型 JavaScript 程序,可用于向网页添加新功能或修改现有功能。使用篡改猴,您可以轻松在任何网站上创建、管理和运行这些用户脚本。此外,篡改猴 使您轻松找到并安装其他用户创建的用户脚本。这意味着您可以快速轻松地访问为您喜爱的网页定制的广泛库,而无需花费数小时编写自己的代码。

简单来说,它是一个管理脚本的工具,它存在的目的就是优化浏览网页的体验。

除了 Tampermonkey 之外,还有 Violentmonkey, Script Cat 等等管理工具。

只需要在你的浏览器插件商店中下载并安装,就可以准备安装各种各样的油猴插件了。

正方教务系统油猴插件

插件仓库:https://github.com/love98ooo/ZhengFangJiaoWuSystemTampermonkeyScript

很多插件都发布在 Greasy Fork 上面,但是这个插件本就小众,只面向南邮的同学,所以为了更方便地开发和维护,这个插件就只在 GitHub 上面发布了。

在插件仓库的说明页面点击安装链接,

2023-07-02_190716

如果油猴是正常的,就会进入如下页面,点击安装(Install)即可。

2023-07-02_083838

来到正方教务系统的成绩查询页面,这里多了一个按钮查询考试详细分数,先不用管它,正常选择学年学期,点击按学期查询按学年查询

2023-07-02_084320

你会和往常一样看到成绩表,

2023-07-02_084657

再点击按钮查询考试详细分数,屏幕中间会出现这样的窗口,包括平时分卷面分期末 : 平时这些原本成绩表上面没有的数据。

2023-07-02_085616

如果发现有 Bug 或者可以改进的地方,点击下方的反馈 Bug按钮,你的将跳转到项目 GitHub 仓库的 Issues 页面,在这里你可以提交 Issues。

如果这个插件对你有帮助,欢迎 Star ⭐️ ,这能让作者开心很久。

插件的开发

起因

最早的时候是学长在群里面分享了一段运行在控制台(console)的代码,

2023-07-02_190855

但是每次使用都需要找到这个文件,复制这段代码到控制台,非常繁琐。而且输出既不易读,又容易抽风,

2023-07-02_191427

所有我就想能不能让这段代码在我查分的时候自动执行,于是我想到了油猴,我可以把这段代码放在油猴里面,而且还可以使用表格展示。

起步

虽然这是我第一次开发油猴插件,不过之前写过一些前端的东西,看看别人的插件怎么写的、问问New Bing,很快就能上手。

2023-07-02_194935

我在页面上添加了一个查分按钮,点击之后会以表格的形式展示所有的内容。

2023-07-02_195025

此时基本解决了最初的痛点,可以只需要在正常查分的基础上,鼠标额外点击一下查分按钮就可以执行相关查分代码,同时清晰地展示平时分卷面分

优化

数据

但是数据容易抽风的问题还是没有得到解决,比如物理实验 (上)会被识别为物理,还会有如上图所示的乱码�$。这里非常感谢 FishZe 同学(算法大佬),他的PR解决了数据匹配异常的问题。

2023-07-02_224755

虽然不方便展示里面的内容,但是我可以很确定地告诉你,数据的错误减少了非常多。

2023-07-02_202031

样式

我平时是写后端的,对于CSS可以说是一窍不通,已有样式仅限于能对齐,其他样式和它们的 Bug 基本都是使用 New Bing 解决的。所以非常感谢 Maxtune Lee 学长(前端大佬)修改CSS,

2023-07-02_224924

和前面的表格对比,明显美观了许多。

2023-07-02_203312

Debug

既然是新开发的东西,开发时的测试用例和测试环境都非常有限,难免的使用的过程中出现一些意料之外的 Bug。

2023-07-02_222339

错误的占比

这个是之前PR中添加的新功能,查看平时分和卷面分的占比,这个结果是通过计算倒推得知的,原始代码如下:

if(!isNaN(totalPoints[i]) && !isNaN(examPoints[i]) && !isNaN(ordinaryPoints[i]) && Number(examPoints[i]) != 0 && Number(ordinaryPoints[i]) != 0){
	if(Number(examPoints[i]) == Number(ordinaryPoints[i])){
		scaleCell.textContent = "5:5";
	} else {
		let a = ((10 * Number(totalPoints[i]) - 10 * Number(ordinaryPoints[i])) / (Number(examPoints[i]) - Number(ordinaryPoints[i]))).toFixed(1);
		scaleCell.textContent = a.toString();
		scaleCell.textContent +=  ":" + (10 - a).toFixed(1).toString();
	}
} else {
	scaleCell.textContent = "";
}

下面是 ChatGPT 对于这段代码的解释:

这段 JavaScript 代码是一个条件语句,根据一系列条件来计算和显示一个分数比例。下面是对代码的解释:

  1. if(!isNaN(totalPoints[i]) && !isNaN(examPoints[i]) && !isNaN(ordinaryPoints[i]) && Number(examPoints[i]) != 0 && Number(ordinaryPoints[i]) != 0): 这个条件语句首先检查三个数组中的元素是否都是数字,然后检查examPoints[i]ordinaryPoints[i]是否都不等于零。如果所有条件都满足,进入条件块内执行相应的逻辑。
  2. if(Number(examPoints[i]) == Number(ordinaryPoints[i])): 这个条件判断语句检查examPoints[i]ordinaryPoints[i]是否相等。如果相等,表示考试分数和普通分数相同,进入条件块内执行相应的逻辑。
    • scaleCell.textContent = "5:5";: 将"5:5"文本设置为scaleCell元素的内容。这表示考试分数和普通分数的比例为5:5,即50%:50%。
  3. 如果examPoints[i]ordinaryPoints[i]不相等,表示考试分数和普通分数不同,执行 else 块中的逻辑。
    • let a = ((10 * Number(totalPoints[i]) - 10 * Number(ordinaryPoints[i])) / (Number(examPoints[i]) - Number(ordinaryPoints[i]))).toFixed(1);: 这行代码计算比例系数 a,通过对总分数和普通分数进行加权平均计算。具体计算方式是:总分数减去普通分数的10倍,再除以考试分数减去普通分数,结果保留一位小数。
    • scaleCell.textContent = a.toString();: 将 a 转换为字符串,并将其设置为 scaleCell 元素的内容。
    • scaleCell.textContent += ":" + (10 - a).toFixed(1).toString();: 将一个冒号(":")和 (10 - a) 的结果保留一位小数的字符串附加到 scaleCell 元素的内容之后。这表示比例为 a 和 (10 - a)。
  4. 如果任何一个条件不满足,则执行 else 块内的逻辑。
    • scaleCell.textContent = "";: 将 scaleCell 元素的内容设置为空字符串。

总体来说,这段代码用于根据给定的考试分数、普通分数和总分数,计算并显示一个比例值,其中考试分数和普通分数的比例是在总分数中的权重。

这段代码的逻辑没有什么问题,但是在数学上存在漏洞,即当平时分和卷面分比较接近时,由于总分是四舍五入的结果,在数学层面上是无法得到正确的比例的,比如如下测试样例:

平时分:92.9
卷面:92.5
总分:93
['2021-2022,2,B00000000,XXXX,必修, ,1.0, ,4.30,92.9, ,92.5, ,93,0, , , ,XX院, ,0, ,0,1,2,3']

经过计算得到的结果是 -2.5:12.0,这显然不正确。

此外,我们知道学校的平时分和卷面分比例一定是整数,所以修改结果如下:

if (!isNaN(totalPoints[i]) && !isNaN(examPoints[i]) && !isNaN(ordinaryPoints[i]) && Number(examPoints[i]) != 0 && Number(ordinaryPoints[i]) != 0) {
	if (Number(examPoints[i]) == Number(ordinaryPoints[i])) {
		scaleCell.textContent = "Unknown";
	} else {
		let a = ((10 * Number(totalPoints[i]) - 10 * Number(ordinaryPoints[i])) / (Number(examPoints[i]) - Number(ordinaryPoints[i]))).toFixed(0);
		if (a <= 0 || a >= 10) {
			scaleCell.textContent = "Unknown";
		} else {
			scaleCell.textContent = a.toString();
			scaleCell.textContent += ":" + (10 - a).toString();
		}
	}
} else {
	scaleCell.textContent = "";
}

这里将无法得知的情况和错误的情况设置为 Unknown,以免误导用户;此外,将结果设置为整数,也更符合用户使用习惯。

按钮的样式

这个 Bug 是一个学长在按学年查询的时候发现的,由于当时就在我旁边,没提 Issue 而是口头跟我说的。

当课程的数量超过20个时,会出现按钮悬浮在表格上面,而不是位于表格下方的空白处。

这个 Bug 与 Flex 布局有关,所以我在修复这个 Bug 的同时,也调整了其他元素的布局,试图让插件窗口布局更加统一。

2023-07-02_224700

对校外的访问者无法查看成绩

查看 Issue

这是一个非常神奇的 Bug,它产生的原因是使用学校的 webvpn 访问教务系统时,原本用于判断是否在对应页面的 iframeautoheightcontentWindow.location.href 参数被 webvpn 的一个函数置为 null

2023-07-03_115901

2023-07-03_120039

所以脚本一直认为不在查看成绩的页面,不工作。在 Debug 的过程中发现很多信息都会在被调用之后置为 null,所幸正方教务系统还有直接写在 HTML 文件里的 breadcrumb 导航,我能够不用通过会被篡改的 window.location 属性判断是否在对应页面。

2023-07-02_230910

意外收获(开始逆向)

从这里开始就与插件本身无关了,单独开一篇文章太短了,又是在插件开发过程中发现的,就加在这里了。

意外收获指的是南邮 webvpn 后面加密后的字符串对于是使用固定 key 的 AES 对称加密算法计算后的得到的字符串,经过比对确认,加密结果正确,相关代码如下:

2023-07-03_130919

不过具体的对于 webvpn 的技术细节还有待发掘,整个 webvpn 混淆打包后的 JS 文件在这 => bundle.debug.js。欢迎大佬前来逆向解答,鄙人实力有限,目前的进展仅限于告诉各位大佬字符串的操作在这个文件的关键位置里使用非常多,比如:

2023-07-03_123635

2023-07-03_124035

此外,有些关键词也未被混淆:

  • enlink
  • http
  • https
  • crypt
  • ……

为了方便我校广大无法使用 Enlink 客户端的同学在校外能方便地访问教务系统和知网,我在这里给出计算后的网址:

2023-07-03_133639

2023-07-03_133732

最后

这次插件的开发经历确实让我收获颇丰,同时也非常感谢为插件做出贡献的另外两位同学。

如果你觉得插件有什么值得改进的,欢迎 Issues(能 PR 更好)。

如果你觉得文章有什么不对的,欢迎评论区留言或者私信告诉我。

Be a Neutral Listener, Dialectical Thinker, and Practitioner of Knowledge