需求分析
- 每天规定时间自动签到
- 签到后能进行免费自动抽奖
- 脚本执行成功后, 能发送签到结果以及抽奖结果到邮箱
- 可以支持多个人并发运行脚本
设计思路
- 首先每个用户应该是独立执行的脚本,通过pm2来管理脚本进程
- 用户可以手动设置
掘金Cookie
以及脚本执行时间 - 用户可以设置邮箱来接收签到结果
- 用户还可以查看脚本执行情况(需要一个日志列表)
- 用户如果不想自动签到还可以手动关闭签到脚本
准备工作
环境搭建
- 安装
nvm
管理 node - 安装
node-schedule
来实现定时任务执行 - 安装
nodemailer
来发送邮件通知 - 安装
axios
来请求掘金API - 安装
pm2
来管理多个人开启的进程
前期准备
- 需要授权得到授权码的
邮件地址
用于向用户发送邮件通知 - 需要一个服务器来执行应用, 如果只自己使用则只需要本地启动, 保持电脑不关机
掘金相关API
- api.juejin.cn/growth_api/... // 查看是否签到
- api.juejin.cn/growth_api/... // 签到接口
- api.juejin.cn/growth_api/... // 获取免费抽奖次数
- api.juejin.cn/growth_api/... // 抽奖接口
- api.juejin.cn/growth_api/... // 获取剩余矿石数量
- api.juejin.cn/study_api/v... // 获取掘金活动
不需要Cookie
项目开始
搭建一个登录页面
- 登录即注册,如果是新的用户名,则是注册;如果是存在的则会校验用户密码
- 邮箱可填可不填,主要用于是否需要邮件通知
javascript
const { username, password } = loginForm
if (!username || !password) {
resolve(returnMsg('账号名或密码不可为空', null, 500))
return
}
if (!/^[A-Za-z0-9]+$/.test(username)) {
resolve(returnMsg('账号名只能包含英文与数字', null, 500))
return
}
const userInfo = checkUserIsExist(username)
if (userInfo && userInfo.password !== password) {
resolve(returnMsg('密码不正确', null, 500))
return
}
const fileName = `${__dirname}/config/token.js`
const { info: fileContent, watcherToken } = handleLoginInfo(userInfo, loginForm)
await writeFileSync(fileName, fileContent)
res.setHeader('Access-Control-Allow-Credentials', 'true')
res.setHeader('Set-Cookie', [`username=${username}`, `watcherToken=${watcherToken}`])
// 用户名已存在时登陆 => 会重新生成watcherToken
resolve(returnMsg())
主要代码接口
- checkUserIsExist: 主要从用户表查看是否存在当前用户
- handleLoginIofo: 主要是生成一个用户的唯一watcherToken
- writeFileSync(): 主要是将用户写入用户表,新用户直接写入,老用户主要是更新watcherToken以及邮箱地址
- 最后设置Cookie
进入主页面
主页面主要分为: 配置模块
,脚本模块
,日志模块
,活动列表
配置模块
点击设置进入配置页面
- 可以更新
掘金Cookie
,设置的Cookie会存入用户表的对应用户下
- 可以设置接收签到结果邮箱接收地址
- 可以设置脚本执行时间(不设置默认签到时间:早上8点整)
- 所有的设置
需要重启脚本
才能起效
脚本模块
- 可以开启自动签到脚本
- 可以关闭自动签到脚本
- 下面脚本进程就是检测当前脚本是否开启成功,对应name就是 autoScript-<登录名>
日志模块
- 获得所有脚本签到结果日志列表
- 包括脚本签到执行日期,剩余矿石数,免费抽奖结果,邮件发送是否成功等
- 可以手动去更新日志列表(不过每日签到应该作用不大)
活动列表
- 不需要设置掘金Token也可以获取的数据
- 主要展示掘金相关的活动
- 活动主要包括当前进行中的以及历史的一些活动
核心代码
自动签到脚本执行
- 开启一个定时任务执行自动签到主函数
javascript
const rule = new schedule.RecurrenceRule()
rule.hour = hour
rule.minute = minute
rule.second = second
schedule.scheduleJob(rule, main)
- 主函数流程
graph LR
A[获得当前用户对应的掘金Cookie] --> B[查看是否已经签到] -- 已签到 --> C[则无需再去签到]
B -- 未签到 --> 则调用接口去签到 --> D[获取签到结果] --> E[获取当前剩余矿石数量] --> F[获取免费抽奖次数]
C --> D
graph LR
A[获取免费抽奖次数] -- 次数0 --> B[没有免费次数则无需调接口去抽奖, 不然浪费矿石]
B --> D
A -- 次数1 --> C[则调用接口去抽奖] --> D[获取抽奖结果] --> E[根据当前用户是否设置邮件来发送执行结果]
javascript
async function main () {
LOG_MSG.currentTime = `${getCurrentDate()} ${getCurrentTime()}`
if (!username || !COOKIE) {
LOG_MSG.error = '用户不存在,脚本执行失败'
await toLog()
return
}
const isSignedObj = await checkIsSignedIn(COOKIE)
if (isSignedObj) {
const { err_no, err_msg } = isSignedObj
if (err_no === 403 || err_msg === 'must login') {
LOG_MSG.error = '当前Token登录失败, 请重新设置正确的Token'
toLog()
// Todo: 关闭脚本?
return
} else {
LOG_MSG.signStatus = true
EMAIL_MSG.signStatus = '今日已签到'
}
} else {
const signStatusObj = await toSignIn(COOKIE)
if (signStatusObj.data) {
LOG_MSG.signResult = 'true'
EMAIL_MSG.signStatus = '签到成功'
} else {
LOG_MSG.signResult = signStatusObj.err_msg || 'false'
EMAIL_MSG.signStatus = signStatusObj.err_msg || '签到失败'
}
}
const count = await toGetPointCount(COOKIE)
LOG_MSG.remainedPoint = count || 0
EMAIL_MSG.pointCount = count || 0
const times = await queryFreeTimes(COOKIE)
if (times) {
LOG_MSG.freeDrawTimes = times
const { lottery_name } = await toDraw(COOKIE)
LOG_MSG.drawResult = lottery_name
EMAIL_MSG.drawStatus = `${lottery_name}\n`
} else {
LOG_MSG.freeDrawTimes = 0
EMAIL_MSG.drawStatus = '今日免费抽奖次数已用完'
}
if (email) {
try {
const emailResult = await sendEmail(email, handleEmailMessage())
LOG_MSG.emailStatus = emailResult.messageId ? true : false
} catch {
LOG_MSG.emailStatus = false
}
}
toLog()
}
- 邮件发送
javascript
const transporter = nodemailer.createTransport({
host: SENDER.HOST,
port: SENDER.PORT,
secureConnection: true, // 不写这句会报错:Greeting never received
auth: {
user: SENDER.USER,
pass: SENDER.PASS
}
})
export const sendEmail = async (userEmail, content = RECIPIENT.DEFAULTMSG) => {
return await transporter.sendMail({
from: SENDER.USER, // 发送邮件的地址
to: userEmail, // 接收邮件的地址
subject: RECIPIENT.SUBJECT, // 邮件标题
text: content, // 有 html,优先 html
html: '', // html body
attachments: '' // 附件
})
}
SENDER.USER: 设置一个用于发送给用户的邮箱
SENDER.PASS: 该邮箱对应的授权码
javascript
export const SENDER = {
HOST: 'smtp.qq.com',
PORT: 465,
USER: '', // 授权smtp邮箱地址
PASS: '' // 申请的授权码
}
export const RECIPIENT = {
// 由用户设置
// USER: '',
SUBJECT: '掘金签到成功',
DEFAULTMSG: `您于${new Date().toLocaleString()}, 在自动签到系统中签到成功`
}
PS: 这是作者自己搭的服务, 读者可自动决定是否加入开启敲到脚本
PS: 如果有任务问题, 请在下面评论区提问