关于疫情统计表的自动填报脚本

学校要求每天都要填疫情相关的情况统计表,每天都需要去那边填一下,而且时间有严格的要求,早上很早就截止了。手动提交实属麻烦,于是开始想方法做自动化。

API

首先要实现自动填报,自然是要抓到API。填报的入口在微信,但是在登录后未见操作另外再调用微信进行鉴权。

通过抓包发现进入页面后和页面对应后端进行通信的鉴权是通过AES加密的两个参数实现的,这两个参数直接附在了URL上,这意味着我们很轻松就能实现自动填报。

由于开发脚本当天已经填报过了,所以直接JS里找到提交方法提交,用Burp截包提取到提交的参数。很意外的是这个提交接口是没有任何鉴权的,那自动填报脚本就更好做了。

有了抓包的基本信息后面的问题就很好解决。

脚本

数据是一个表单序列化成了JSON,常规操作,把表单序列化之后的内容提取出来,放到JSON文件里,后续填报的时候直接反序列化这一份数据,修改好之后提交过去就行了。

考虑到表单里面含有日期参数,引入dayjs根据参数的形态构造一个日期的字符串,填入JSON内日期字符串所在的位置,就能够实现动态改变数据里的日期。

剩下的操作就是node-cron + axios提交就行了,API也没有CORS限制,安全方面很松,加了CORS限制也无妨,走cors-anywhere等就能解决问题。

第一天测试这个失败了,然后我被挂在了未及时填报的名单上。后面追查原因发现服务器接受的参数不一般,JSON数据要先使用JSON.stringify序列化成字符串之后再进行提交。

一开始我也很奇怪为什么会出现这样的错误,如果数据提交过去了,对于Java后端来说做不做这一步操作其实都是一样的,发送过去的东西本身就不会是一个没有序列化的数据。

这个坑其实是出在axios上,这个接口不是POST方法,而是GET方法,在用GET方法发请求的时候,params里的对象是没有办法正确传递过去的,我的代码也没有预先设置好序列化的东西。

从API来看它这个表单参数的体现是JSON序列化后的字符串,所以我先把它序列化好就能正常提交。

增强

脚本姑且到这里是能用了,但是缺少一个提醒,不知道每天填报是不是做好了,而且也缺少一个容错的措施,万一网络波动了没有填报上,那么后续最好还要重试。

重试这个简单,在规定时间范围内执行多次cron,如果当天没有成功就持续重试,成功了就不用再重试了。

提醒这方面主要是通过邮件,短信的话需要一定费用,我没有开通短信包所以没有选用短信。邮件使用node-mailer对接SMTP就能直接发邮件,我选用的是我自己的阿里企业邮SMTP,成功和失败都发通知,就解决了通知方面的问题。

每天看到通知就能知道是不是填成功了,不至于无感知还要人工确认,每天人工确认不如自己手动提交来得稳。所以提醒还是很重要的。

做好了这一切就上线运行了,至今几天都运转稳定,感觉良好。

代码

代码基于MIT协议开源,附表单模板。API地址出于安全原因不提供,如果你也是同校的同学,请自行抓包。这个代码还有改进的控件,比如抽出SMTP配置和模板到文件里面,但是不太想改进了,毕竟这是一个临时工具,不值得投入那么多。

具体的用法请阅读代码自行理解。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
const axios = require('axios');
const dayjs = require('dayjs');
const cron = require('node-cron');
const log4js = require('log4js');
const nodemailer = require("nodemailer");

log4js.configure({
appenders: {
log2file: {
type: 'file',
filename: 'submit.log'
}
},
categories: {
default: { appenders: ['log2file'], level: 'debug' },
}
});

const logger = log4js.getLogger();

/* Submit data */
const formId = '107';
const deptCode = '047';
const classNo = '';
const deptName = '所在部门';
const data = require('./data.json');

logger.debug('填报脚本开始执行');
var status = {};

