├── .gitignore ├── jest.config.js ├── src ├── types │ └── index.d.ts ├── utils │ ├── showResult.ts │ ├── ConfigPage.ts │ ├── login.ts │ ├── sign.ts │ ├── queryActiveTask.ts │ └── getCourseIds.ts ├── app.ts └── controllers │ └── rootController.ts ├── tsconfig.json ├── package.json ├── README.md └── LICENSE /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: 'ts-jest', 3 | testEnvironment: 'node', 4 | }; -------------------------------------------------------------------------------- /src/types/index.d.ts: -------------------------------------------------------------------------------- 1 | declare type CourseIDItem = { 2 | classId: string 3 | courseId: string 4 | title: string 5 | } 6 | declare type ActiveItem = { 7 | classId: string 8 | courseId: string 9 | title: string 10 | activeId: string 11 | } 12 | -------------------------------------------------------------------------------- /src/utils/showResult.ts: -------------------------------------------------------------------------------- 1 | type ResultItem = { 2 | title: string 3 | success: boolean 4 | } 5 | 6 | export const showResult = (resultArray: ResultItem[]) => { 7 | let count = { 8 | success: 0, 9 | fail: 0 10 | } 11 | resultArray.forEach(item => { 12 | if (item.success) { 13 | count.success++ 14 | } else { 15 | count.fail++ 16 | } 17 | }) 18 | console.log('💰 签到结果') 19 | return `🚗 成功 ${count.success} 门,失败: ${count.fail} 门` 20 | } 21 | -------------------------------------------------------------------------------- /src/utils/ConfigPage.ts: -------------------------------------------------------------------------------- 1 | import { Page } from 'puppeteer' 2 | 3 | class ConfigPage { 4 | static noImageRequest = async (page: Page) => { 5 | await page.setRequestInterception(true); 6 | page.on('request', interceptedRequest => { 7 | if (interceptedRequest.url().endsWith('.png') || interceptedRequest.url().endsWith('.jpg')) 8 | interceptedRequest.abort() 9 | else 10 | interceptedRequest.continue() 11 | }) 12 | } 13 | } 14 | 15 | export default ConfigPage 16 | 17 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es2017", 5 | "noImplicitAny": true, 6 | "moduleResolution": "node", 7 | "esModuleInterop": true, 8 | "sourceMap": true, 9 | "outDir": "dist", 10 | "baseUrl": ".", 11 | "paths": { 12 | "*": [ 13 | "node_modules/*", 14 | "src/types/*" 15 | ] 16 | } 17 | }, 18 | "include": [ 19 | "src/**/*" 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /src/utils/login.ts: -------------------------------------------------------------------------------- 1 | import { Page } from 'puppeteer' 2 | 3 | export const login = async (page: Page, username: string, password: string) => { 4 | await page.goto(`http://i.chaoxing.com/vlogin?passWord=${password}&userName=${username}`); 5 | const bodyHandle = await page.$('body'); 6 | const loginRet = await page.evaluate(body => body.innerHTML, bodyHandle); 7 | await bodyHandle.dispose(); 8 | let result 9 | if (loginRet.includes('true')) { 10 | result = true 11 | } else { 12 | result = false 13 | } 14 | return result 15 | } 16 | 17 | -------------------------------------------------------------------------------- /src/app.ts: -------------------------------------------------------------------------------- 1 | import Koa from 'koa' 2 | import Router from 'koa-router' 3 | import bodyParser from 'koa-bodyparser' 4 | import puppeteer from 'puppeteer' 5 | import rootController from './controllers/rootController' 6 | 7 | 8 | type CourseIdItem = { 9 | name: string, 10 | id: string 11 | } 12 | 13 | 14 | const app = new Koa(); 15 | const router = new Router() 16 | 17 | router.post('/', async ctx => { 18 | await rootController(ctx) 19 | // await browser.close(); 20 | }); 21 | 22 | app 23 | .use(bodyParser()) 24 | .use(router.routes()) 25 | .use(router.allowedMethods()) 26 | 27 | app.listen(3000); 28 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "scripts": { 3 | "dev": "nodemon ./dist/app.js", 4 | "build": "tsc", 5 | "start": "pm2 start ./dist/app.js --name autoSign", 6 | "restart": "pm2 stop autoSign" 7 | }, 8 | "dependencies": { 9 | "@types/jest": "^25.1.4", 10 | "@types/koa": "^2.11.2", 11 | "@types/koa-bodyparser": "^4.3.0", 12 | "@types/koa-router": "^7.4.0", 13 | "@types/puppeteer": "^2.0.1", 14 | "axios": "^0.19.2", 15 | "jest": "^25.1.0", 16 | "koa": "^2.11.0", 17 | "koa-bodyparser": "^4.2.1", 18 | "koa-router": "^8.0.8", 19 | "nodemon": "^2.0.2", 20 | "pm2": "^4.2.3", 21 | "puppeteer": "^2.1.1", 22 | "ts-jest": "^25.2.1", 23 | "ts-node": "^8.6.2", 24 | "typescript": "^3.8.3" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ✌️ 超星学习通自动签到 2 | 3 | 整个自动签到,在家安稳睡觉。 4 | ## 技术栈 5 | 6 | 本项目使用 TypeScript Koa Puppeteer 实现。 7 | 8 | ## 开始 9 | 10 | ### 安装依赖 11 | 12 | > 本项目使用 `yarn` 进行依赖管理 13 | 14 | ```bash 15 | yarn install 16 | ``` 17 | 18 | ### 开发调试 19 | 20 | 因为 nodemon + ts-node 会有各种奇怪的问题。 21 | 22 | 目前本项目采用 tsc -w + nodemon ./dist 进行调试。 23 | 24 | 分别在两个 terminal 中执行 25 | ```bash 26 | tsc -w 27 | yarn dev 28 | ``` 29 | 30 | ### 使用方式 31 | 32 | url: localhost:3000 33 | path: '/' 34 | 35 | Request Headers 36 | 37 | Method: POST 38 | Content-Type: application/json 39 | 40 | Request Body 41 | ```json 42 | { 43 | "username": "13800001234", // 账号:请使用手机号。不要使用学号! 44 | "password": "password" // 你的密码 45 | } 46 | ``` 47 | 48 | ## 部署上线 49 | 50 | ```bash 51 | tsc 52 | yarn start 53 | ``` 54 | 55 | ## License 56 | 57 | MIT@Wzb3422 58 | -------------------------------------------------------------------------------- /src/utils/sign.ts: -------------------------------------------------------------------------------- 1 | import { Page, Browser } from 'puppeteer' 2 | import ConfigPage from './ConfigPage' 3 | 4 | export const signAll = async (browser: Browser, activeItem: ActiveItem[]) => { 5 | 6 | const signOne = async (activeItem: ActiveItem) => { 7 | const page = await browser.newPage() 8 | ConfigPage.noImageRequest(page) 9 | const { classId, activeId, courseId, title } = activeItem 10 | let arr = activeId.split(',') 11 | if (arr[1] === '2') { 12 | const signPage = await page.goto(`https://mobilelearn.chaoxing.com/widget/sign/pcStuSignController/preSign?activeId=${arr[0]}&classId=${classId}&courseId=${courseId}`) 13 | const signDivHandler = await page.$eval('.qd_Success .greenColor', el => el.textContent) 14 | console.log(`🐢 课程 - ${title} - ${signDivHandler}`) 15 | return { 16 | title, 17 | success: true 18 | } 19 | } 20 | return { 21 | title, 22 | success: false 23 | } 24 | } 25 | let retList= [] 26 | retList = await Promise.all(activeItem.map(async item => Promise.resolve(await signOne(item)))) 27 | return retList 28 | } 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Facebook, Inc. and its affiliates. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/utils/queryActiveTask.ts: -------------------------------------------------------------------------------- 1 | import { Browser } from 'puppeteer' 2 | import ConfigPage from './ConfigPage' 3 | 4 | 5 | 6 | export const queryActiveTask = async (browser: Browser, courseIDArray: CourseIDItem[]) => { 7 | const queryActive = async (courseIdItem: CourseIDItem): Promise => { 8 | const { classId, courseId, title } = courseIdItem 9 | const page = await browser.newPage() 10 | ConfigPage.noImageRequest(page) 11 | await page.goto(`https://mobilelearn.chaoxing.com/widget/pcpick/stu/index?courseId=${courseId}&jclassId=${classId}`) 12 | // endlist for test startList 13 | const activeListHandle = await page.$('#startList') 14 | const active = await activeListHandle.$$eval('.Mct', nodes => nodes.map(n => n.getAttribute('onclick'))) 15 | return { 16 | classId: classId, 17 | courseId: courseId, 18 | activeId: active[0], 19 | title: title 20 | } 21 | } 22 | 23 | let activeItemList: ActiveItem[] = [] 24 | activeItemList = await Promise.all(courseIDArray.map(async item => Promise.resolve(await queryActive(item)))) 25 | let activeSign: ActiveItem[] = activeItemList.filter(item => item.activeId !== undefined) 26 | activeSign.forEach((item, index) => { 27 | item.activeId = item.activeId.substr(13) 28 | item.activeId = item.activeId.substr(0, item.activeId.length - 1) 29 | }) 30 | return activeSign 31 | } 32 | -------------------------------------------------------------------------------- /src/utils/getCourseIds.ts: -------------------------------------------------------------------------------- 1 | import { Browser } from 'puppeteer' 2 | import ConfigPage from './ConfigPage' 3 | 4 | export const getCourseIds = async (browser: Browser) => { 5 | try { 6 | const page = await browser.newPage(); 7 | // 不请求图片 8 | ConfigPage.noImageRequest(page) 9 | await page.goto('http://mooc1-2.chaoxing.com/visit/interaction') 10 | const lisHandles = await page.$$('.ulDiv ul li') 11 | let result: CourseIDItem[] = [] 12 | let i = 0 13 | await new Promise(resolve => { 14 | lisHandles.forEach(async (handle, index) => { 15 | const oneCourseIdArr = await handle.$$eval('input', nodes => nodes.map(n => { 16 | const name = n.getAttribute('name') 17 | const id = n.getAttribute('value') 18 | return { 19 | [name]: id 20 | } 21 | })) 22 | const courseTitle = await handle.$$eval('.Mconright a', nodes => nodes.map(n => n.textContent)) 23 | if (oneCourseIdArr[0] && oneCourseIdArr[0].courseId) { 24 | result[i] = { 25 | courseId: oneCourseIdArr[0].courseId, 26 | classId: oneCourseIdArr[1].classId, 27 | title: courseTitle[0] 28 | } as CourseIDItem 29 | i++ 30 | } 31 | if (index === lisHandles.length - 1) { 32 | resolve('ok') 33 | } 34 | }) 35 | }) 36 | return { 37 | status: 0, 38 | message: 'courseID & classID 获取成功', 39 | data: result 40 | } 41 | } catch (error) { 42 | console.log(error) 43 | return { 44 | status: 1, 45 | message: error 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/controllers/rootController.ts: -------------------------------------------------------------------------------- 1 | import puppeteer from 'puppeteer' 2 | import { Context } from 'koa' 3 | import { login } from '../utils/login' 4 | import { getCourseIds } from '../utils/getCourseIds' 5 | import { queryActiveTask } from '../utils/queryActiveTask' 6 | import { signAll } from '../utils/sign' 7 | import { showResult } from '../utils/showResult' 8 | import ConfigPage from '../utils/ConfigPage' 9 | 10 | const rootController = async (ctx: Context) => { 11 | 12 | console.log('💡 接收到了请求') 13 | console.log('🐛 开始进行登录操作') 14 | const {username, password} = ctx.request.body 15 | 16 | const browser = await puppeteer.launch({ headless: true, args: ['--no-sandbox', '--disable-setuid-sandbox'] }) 17 | const page = await browser.newPage() 18 | // 不请求图片 19 | ConfigPage.noImageRequest(page) 20 | 21 | // 登录 22 | if (await login(page, username, password) === false) { 23 | ctx.body = { 24 | status: 0, 25 | message: '登录失败' 26 | } 27 | console.log('⚠️ 登录失败') 28 | return 29 | } 30 | 31 | console.log('🚪 登录成功') 32 | 33 | // 获取各种 ID 34 | console.log(`🔍 开始获取 classId courseId 等信息`) 35 | const courseIdResult = await getCourseIds(browser) 36 | let courseIDArray: CourseIDItem[] = [] 37 | if (courseIdResult.status) { 38 | ctx.body = { 39 | status: 1, 40 | message: `courseID 获取失败 Error: ${courseIdResult.message}` 41 | } 42 | return 43 | } else { 44 | courseIDArray = courseIdResult.data 45 | } 46 | console.log(`😯 classId courseId 等信息 获取成功`) 47 | console.log(`🤔 你共有 ${courseIDArray.length} 门课程`) 48 | 49 | console.log(`🔍 正在查看活动中的任务`) 50 | const actvieSignArray = await queryActiveTask(browser, courseIDArray) 51 | console.log(`📖 活动中的任务查询结束`) 52 | if (actvieSignArray.length === 0) { 53 | console.log('🐷 此时没有需要签到的课') 54 | ctx.response.body = '🐷 此时没有需要签到的课' 55 | return 56 | } 57 | console.log('✍️ 开始签到操作') 58 | const result = await signAll(browser, actvieSignArray) 59 | showResult(result) 60 | ctx.body = result 61 | await browser.close() 62 | 63 | } 64 | export default rootController 65 | --------------------------------------------------------------------------------