上应大健康上报器

登录后,每天打开 http://xgfy.sit.edu.cn/h5/#/pages/index/report 时,自动检查上报状态,发出上报请求,并刷新页面。

// ==UserScript==
// @name        上应大健康上报器
// @description 登录后,每天打开 http://xgfy.sit.edu.cn/h5/#/pages/index/report 时,自动检查上报状态,发出上报请求,并刷新页面。
// @namespace   UnKnown
// @author      UnKnown
// @icon        
// @match       http://xgfy.sit.edu.cn/h5/*
// @version     1.2
// @require     https://unpkg.com/js-md5/build/md5.min.js
// @grant       GM_getValue
// @grant       GM_setValue
// @run-at      document-idle
// @inject-into auto
// @compatible  脚本运行环境必须支持 GM_getValue() 和 GM_setValue()
// @license     AGPL-3.0-or-later or CC-BY-NC-SA-4.0
// ==/UserScript==

"use strict";

location.hash.startsWith("#/pages/index/jksb") &&
(() => {

// lastTime
// 脚本存储的上次上报时间

// 初始化
GM_getValue("lastTime") === undefined &&
GM_setValue("lastTime", 0); // 1970/1/1

// getLastTime(): date
// 将 lastTime 转换回 Date 对象并返回
// JavaScript 的 Date 对象其实是 DateTime 对象…
const getLastTime = () => new Date(GM_getValue("lastTime"));

// logLastTime()
// 输出 lastTime 的日期
const logLastTime = () => console.log(
	"lastTime: ".concat(getLastTime().toLocaleDateString())
);

// setLastTime()
// 设置 lastTime 并输出其日期
const setLastTime = ( timeStamp = Date.now() ) => {
	GM_setValue( "lastTime", timeStamp );
	logLastTime();
};

// 每次打开页面时,输出一次脚本存储的上次上报时间
logLastTime();

// 页面中的 uni-app 的根属性,具体详见 https://uniapp.dcloud.io/
const uni = unsafeWindow.uni;

// showToast(title, icon, duration)
const showToast = uni

	? (title, icon = "none", duration = 1500) =>
		uni.showToast({ title, icon, duration })

	: (title, icon = "none", duration = 1500) => {

		const background = document.createElement("div");
		background.style =
"display: flex; pointer-events: none;" +
"position: fixed; top: 0; right: 0; bottom: 0; left: 0; z-index: 999";

		const toast = document.createElement("div");
		toast.textContent = title;
		toast.style =
"margin: auto; padding: 10px 20px; border-radius: 5px; font-size: 13px;" +
"color: #fff; background-color: rgba(17, 17, 17, .7);" +
"text-align: center; word-break: break-all; white-space: normal;";

	background.appendChild(toast);
	document.body.appendChild(background);
	setTimeout(
		() => document.body.removeChild(background), duration
	);

};

// isNotToday(date): Boolean
// 判断给出的 Date 对象的日期是否不是今天,返回 true 或 false
// 如果上次上报的日期不是今天,那就要上报喽
const today = new Date();
const isNotToday = date => [
	"getFullYear", "getMonth", "getDate"
].some(
	method => date[method]() !== today[method]()
);

// getElement(selector=...): Element | null
// 尝试获取最新一条上报记录的日期元素
const getElement = (
	selector = '.cu-list.menu-avatar > .cu-item:first-child > .action'
) => document.querySelector(selector);

// 为什么 getElement() 不在 isNotReported() 里面:
// 页面加载完成后,需要先异步获取上报记录,才能生成对应的元素,依网络情况不同,
// 这可能需要几秒,甚至可能失败;所以,在 isNotReported() 之后的代码里,需要
// 用到 getElement() 来进行数次定时尝试。

// 那为啥不用 MutationObserver?
// 依签到天数,可能会同时生成上百条上报记录,不值得。

// ----

// isNotReported(element) : Boolean | null
// 按上报记录中的日期字符串判断是否未上报
const isNotReported = element => {

	// parseElement(element)
	// 将日期元素处理成日期字符串
	const parseElement = element => element.textContent.trim();

	// parseDateString(dateStr)
	// 将形如 19700101 的日期字符串转换为 Date 对象,若无法转换则返回 null
	const parseDateString = dateStr => {

		// dateStr: "19700101"
		// dateArr: [1970, 1, 1]
		if (dateStr.length === 8) {
			const dateArr = [ [0, 1, 2, 3], [4, 5], [6, 7] ].map( 
				indexArr => Number.parseInt(
					indexArr.map( index => dateStr[index] ).join("")
				)
			);
			if ( dateArr.every( num => Number.isInteger(num) && num > 0 ) ) {
				return new Date( // --dateArr[1] : months range from 0 to 11, not 1 to 12
					Date.UTC( dateArr[0], --dateArr[1], dateArr[2] )
				);
			}
			else {
				console.warn("dateArr should contain 3 positive integer");
				return null;
			}
		} else {
			console.warn("the length of dateStr must be 8");
			return null;
		}

	};

	// 按照 Date 对象的可用性与日期,返回结果
	const pageLastTime = parseDateString(parseElement(element));
	if ( pageLastTime !== null ) {
		if ( isNotToday(pageLastTime) ) {
			return true;
		} else {
			// 如果页面中的上报记录为今天,额外提示“今天已经上报过了”
			showToast("今天已经上报过了");
			// 同时,如果脚本存储的上次上报时间不是今天,就更新一下
			isNotToday(getLastTime()) && setLastTime();
			return false;
		}
	} else {
		return null;
	}

};

// report()
// 自动上报
const report = () => {

	// data
	// data 对象,包含所有请求内容,由用户数据和上报信息构成

	// 尝试获取用户数据
	const rawUserInfo = uni
		? uni.getStorageSync("userInfo")
		: localStorage.getItem("userInfo");

	if (!rawUserInfo) {
		console.error("userInfo not found");
		return false;
	}

	// 将 JSON 格式的用户数据解析为对象
	const userInfo = JSON.parse(rawUserInfo);

	// 准备 data 对象
	const data = {

		// 布尔值:{ 否: 0, 是: 1 }

		/* 用户数据 */
		usercode: userInfo.code, // 学号
		username: userInfo.name, // 姓名
		usertype: userInfo.usertype, // 用户类型(数字)学生默认为 2
		jiguan: userInfo.jiguan, // 籍贯 { 武汉: 1, 湖北: 2, 其他: 3 }
		deptno: userInfo.deptno, // 部门/学院代号

		/* 今日身体情况 */
		currentsituation: 3, // 新冠诊断 { 确诊: 1, 疑似: 2, 正常: 3, 无症状感染者: 4 }
		           wendu: 0, // 今日体温 { ≤37.3℃: 0, >37.3℃: 1 }
		            ksfl: 0, // 其他症状/咳嗽乏力(布尔值)
		        mjjcqzhz: 0, // 密切接触确诊患者(布尔值)
		     qwhtjzgfxdq: 0, // 14天内前往或途经中高风险地区(布尔值)
		  tzrqwhtjzgfxdq: 0, // 同住人14天内前往或途经中高风险地区(布尔值)

		/* 今日位置 */
		     position: "上海市-市辖区-奉贤区", // 当前位置 "省-市-区县"
		     inschool: "1", // 是否在校(布尔值)
		  szdsfzgfxdq: "0", // 所在地区是否为中高风险地区(布尔值)
		szdssfyzgfxdq: "0", // 所在地市(区)是否有中高风险地区(布尔值)

		/* 今日行程 */
		 mdd: "请选择", // 目的地 "请选择" / "省-市-区县"
		  sy: null,     // 行程事由
		drwf: 0,        // 当日往返(布尔值)
		jtgj: 0,        // 交通工具
		cfsj: "请选择", // 出发时间 "请选择" / "yyyy-MM-dd"
		jtcc: null,     // 航班号/车次号/交通车次

		// 备注     | ID
		remarks: "" , id: 0

	};

	// sign: {decodes, ts}
	// 请求头中必须包含 sign 对象的全部属性,否则服务器不会接受请求
	const sign =
		unsafeWindow.getSign instanceof Function
			? unsafeWindow.getSign(data.usercode)
		: md5 instanceof Function
			? ((str, time) => {
				str = md5( str + "Unifri" + time ).toUpperCase();
				return {
					decodes: str.slice(16) + str.slice(0, 16),
					ts: time
				};
			})( data.usercode, Date.now() )
		: null;

	if (!sign) {
		console.error("Can't create sign");
		return false;
	}

	const confirmReload = (str, error) => {
		console.error(error);
		confirm(
			str + "时发生错误,错误日志已输出至控制台:\n" +
			error + "\n是否刷新页面并重试?"
		) && location.reload();
	};

	// fetch
	// 发出请求,处理相应
	if ( data && sign ) fetch(
		"http://210.35.96.114/report/report/todayReport", {
			method      : "POST",
			mode        : "cors",
			credentials : "omit",
			referrer    : "http://xgfy.sit.edu.cn/h5/",
			headers: Object.assign(
				{ "Content-Type" : "application/json" }, sign
			),
			body: JSON.stringify(data)
		}
	).catch(
		error => confirmReload("请求")
	).then( // 将响应对象中的数据作为 JSON 处理,并转换为对象
		// { "code":0,"msg":null,"data":null }
		response => response.json()
	).catch(
		error => confirmReload("解析响应数据")
	).then( // 上报成功
		jsonObj => {
			console.info(jsonObj); // 输出响应数据
			setLastTime();         // 存储(并输出)上次上报时间
			showToast("上报成功", "success");
			setTimeout( () => showToast("即将刷新页面") , 1750 );
			setTimeout( () => location.reload()         , 2500 );
		}
	);
	else console.error(
		!data ? "data is not ready" :
		!sign ? "sign is not ready" :
		"what"
	);

};

// showHistory(selector=...)
// 自动切到签到历史
const showHistory = (
	selector = '.nav .cu-item[data-id="1"]'
) => {
	const history = document.querySelector(selector);
	history && history.click();
};

// enableButton(selector=...)
const enableButton = (
	selector = 'uni-button[disabled]'
) => document.querySelectorAll(selector).forEach(
	button => button.removeAttribute("disabled")
);

// fallback(message = "发生未知错误")
// 改按 lastTime 判断今天是否没上报。lastTime 不一定准确,所以,lastTime 判定
// 没上报就自动上报,但要是 lastTime 判定上报了,还是弹对话框问一下要不要上报。
const fallback = ( message = "发生未知错误" ) => {
	console.warn( message + ",改用脚本存储的上次上报时间" );
	(
		isNotToday(getLastTime()) ||
		confirm(
			message +
			",无法获取页面中的上报记录,但脚本存储的上次上报时间为今天。\n" +
			"是否上报并刷新页面?"
		)
	) && report();
};

// ----

// 定时查找页面元素

// 间隔时间(毫秒)
const interval = 500;

// 重试次数
let retry = 30;

const intervalId = setInterval(
	() => {
		enableButton();
		const element = getElement();
		if ( element !== null ) {
			clearInterval(intervalId);
			switch ( isNotReported(element) ) {
				case true: report(); break;
				case false: showHistory(); break;
				case null: fallback("找到了上报记录元素,但无法解析");
			}
		} else if ( --retry <= 0 ) {
			clearInterval(intervalId);
			fallback("找不到上报记录元素");
		}
	}, interval
);

})();