async function sendMail(date, status, message) {
let transporter = nodemailer.createTransport({
host: "",
port: 465,
secure: true, // true for 465, false for other ports
auth: {
user: '',
pass: ''
}
});

let content = '';

if (status == 'success') {
content = '<div style="text-align: center;><p style="margin: 8px 0; font-size: 16px; color:#4f4f4f">表单填报任务通知</p></div>'+
`<div style="text-align: center;margin-top: 4px;"><p style="font-size:14px;color:#6e6e6e">${date}</p></div>`+
'<div style="margin-top: 12px;border-radius:16px;padding:12px 20px;background:#67C23A"><p style="color:#fff;font-size:15px">今天的表单填报任务已完成</p></div>'+
'<div style="text-align: center;margin-top:18px"><p style="font-size: 12px;color:#cecece">表单自动填报 - Made by BackRunner</p></div>';
} else {
content = '<div style="text-align: center;><p style="margin: 8px 0; font-size: 16px; color:#4f4f4f">表单填报任务通知</p></div>'+
`<div style="text-align: center;margin-top: 4px;"><p style="font-size:14px;color:#6e6e6e">${date}</p></div>`+
`<div style="margin-top: 12px;border-radius:16px;padding:12px 20px;background:#F56C6C"><p style="color:#fff;font-size:15px">填报任务出现错误:${message}</p></div>`+
'<div style="text-align: center;margin-top:18px"><p style="font-size: 12px;color:#cecece">表单自动填报 - Made by BackRunner</p></div>';
}

transporter.sendMail({
from: '',
to: "",
subject: `表单填报任务 - ${date}`,
text: "今天的表单填报任务已完成",
html: content
});
}

cron.schedule('10 0,1,2,3,4,5,6,7,8 * * *', () => {
logger.debug('填报任务开始执行');
let date = dayjs().format('YYYY-MM-DD');
data[0].content = date;
if (!status[date]) {
axios.get(`/front/submit`, {
params: {
username: data[3].content,
formId: formId,
//deptCode: deptCode,
//classNo: classNo,
//deptName: deptName,
datas: JSON.stringify(data),
dateStr: date
}
}).then(response => {
if (response.status == 200) {
if (!response.data.success) {
logger.error('填报失败 - '+JSON.stringify(response.data));
sendMail(date, 'error', JSON.stringify(response.data));
return;
}
logger.info('填报成功');
sendMail(date, 'success');
status[date] = true;
} else {
logger.debug('填报请求提交失败');
sendMail(date, 'error', '提交失败');
}
});
} else {
logger.debug('今日已成功填报');
}
});

data.json(填报信息模板):
Tips:括号里的内容需要你自行填写

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
[{
"ffId": 383,
"fid": "107",
"content": ""
}, {
"ffId": 384,
"fid": "107",
"content": "(班级)"
}, {
"ffId": 385,
"fid": "107",
"content": "(名字)"
}, {
"ffId": 386,
"fid": "107",
"content": "(学号)"
}, {
"ffId": 387,
"fid": "107",
"content": "学生"
}, {
"ffId": 388,
"fid": "107",
"content": "(性别)"
}, {
"ffId": 389,
"fid": "107",
"content": "(身份证号)"
}, {
"ffId": 390,
"fid": "107",
"content": "(电话)"
}, {
"ffId": 391,
"fid": "107",
"content": "正常"
}, {
"ffId": 392,
"fid": "107",
"content": "无"
}, {
"ffId": 393,
"fid": "107",
"content": "否"
}, {
"ffId": 394,
"fid": "107",
"content": "(省份)"
}, {
"ffId": 395,
"fid": "107",
"content": "(详细地址)"
}, {
"ffId": 396,
"fid": "107",
"content": "否"
}, {
"ffId": 397,
"fid": "107",
"content": "否"
}, {
"ffId": 398,
"fid": "107",
"content": "否"
}, {
"ffId": 399,
"fid": "107",
"content": "否"
}, {
"ffId": 400,
"fid": "107",
"content": "否"
}, {
"ffId": 401,
"fid": "107",
"content": "否"
}, {
"ffId": 402,
"fid": "107",
"content": "否"
}, {
"ffId": 403,
"fid": "107",
"content": ""
}, {
"ffId": 404,
"fid": "107",
"content": ""
}, {
"ffId": 405,
"fid": "107",
"content": ""
}, {
"ffId": 406,
"fid": "107",
"content": ""
}, {
"ffId": 407,
"fid": "107",
"content": "是"
}, {
"ffId": 408,
"fid": "107",
"content": "是"
}, {
"ffId": 409,
"fid": "107",
"content": ""
}]

package.json

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
{
"name": "form-auto-submit",
"version": "1.1.0",
"description": "Auto submit form to system",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "BackRunner",
"license": "MIT",
"dependencies": {
"axios": "^0.19.2",
"dayjs": "^1.8.21",
"log4js": "^6.1.2",
"node-cron": "^2.0.3",
"nodemailer": "^6.4.4",
"urlencode": "^1.1.0"
}
}

建议使用pm2运行。

更新:method修改

接口从GET转成了POST,GET会返回405,需要调整调用方式。

axios的POST需要规避相关的坑,对于升级后端,提交的参数需要进行一定调整,有在使用该脚本的最好抓个包把参数也更新一下。

特别提醒

填报模板基于完全正常状态制作,如果你的情况特殊,请如实手动填报表格。

这里提供的代码不保证稳定性,如果你的填报出现了问题请自担责任,与我无关。