├── .env ├── .eslintrc.js ├── .gitignore ├── .prettierrc ├── README.md ├── chaogu.xlsx ├── nest-cli.json ├── package.json ├── qrcode.png ├── src ├── app.controller.ts ├── app.module.ts ├── excel │ ├── export-excel.service.ts │ └── test.controller.ts ├── main.ts ├── rushInJd │ ├── login.service.ts │ ├── product.service.ts │ ├── rushInJd.controller.ts │ ├── rushInJd.service.ts │ ├── rushInjd.module.ts │ └── task.service.ts ├── stock │ ├── gupiao.service.ts │ └── stock.module.ts ├── types.ts ├── ua.ts └── utils.ts ├── test.js ├── test ├── app.e2e-spec.ts └── jest-e2e.json ├── tsconfig.build.json ├── tsconfig.json └── yarn.lock /.env: -------------------------------------------------------------------------------- 1 | SEC=0 2 | MINUTE=44 3 | HOUR=17 4 | DAY=10 5 | MONTH=1 6 | PORT=3000 7 | PRODUCTID=100012354238 8 | BYNUM=1 9 | PAYPASSWORD=123456 10 | EID=QNLGMGBSBC5VT3RF5J5A6LGASN3NOVVK4CN3SZYW7FUTPI7UGQEIQWRIUP2TXAAUFJALWJECWU2XEFLRDFAQOWKBE4 11 | FP=901601812da514b462d7dae815c6648d 12 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: '@typescript-eslint/parser', 3 | parserOptions: { 4 | project: 'tsconfig.json', 5 | sourceType: 'module', 6 | }, 7 | plugins: ['@typescript-eslint/eslint-plugin'], 8 | extends: [ 9 | 'plugin:@typescript-eslint/eslint-recommended', 10 | 'plugin:@typescript-eslint/recommended', 11 | 'prettier', 12 | 'prettier/@typescript-eslint', 13 | ], 14 | root: true, 15 | env: { 16 | node: true, 17 | jest: true, 18 | }, 19 | rules: { 20 | '@typescript-eslint/interface-name-prefix': 'off', 21 | '@typescript-eslint/explicit-function-return-type': 'off', 22 | '@typescript-eslint/explicit-module-boundary-types': 'off', 23 | '@typescript-eslint/no-explicit-any': 'off', 24 | }, 25 | }; 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # compiled output 2 | /dist 3 | /node_modules 4 | 5 | # Logs 6 | logs 7 | *.log 8 | npm-debug.log* 9 | yarn-debug.log* 10 | yarn-error.log* 11 | lerna-debug.log* 12 | 13 | # OS 14 | .DS_Store 15 | 16 | # Tests 17 | /coverage 18 | /.nyc_output 19 | 20 | # IDEs and editors 21 | /.idea 22 | .project 23 | .classpath 24 | .c9/ 25 | *.launch 26 | .settings/ 27 | *.sublime-workspace 28 | 29 | # IDE - VSCode 30 | .vscode/* 31 | !.vscode/settings.json 32 | !.vscode/tasks.json 33 | !.vscode/launch.json 34 | !.vscode/extensions.json 35 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all" 4 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## 1. 重要声明 2 | - 本项目以学习为目的,本人不承担任何责任。 3 | - 本项目仅限学习本人使用,切勿泄露出去 4 | 5 | ## 2.使用说明 6 | ### 2.1 下载安装 7 | ```js 8 | git clone xx.git 9 | yarn install 10 | ``` 11 | 12 | ### 2.1 获取EID和FP 13 | - 在[京东](https://www.jd.com/)的网站添加任何商品,然后进入[结算页](https://trade.jd.com/shopping/order/getOrderInfo.action),在控制台输入`_JdTdudfp`就可以获取到一个对象,拷贝出来EID和FP 14 | ```js 15 | { 16 | eid: "QLKKDNCMZATGWEKG67UKWZEV56F7VOPUK74I6PSHKE242E4AU6TYLFOMXRTN6UPLBYTLPARIGDQ2D62NLZXVPMS6VKE", fp: "2e3ebc3e9bddb7f38990d390acfa9056d5", 17 | date: 16100704100000, 18 | token: "3BZ4QRLOU267HLGQW0QDYWCAZCKKQ8E3OVKYP7SGZTA24E5IKOPKCQZ4EGAWO6LKUDWQS53UKGKRMO26", 19 | jstub:"YKUXL6OHVZHZKLUYVBM0F2MY2LLTAZU7UWIOE634BPVIHZXY4OOE3…6BB2LBVGR4FXRBWT2ALBKGMIFGQDGQVIFNPJM66U3GWDGM5MI"} 20 | ``` 21 | 22 | ### 2.2 填写 .env 23 | 24 | ```js 25 | SEC=0 26 | MINUTE=0 27 | HOUR=10 28 | DAY=7 29 | MONTH=1 30 | PORT=3000 31 | PRODUCTID=100012043978 32 | BYNUM=1 33 | PAYPASSWORD=123456 34 | EID=QLKKDNCMZATGWEKG2UKWZEV56F7VOPUK74I6PSHKE242E4AU6TYLFOMXRTN6UPLBYTLPARIGDQ2D62NLZXVPMS6VKE 35 | FP=2e3ebc3e9bddb7f390d390acfa9056d5 36 | ``` 37 | 38 | SEC=0 39 | MINUTE=54 40 | HOUR=13 41 | DAY=6 42 | MONTH=1 43 | 44 | 这是脚本启动时间,会自动与京东对时。比如 7 点 0 分抢且比本地比京东时间快 30 秒,那么就会在本地时间 7 点 30 秒执行脚本 45 | 46 | PORT=3000 47 | 启动端口号 48 | 49 | PRODUCTID=100012043978 50 | 商品 id 51 | 52 | BYNUM=1 53 | 购买数量 54 | 55 | PAYPASSWORD=123456 56 | 支付密码 57 | 58 | EID='QLKKDNCMZATGWEKG2UKWZEV56F7VOPUK74I6PSHKE242E4AU6TYLFOMXRTN6UPLBYTLPARIGDQ2D62NLZXVPMS6VKE' 59 | FP='2e3ebc3e9bddb7f390d390acfa9056d5' 60 | 61 | ### 2.3 启动项目 62 | - 执行命令启动 63 | 64 | ```js 65 | yarn start 66 | ``` 67 | 68 | ### 2.3 登录 69 | - 启动后会弹出二维码,用京东APP扫码登录即可,如果卡住,按回车 70 | - 如果想重复登录,可以删除目录下面的qrcode.png,然后重新执行yarn start 71 | 72 | 73 | ### 2.4 update : 更新获取茅台股价信息 74 | - 可以将茅台股票导出成excel 75 | ```js 76 | import { StockModule } from './stock/stock.module'; // 获取股票信息模块 77 | ``` 78 | 79 | -------------------------------------------------------------------------------- /chaogu.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iceycc/tools_maotai/80b033dd8c0b475b5620d73669f0867375d69349/chaogu.xlsx -------------------------------------------------------------------------------- /nest-cli.json: -------------------------------------------------------------------------------- 1 | { 2 | "collection": "@nestjs/schematics", 3 | "sourceRoot": "src" 4 | } 5 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jd", 3 | "version": "0.0.1", 4 | "description": "", 5 | "author": "", 6 | "private": true, 7 | "license": "UNLICENSED", 8 | "scripts": { 9 | "prebuild": "rimraf dist", 10 | "build": "nest build", 11 | "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", 12 | "start": "nest start", 13 | "start:dev": "nest start --watch", 14 | "start:debug": "nest start --debug --watch", 15 | "start:prod": "node dist/main", 16 | "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", 17 | "test": "jest", 18 | "test:watch": "jest --watch", 19 | "test:cov": "jest --coverage", 20 | "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", 21 | "test:e2e": "jest --config ./test/jest-e2e.json" 22 | }, 23 | "dependencies": { 24 | "@nestjs/common": "^7.0.0", 25 | "@nestjs/core": "^7.0.0", 26 | "@nestjs/platform-express": "^7.0.0", 27 | "@nestjs/schedule": "^0.4.1", 28 | "@types/cron": "^1.7.2", 29 | "@types/fs-extra": "^9.0.6", 30 | "@types/shelljs": "^0.8.8", 31 | "cheerio": "^1.0.0-rc.5", 32 | "dotenv": "^8.2.0", 33 | "fs-extra": "^9.0.1", 34 | "moment": "^2.29.1", 35 | "node-xlsx": "^0.15.0", 36 | "qrcode-terminal": "^0.12.0", 37 | "reflect-metadata": "^0.1.13", 38 | "rimraf": "^3.0.2", 39 | "rxjs": "^6.5.4", 40 | "shelljs": "^0.8.4" 41 | }, 42 | "devDependencies": { 43 | "@nestjs/cli": "^7.0.0", 44 | "@nestjs/schematics": "^7.0.0", 45 | "@nestjs/testing": "^7.0.0", 46 | "@types/express": "^4.17.3", 47 | "@types/jest": "26.0.10", 48 | "@types/node": "^13.9.1", 49 | "@types/supertest": "^2.0.8", 50 | "@typescript-eslint/eslint-plugin": "3.9.1", 51 | "@typescript-eslint/parser": "3.9.1", 52 | "eslint": "7.7.0", 53 | "eslint-config-prettier": "^6.10.0", 54 | "eslint-plugin-import": "^2.20.1", 55 | "jest": "26.4.2", 56 | "prettier": "^1.19.1", 57 | "supertest": "^4.0.2", 58 | "ts-jest": "26.2.0", 59 | "ts-loader": "^6.2.1", 60 | "ts-node": "9.0.0", 61 | "tsconfig-paths": "^3.9.0", 62 | "typescript": "^3.7.4" 63 | }, 64 | "jest": { 65 | "moduleFileExtensions": [ 66 | "js", 67 | "json", 68 | "ts" 69 | ], 70 | "rootDir": "src", 71 | "testRegex": ".spec.ts$", 72 | "transform": { 73 | "^.+\\.(t|j)s$": "ts-jest" 74 | }, 75 | "coverageDirectory": "../coverage", 76 | "testEnvironment": "node" 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /qrcode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iceycc/tools_maotai/80b033dd8c0b475b5620d73669f0867375d69349/qrcode.png -------------------------------------------------------------------------------- /src/app.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Get } from '@nestjs/common'; 2 | 3 | @Controller() 4 | export class AppController { 5 | @Get() 6 | getHello(): string { 7 | return 'hello'; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/app.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { AppController } from './app.controller'; 3 | import { ScheduleModule } from '@nestjs/schedule'; 4 | import { ExportExcelService } from './excel/export-excel.service'; 5 | import { RushInJd } from './rushInJd/rushInjd.module';// 京东抢购模块 6 | import { StockModule } from './stock/stock.module'; // 获取股票信息模块 7 | 8 | @Module({ 9 | imports: [ScheduleModule.forRoot(), 10 | RushInJd, 11 | // StockModule, 12 | ], 13 | controllers: [AppController], 14 | providers: [ExportExcelService], 15 | }) 16 | export class AppModule { 17 | } 18 | -------------------------------------------------------------------------------- /src/excel/export-excel.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import xlsx from 'node-xlsx'; 3 | interface ObjectType { 4 | [key: string]:any 5 | } 6 | @Injectable() 7 | export class ExportExcelService { 8 | /** 9 | * 导出excel 10 | * @param titleList 标题 11 | * @param dataList 数据 12 | * @param xlsName sheet的名称 13 | */ 14 | public exportExcel(titleList: Array<{ label: string, value: string }>, dataList: ObjectType[], xlsName = 'sheet1'): ArrayBuffer { 15 | const data = []; // 其实最后就是把这个数组写入excel 16 | data.push(titleList.map(item => item.label)); // 添加完列名 下面就是添加真正的内容了 17 | dataList.forEach((element) => { 18 | const arrInner = []; 19 | for (let i = 0; i < titleList.length; i++) { 20 | arrInner.push(element[titleList[i].value]); 21 | } 22 | data.push(arrInner); // data中添加的要是数组,可以将对象的值分解添加进数组,例如:['1','name','上海'] 23 | }); 24 | const buffer = xlsx.build([ 25 | { 26 | name: xlsName, 27 | data 28 | } 29 | ]); 30 | return buffer; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/excel/test.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Get, Response } from '@nestjs/common'; 2 | 3 | import { Response as Res } from "express"; 4 | import { ExportExcelService } from './export-excel.service'; 5 | 6 | @Controller('') 7 | export class FileController { 8 | constructor ( 9 | private readonly exportExcelService: ExportExcelService, 10 | ) { } 11 | 12 | @Get('/excel') 13 | exportExcel( 14 | @Response() res: Res, 15 | ) { 16 | const titleList = [ 17 | { label: 'ID', value: 'id' }, 18 | { label: '姓名', value: 'name' }, 19 | { label: '地址', value: 'address' } 20 | ]; 21 | const dataList = [ 22 | { 23 | id: 1, 24 | name: '张三', 25 | address: '深圳', 26 | } 27 | ]; 28 | const result = this.exportExcelService.exportExcel(titleList, dataList); 29 | res.setHeader('Content-Type', 'application/vnd.openxmlformats;charset=utf-8'); 30 | res.setHeader('Content-Disposition', 'attachment; filename=' + encodeURIComponent("文件") + '.xlsx'); // 中文名需要进行url转码 31 | res.setTimeout(30 * 60 * 1000); // 防止网络原因造成超时。 32 | res.end(result, 'binary'); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { Logger } from '@nestjs/common'; 2 | import { NestFactory } from '@nestjs/core'; 3 | import { AppModule } from './app.module'; 4 | import 'dotenv/config'; 5 | const port = process.env.PORT; 6 | 7 | async function bootstrap() { 8 | const app = await NestFactory.create(AppModule); 9 | Logger.log(`start in ${port}`); 10 | await app.listen(port); 11 | } 12 | bootstrap(); 13 | -------------------------------------------------------------------------------- /src/rushInJd/login.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, Logger } from '@nestjs/common'; 2 | import axios from 'axios'; 3 | import 'dotenv/config'; 4 | import { defaultUa } from '../ua'; 5 | import * as fs from 'fs'; 6 | import { randomRange } from '../utils'; 7 | import * as shell from 'shelljs'; 8 | import * as path from 'path'; 9 | // import * as qrcode from 'qrcode-terminal'; 10 | 11 | const cookiePath = path.resolve(__dirname, '../', 'maotai.cookie'); 12 | const qrcodePath = path.resolve(__dirname, '../', 'qrcode.png'); 13 | console.log(__dirname); 14 | console.log(qrcodePath); 15 | 16 | interface CookieData { 17 | cookie: string[]; 18 | ua: string; 19 | } 20 | 21 | @Injectable() 22 | export class LoginService { 23 | private readonly logger = new Logger(LoginService.name); 24 | cookies: string[]; 25 | islogin: boolean; 26 | ua: string = defaultUa; 27 | 28 | /** 29 | * 30 | * cookie判断 31 | * @return {*} 32 | * @memberof LoginService 33 | */ 34 | async init() { 35 | const isExist = fs.existsSync(cookiePath); 36 | if (isExist) { 37 | const data = this.getCookieFromLocal(); 38 | this.cookies = data.cookie; 39 | } else { 40 | this.logger.log('未找到cookie文件'); 41 | } 42 | const sign = await this.validate(); 43 | if (!sign) { 44 | // 验证失败不是过期就是未登录。都需要清理cookie 45 | this.cookies = []; 46 | const fn = async () => { 47 | return new Promise(async resolve => { 48 | const s = await this.main(); 49 | if (!s) { 50 | setTimeout(() => { 51 | fn(); 52 | }, 2000); 53 | } else { 54 | resolve(''); 55 | } 56 | }); 57 | }; 58 | await fn(); 59 | } 60 | this.logger.log('已在登录状态'); 61 | return; 62 | } 63 | 64 | /** 65 | * 66 | * 存储到本地 67 | * @return {*} 68 | * @memberof LoginService 69 | */ 70 | storeCookieTolocal() { 71 | const writeData: CookieData = { 72 | cookie: this.cookies, 73 | ua: this.ua, 74 | }; 75 | fs.writeFileSync(cookiePath, JSON.stringify(writeData)); 76 | return; 77 | } 78 | 79 | /** 80 | * 81 | * 从本地获取cookie 82 | * @return {*} 83 | * @memberof LoginService 84 | */ 85 | getCookieFromLocal() { 86 | const cookie = fs.readFileSync(cookiePath).toString(); 87 | const data: CookieData = JSON.parse(cookie); 88 | return data; 89 | } 90 | 91 | /** 92 | * 93 | * 处理cookie 94 | * @param {string[]} cookie 95 | * @returns 96 | * @memberof LoginService 97 | */ 98 | cookieResolve(cookie: string[]) { 99 | if (Array.isArray(cookie)) { 100 | return cookie.reduce((prev, next) => { 101 | const ls = next.split(';'); 102 | prev.push(ls[0]); 103 | return prev; 104 | }, []); 105 | } else { 106 | return []; 107 | } 108 | } 109 | 110 | /** 111 | * 112 | * 处理请求头的cookie 113 | * @param {*} headers 114 | * @memberof LoginService 115 | */ 116 | cookieStore(headers) { 117 | const cookie = headers['set-cookie']; 118 | const rescookie = this.cookieResolve(cookie); 119 | Array.isArray(this.cookies) 120 | ? (this.cookies = this.cookies.concat(rescookie)) 121 | : (this.cookies = rescookie); 122 | } 123 | 124 | /** 125 | * 126 | * 将cookie给到请求头 127 | * @return {*} 128 | * @memberof LoginService 129 | */ 130 | cookieToHeader() { 131 | if (this.cookies && this.cookies.length > 0) { 132 | return this.cookies.join('; '); 133 | } 134 | return ''; 135 | } 136 | 137 | /** 138 | * 139 | * 返回请求头 140 | * @returns 141 | * @memberof LoginService 142 | */ 143 | getHeaders() { 144 | return { 145 | 'User-Agent': this.ua, 146 | Accept: 147 | 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9', 148 | Connection: 'keep-alive', 149 | }; 150 | } 151 | 152 | /** 153 | * 154 | * 主登录流程 155 | * @memberof LoginService 156 | */ 157 | async main() { 158 | this.logger.log('进行登录流程'); 159 | const res = await axios.get('https://passport.jd.com/new/login.aspx', { 160 | headers: this.getHeaders(), 161 | }); 162 | this.cookieStore(res.headers); 163 | const sign = await this.getQrcode(); 164 | if (!sign) { 165 | this.logger.log('未获取到登录二维码'); 166 | return false; 167 | } 168 | this.logger.log(`二维码文件位于${qrcodePath}`); 169 | this.openQrcode(); 170 | const ticket = await this.qrcodeScan(); 171 | const result = await this.validateTicket(ticket); 172 | if (!result) { 173 | this.logger.log('登录票据有误'); 174 | return false; 175 | } 176 | return await this.validate(); 177 | } 178 | 179 | async openQrcode() { 180 | shell.exec(`open ${qrcodePath}`); 181 | // qrcode.generate(`open ${qrcodePath}`, { 182 | // small: true, 183 | // }); 184 | return; 185 | } 186 | 187 | /** 188 | * 189 | * 获取qrcode 190 | * @returns 191 | * @memberof LoginService 192 | */ 193 | async getQrcode() { 194 | const url = 'https://qr.m.jd.com/show'; 195 | const cookie = this.cookieToHeader(); 196 | return new Promise(resolve => { 197 | axios 198 | .get(url, { 199 | headers: { 200 | 'User-Agent': this.ua, 201 | Referer: 'https://passport.jd.com/new/login.aspx', 202 | cookie, 203 | }, 204 | responseType: 'arraybuffer', 205 | params: { 206 | appid: 133, 207 | size: 147, 208 | t: Date.now(), 209 | }, 210 | }) 211 | .then(res => { 212 | if (res.status === 200) { 213 | this.cookieStore(res.headers); 214 | // qrcode.generate(res.data, { 215 | // small: true, 216 | // }); 217 | fs.writeFile(qrcodePath, res.data, err => { 218 | if (err) throw err; 219 | // qrcode.generate(res.data, { 220 | // small: true, 221 | // }) 222 | resolve(true); 223 | }); 224 | } 225 | }) 226 | .catch(e => { 227 | this.logger.log(e); 228 | resolve(false); 229 | }); 230 | }); 231 | } 232 | 233 | /** 234 | * 235 | * 用户扫描等待逻辑 236 | * @memberof LoginService 237 | */ 238 | async qrcodeScan() { 239 | const reg = /(?<=wlfstk_smdl=)(.*)/; 240 | let stk: string; 241 | this.cookies.some(v => { 242 | const res = reg.exec(v); 243 | if (res) { 244 | stk = res[0]; 245 | return true; 246 | } 247 | return false; 248 | }); 249 | const randomInt = randomRange(1000000, 9999999); 250 | const params = { 251 | callback: `jQuery${randomInt}`, 252 | appid: '133', 253 | token: stk, 254 | _: Date.now(), 255 | }; 256 | const headers = { 257 | 'User-Agent': this.ua, 258 | Referer: 'https://passport.jd.com/new/login.aspx', 259 | cookie: this.cookieToHeader(), 260 | }; 261 | const msgReg = /(?<="msg" : ")(.+)(?=")/; 262 | const codeReg = /(?<="code" : )(.+)(?=,)/; 263 | const ticketReg = /(?<="ticket" : ")(.+)(?=")/; 264 | return new Promise(resolve => { 265 | const fn = () => { 266 | axios 267 | .get('https://qr.m.jd.com/check', { 268 | headers, 269 | params, 270 | }) 271 | .then(res => { 272 | const code = codeReg.exec(res.data)[0]; 273 | // 201 未扫描 203过期 202确认登录中 200给你个ticket 274 | if (code !== '200') { 275 | const msg = msgReg.exec(res.data)[0]; 276 | this.logger.log(msg); 277 | if (code !== '203') { 278 | setTimeout(() => { 279 | fn(); 280 | }, 2000); 281 | } else { 282 | resolve(''); 283 | } 284 | } else { 285 | const ticket = ticketReg.exec(res.data)[0]; 286 | resolve(ticket); 287 | } 288 | }) 289 | .catch(e => { 290 | console.log(e); 291 | this.logger.log('京东更新了登录逻辑,请联系作者yehuozhili'); 292 | }); 293 | }; 294 | fn(); 295 | }); 296 | } 297 | 298 | /** 299 | * 300 | * 验证ticket 301 | * @param {string} ticket 302 | * @returns 303 | * @memberof LoginService 304 | */ 305 | async validateTicket(ticket: string) { 306 | const url = 'https://passport.jd.com/uc/qrCodeTicketValidation'; 307 | const headers = { 308 | 'User-Agent': this.ua, 309 | Referer: 'https://passport.jd.com/uc/login?ltype=logout', 310 | cookie: this.cookieToHeader(), 311 | }; 312 | const res = await axios.get(url, { 313 | headers, 314 | params: { 315 | t: ticket, 316 | }, 317 | }); 318 | if (res.data.returnCode === 0) { 319 | this.cookieStore(res.headers); 320 | return true; 321 | } 322 | return false; 323 | } 324 | 325 | /** 326 | * 327 | * 验证是否能登录 328 | * @return {*} 329 | * @memberof LoginService 330 | */ 331 | async validate() { 332 | const url = 'https://order.jd.com/center/list.action'; 333 | try { 334 | await axios.get(url, { 335 | headers: { 336 | 'User-Agent': this.ua, 337 | cookie: this.cookieToHeader(), 338 | }, 339 | params: { 340 | rid: Date.now(), 341 | }, 342 | maxRedirects: 0, 343 | }); 344 | this.storeCookieTolocal(); 345 | this.logger.log('验证成功'); 346 | this.islogin = true; 347 | return true; 348 | } catch { 349 | this.logger.log('验证失败'); 350 | this.islogin = false; 351 | return false; 352 | } 353 | } 354 | } 355 | -------------------------------------------------------------------------------- /src/rushInJd/product.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, Logger } from '@nestjs/common'; 2 | import 'dotenv/config'; 3 | import { LoginService } from './login.service'; 4 | import axios from 'axios'; 5 | import * as cheerio from 'cheerio'; 6 | import { randomRange } from '../utils'; 7 | 8 | const productId = process.env.PRODUCTID; 9 | const byNum = process.env.BYNUM; 10 | const password = process.env.PAYPASSWORD; 11 | const eid = process.env.EID; 12 | const fp = process.env.FP; 13 | @Injectable() 14 | export class ProductService { 15 | private readonly logger = new Logger(ProductService.name); 16 | constructor(private readonly loginService: LoginService) {} 17 | 18 | /** 19 | * 20 | * 访问商品获取名称拿cookie 21 | * @returns 22 | * @memberof ProductService 23 | */ 24 | async getProduct() { 25 | const url = `https://item.jd.com/${productId}.html`; 26 | const res = await axios.get(url, { 27 | headers: { 28 | 'User-Agent': this.loginService.ua, 29 | cookie: this.loginService.cookieToHeader(), 30 | }, 31 | }); 32 | this.loginService.cookieStore(res.headers); 33 | const $ = cheerio.load(res.data); 34 | const title = $('title').text(); 35 | this.logger.log(`您设定的抢购商品为:${title}`); 36 | return; 37 | } 38 | 39 | /** 40 | * 41 | * 获取抢购地址 42 | * @returns 43 | * @memberof ProductService 44 | */ 45 | async getSeckillUrl() { 46 | const url = 'https://itemko.jd.com/itemShowBtn'; 47 | const params = { 48 | callback: `jQuery${randomRange(1000000, 9999999)}`, 49 | skuId: productId, 50 | from: 'pc', 51 | _: Date.now(), 52 | }; 53 | const headers = { 54 | 'User-Agent': this.loginService.ua, 55 | Host: 'itemko.jd.com', 56 | Referer: `https://item.jd.com/${productId}.html`, 57 | cookie: this.loginService.cookieToHeader(), 58 | }; 59 | const reg = /(?<="url":")(.*)(?=")/; 60 | return new Promise(resolve => { 61 | const fn = () => { 62 | axios 63 | .get(url, { 64 | params, 65 | headers, 66 | }) 67 | .then(res => { 68 | const path = reg.exec(res.data)[0]; 69 | console.log(res.data); 70 | if (path === '') { 71 | this.logger.error( 72 | '获取抢购地址失败,请检查是否有权限或者是否处于时间内,2秒后重试', 73 | ); 74 | setTimeout(() => { 75 | fn(); 76 | }, 2000); 77 | } else { 78 | this.loginService.cookieStore(res.headers); 79 | this.logger.log(`获取抢购原始地址${path}`); 80 | resolve(path); 81 | } 82 | }) 83 | .catch(e => { 84 | console.log(e); 85 | this.logger.log('京东更新了登录逻辑,请联系作者yehuozhili'); 86 | }); 87 | }; 88 | fn(); 89 | }); 90 | } 91 | 92 | /** 93 | * 94 | * 处理路径转换 95 | * @param {string} path 96 | * @returns 97 | * @memberof ProductService 98 | */ 99 | resolvePath(path: string) { 100 | const full = 'https:' + path; 101 | return full 102 | .replace('divide', 'marathon') 103 | .replace('user_routing', 'captcha.html'); 104 | } 105 | 106 | /** 107 | * 108 | * 访问转换后链接 109 | * @param {string} path 110 | * @returns 111 | * @memberof ProductService 112 | */ 113 | async goToKillUrl(path: string) { 114 | this.logger.log('正在访问生成的链接'); 115 | const headers = { 116 | 'User-Agent': this.loginService.ua, 117 | Host: 'marathon.jd.com', 118 | Referer: `https://item.jd.com/${productId}.html`, 119 | cookie: this.loginService.cookieToHeader(), 120 | }; 121 | return await axios.get(path, { 122 | headers, 123 | maxRedirects: 0, 124 | }); 125 | } 126 | 127 | /** 128 | * 129 | * 订单结算页 130 | * @returns 131 | * @memberof ProductService 132 | */ 133 | async toCheckOut() { 134 | this.logger.log('正在访问订单结算页面'); 135 | const url = 'https://marathon.jd.com/seckill/seckill.action'; 136 | const params = { 137 | skuId: productId, 138 | num: byNum, 139 | rid: Date.now(), 140 | }; 141 | const headers = { 142 | 'User-Agent': this.loginService.ua, 143 | Host: 'marathon.jd.com', 144 | Referer: `https://item.jd.com/${productId}.html`, 145 | cookie: this.loginService.cookieToHeader(), 146 | }; 147 | return await axios.get(url, { 148 | headers, 149 | params, 150 | maxRedirects: 0, 151 | }); 152 | } 153 | 154 | /** 155 | * 156 | * 提交订单 157 | * @param {*} jsondata 158 | * @returns 159 | * @memberof ProductService 160 | */ 161 | async submitOrder(jsondata) { 162 | this.logger.log('正在提交订单'); 163 | const defaultAddress = jsondata['addressList'][0]; 164 | const invoice = jsondata['invoiceInfo'] || {}; 165 | const token = jsondata['token']; 166 | const data = { 167 | skuId: productId, 168 | num: byNum, 169 | addressId: defaultAddress['id'], 170 | yuShou: 'true', 171 | isModifyAddress: 'false', 172 | name: defaultAddress['name'], 173 | provinceId: defaultAddress['provinceId'], 174 | cityId: defaultAddress['cityId'], 175 | countyId: defaultAddress['countyId'], 176 | townId: defaultAddress['townId'], 177 | addressDetail: defaultAddress['addressDetail'], 178 | mobile: defaultAddress['mobile'], 179 | mobileKey: defaultAddress['mobileKey'], 180 | email: defaultAddress['email'] || '', 181 | postCode: '', 182 | invoiceTitle: invoice['invoiceTitle'] || -1, 183 | invoiceCompanyName: '', 184 | invoiceContent: invoice['invoiceContentType'] || 1, 185 | invoiceTaxpayerNO: '', 186 | invoiceEmail: '', 187 | invoicePhone: invoice['invoicePhone'] || '', 188 | invoicePhoneKey: invoice['invoicePhoneKey'] || '', 189 | invoice: invoice ? 'true' : 'false', 190 | password: password, 191 | codTimeType: 3, 192 | paymentType: 4, 193 | areaCode: '', 194 | overseas: 0, 195 | phone: '', 196 | eid: eid, 197 | fp: fp, 198 | token: token, 199 | pru: '', 200 | }; 201 | this.logger.log('提交抢购订单中'); 202 | const url = 203 | 'https://marathon.jd.com/seckillnew/orderService/pc/submitOrder.action'; 204 | const params = { 205 | skuId: productId, 206 | }; 207 | const headers = { 208 | 'User-Agent': this.loginService.ua, 209 | Host: 'marathon.jd.com', 210 | Referer: `https://marathon.jd.com/seckill/seckill.action?skuId=${productId}&num=${byNum}&rid=${Date.now()}`, 211 | cookie: this.loginService.cookieToHeader(), 212 | }; 213 | return await axios.post(url, data, { 214 | params, 215 | headers, 216 | }); 217 | } 218 | 219 | /** 220 | * 221 | * 获取地址信息 222 | * @returns 223 | * @memberof ProductService 224 | */ 225 | async killInfo() { 226 | this.logger.log('正在获取地址发票等信息'); 227 | const url = 228 | 'https://marathon.jd.com/seckillnew/orderService/pc/init.action'; 229 | const data = { 230 | sku: productId, 231 | num: byNum, 232 | isModifyAddress: 'false', 233 | }; 234 | const headers = { 235 | 'User-Agent': this.loginService.ua, 236 | Host: 'marathon.jd.com', 237 | cookie: this.loginService.cookieToHeader(), 238 | }; 239 | return await axios.post(url, data, { 240 | headers, 241 | }); 242 | } 243 | } 244 | -------------------------------------------------------------------------------- /src/rushInJd/rushInJd.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Get } from '@nestjs/common'; 2 | import { RushInJdService } from './rushInJd.service'; 3 | 4 | @Controller() 5 | export class RushInJdController { 6 | constructor(private readonly appService: RushInJdService) {} 7 | 8 | @Get() 9 | getHello(): string { 10 | return 'hello'; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/rushInJd/rushInJd.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, Logger } from '@nestjs/common'; 2 | import { LoginService } from './login.service'; 3 | import { ProductService } from './product.service'; 4 | 5 | @Injectable() 6 | export class RushInJdService { 7 | private readonly logger = new Logger(RushInJdService.name); 8 | constructor( 9 | private readonly loginService: LoginService, 10 | private readonly productService: ProductService, 11 | ) {} 12 | 13 | /** 14 | * 15 | * 主流程 16 | * @memberof AppService 17 | */ 18 | async main() { 19 | await this.loginService.init(); 20 | await this.productService.getProduct(); 21 | const path = await this.productService.getSeckillUrl(); 22 | console.log(path, 'path'); 23 | const newpath = this.productService.resolvePath(path); 24 | this.logger.log(`抢购链接为${newpath}`); 25 | 26 | //有可能进行跳转 原Python这里是发生302直接重复执行,也可以放开redirect看下到底返回了啥 27 | const gotokill = () => { 28 | return new Promise(resolve => { 29 | const fn = () => { 30 | this.productService 31 | .goToKillUrl(newpath) 32 | .then(res => { 33 | console.log(res.data, 'newpath数据'); 34 | this.loginService.cookieStore(res.headers); 35 | resolve(''); 36 | }) 37 | .catch(e => { 38 | console.log(e); 39 | this.logger.error('发生跳转,1秒后重试'); 40 | setTimeout(() => { 41 | fn(); 42 | }, 1000); 43 | }); 44 | }; 45 | fn(); 46 | }); 47 | }; 48 | await gotokill(); 49 | const checkout = () => { 50 | return new Promise(resolve => { 51 | const fn = () => 52 | this.productService 53 | .toCheckOut() 54 | .then(checkRes => { 55 | console.log(checkRes.data, 'checkres结果'); 56 | this.loginService.cookieStore(checkRes.headers); 57 | resolve(''); 58 | }) 59 | .catch(e => { 60 | console.log(e); 61 | this.logger.error('发生跳转,1秒后重试'); 62 | setTimeout(() => { 63 | fn(); 64 | }, 1000); 65 | }); 66 | fn(); 67 | }); 68 | }; 69 | await checkout(); 70 | const info = await this.productService.killInfo(); 71 | console.log(info.data, 'info信息'); 72 | const jsondata = JSON.parse(info.data); 73 | this.loginService.cookieStore(info.headers); 74 | const final = await this.productService.submitOrder(jsondata); 75 | console.log(final.data, '抢购结果'); 76 | const result = JSON.parse(final.data); 77 | if (result.success) { 78 | this.logger.log(`抢购成功,电脑付款链接:https:${result.pcUrl}`); 79 | } 80 | return; 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/rushInJd/rushInjd.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { RushInJdController } from './rushInJd.controller'; 3 | import { RushInJdService } from './rushInJd.service'; 4 | import { TasksService } from './task.service'; 5 | import { LoginService } from './login.service'; 6 | import { ProductService } from './product.service'; 7 | import { ExportExcelService } from '../excel/export-excel.service'; 8 | 9 | @Module({ 10 | imports: [], 11 | controllers: [RushInJdController], 12 | providers: [RushInJdService, TasksService, LoginService, ProductService, ExportExcelService], 13 | }) 14 | export class RushInJd { 15 | } 16 | -------------------------------------------------------------------------------- /src/rushInJd/task.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, Logger } from '@nestjs/common'; 2 | import { Cron, SchedulerRegistry } from '@nestjs/schedule'; 3 | import { CronJob } from 'cron'; 4 | import axios from 'axios'; 5 | import * as moment from 'moment'; 6 | 7 | import 'dotenv/config'; 8 | import { LoginService } from './login.service'; 9 | import { RushInJdService } from './rushInJd.service'; 10 | 11 | const env = process.env; 12 | 13 | interface IJdServTimeRes { 14 | serverTime: number; 15 | } 16 | 17 | @Injectable() 18 | export class TasksService { 19 | private readonly logger = new Logger(TasksService.name); 20 | constructor( 21 | private schedulerRegistry: SchedulerRegistry, 22 | private readonly loginService: LoginService, 23 | private readonly appSrv: RushInJdService, 24 | ) { 25 | this.handleTimeDiff(); 26 | } 27 | 28 | /** 29 | * 30 | * 处理时差 添加任务 31 | * @memberof TasksService 32 | */ 33 | async handleTimeDiff() { 34 | const url = 'https://a.jd.com//ajax/queryServerData.html'; 35 | const res = await axios.get(url); // 获得的是时间戳 36 | const now = Date.now(); 37 | const diff = (now - res.data.serverTime) / 1000; 38 | const differtime = moment.duration(Math.abs(diff), 'seconds'); 39 | const origintime = moment( 40 | `${env.SEC}-${env.MINUTE}-${env.HOUR}-${env.DAY}-${env.MONTH}`, 41 | 's-m-H-D-M', 42 | ); 43 | this.logger.warn(`您设定时间为${origintime.format('M月D日H点m分s秒')}`); 44 | let fixStart: moment.Moment; 45 | if (diff > 0) { 46 | this.logger.warn(`您电脑时间比京东快${diff}秒`); 47 | fixStart = origintime.add(differtime); 48 | } else { 49 | this.logger.warn(`您的电脑时间比京东慢${-diff}秒`); 50 | fixStart = origintime.subtract(differtime); 51 | } 52 | this.logger.warn(`已修正启动时间为${fixStart.format('M月D日H点m分s秒')}`); 53 | const month = fixStart.get('month'); 54 | const fixCornTime = fixStart.format('s m H D ') + month + ' *'; 55 | this.addCronJob('user', fixCornTime); 56 | } 57 | 58 | /** 59 | * 60 | * 每小时输出一次,方便观察 61 | * @memberof TasksService 62 | */ 63 | @Cron('0 0 * * * *') 64 | displayServerTime() { 65 | this.logger.debug(`${moment().format('YYYY年MM月DD日 HH时mm分ss秒')}`); 66 | } 67 | 68 | /** 69 | * 70 | * 添加修正时间后的任务 71 | * @param {string} name 72 | * @param {string} coreTime 73 | * @memberof TasksService 74 | */ 75 | async addCronJob(name: string, coreTime: string) { 76 | const job = new CronJob(coreTime, () => { 77 | this.logger.log(`执行脚本启动`); 78 | this.appSrv.main(); 79 | }); 80 | 81 | this.schedulerRegistry.addCronJob(name, job); 82 | job.start(); 83 | this.logger.log(`任务coretime为${coreTime}`); 84 | // 如果要提前登录就放开 85 | this.logger.log('检查登录情况'); 86 | this.loginService.init(); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/stock/gupiao.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, Logger } from '@nestjs/common'; 2 | import axios from 'axios'; 3 | import { defaultUa } from '../ua'; 4 | import * as cheerio from 'cheerio'; 5 | import { ExportExcelService } from '../excel/export-excel.service'; 6 | import * as fs from 'fs'; 7 | import * as Path from 'path'; 8 | 9 | const excelPath = Path.resolve(__dirname, '..', 'chaogu.xlsx'); 10 | 11 | @Injectable() 12 | export class GupiaoSevice { 13 | private readonly logger = new Logger(GupiaoSevice.name); 14 | ua: string = defaultUa; 15 | 16 | constructor(private readonly excelService: ExportExcelService) { 17 | this.getHtml(); 18 | } 19 | 20 | async getHtml() { 21 | this.logger.debug('获取股票信息开始'); 22 | const url = 'http://quotes.money.163.com/f10/zycwzb_600519.html#01c01'; 23 | const res = await axios.get(url, { 24 | headers: { 25 | 'User-Agent': this.ua, 26 | }, 27 | }); 28 | const $ = cheerio.load(res.data); 29 | const title = $('title').text(); 30 | this.logger.log(`您设定的股票为:${title}`); 31 | const tablesKey = $('.limit_sale').first(); 32 | const tables = $('.scr_table').find('tr'); 33 | const keys = ['']; 34 | const titleList: { label: string, value: any }[] = []; 35 | const dataList: any[] = []; 36 | tablesKey.find('td').each(function() { 37 | keys.push($(this).text()); 38 | }); 39 | tables.find('th').each(function(__) { 40 | const th = $(this).text(); 41 | titleList.push({ 42 | label: th, 43 | value: th, 44 | }); 45 | }); 46 | // console.log(JSON.stringify(titleList)) 47 | tables.each(function(i) { 48 | const data = {}; 49 | data['date'] = keys[i]; 50 | $(this).find('td').each(function(i) { 51 | data[titleList[i].value] = $(this).text(); 52 | }); 53 | dataList.push(data); 54 | }); 55 | titleList.unshift({ 56 | label: '报告日期', 57 | value: 'date', 58 | }); 59 | 60 | dataList.shift(); 61 | const excel = await this.excelService.exportExcel(titleList, dataList); 62 | const isExist = fs.existsSync(excelPath); 63 | if (isExist) fs.unlinkSync(excelPath); 64 | fs.writeFileSync(excelPath, excel); 65 | this.logger.debug('获取股票信息结束结束' + excelPath); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/stock/stock.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { GupiaoSevice } from './gupiao.service'; 3 | import { ExportExcelService } from '../excel/export-excel.service'; 4 | 5 | @Module({ 6 | controllers: [], 7 | providers: [GupiaoSevice, ExportExcelService], 8 | }) 9 | export class StockModule { 10 | } 11 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | export interface ObjectType { 2 | [keyName: string]: any 3 | } 4 | -------------------------------------------------------------------------------- /src/ua.ts: -------------------------------------------------------------------------------- 1 | export const UA = [ 2 | 'Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2228.0 Safari/537.36', 3 | 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2227.1 Safari/537.36', 4 | 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2227.0 Safari/537.36', 5 | 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2227.0 Safari/537.36', 6 | 'Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2226.0 Safari/537.36', 7 | 'Mozilla/5.0 (Windows NT 6.4; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2225.0 Safari/537.36', 8 | 'Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2225.0 Safari/537.36', 9 | 'Mozilla/5.0 (Windows NT 5.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2224.3 Safari/537.36', 10 | 'Mozilla/5.0 (Windows NT 10.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/40.0.2214.93 Safari/537.36', 11 | 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/37.0.2062.124 Safari/537.36', 12 | 'Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/37.0.2049.0 Safari/537.36', 13 | 'Mozilla/5.0 (Windows NT 4.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/37.0.2049.0 Safari/537.36', 14 | 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/36.0.1985.67 Safari/537.36', 15 | 'Mozilla/5.0 (Windows NT 5.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/36.0.1985.67 Safari/537.36', 16 | 'Mozilla/5.0 (X11; OpenBSD i386) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/36.0.1985.125 Safari/537.36', 17 | 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/36.0.1944.0 Safari/537.36', 18 | 'Mozilla/5.0 (Windows NT 5.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/35.0.3319.102 Safari/537.36', 19 | 'Mozilla/5.0 (Windows NT 5.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/35.0.2309.372 Safari/537.36', 20 | 'Mozilla/5.0 (Windows NT 5.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/35.0.2117.157 Safari/537.36', 21 | 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/35.0.1916.47 Safari/537.36', 22 | 'Mozilla/5.0 (Windows NT 5.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/34.0.1866.237 Safari/537.36', 23 | 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/34.0.1847.137 Safari/4E423F', 24 | 'Mozilla/5.0 (Windows NT 5.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/34.0.1847.116 Safari/537.36 Mozilla/5.0 (iPad; U; CPU OS 3_2 like Mac OS X; en-us) AppleWebKit/531.21.10 (KHTML, like Gecko) Version/4.0.4 Mobile/7B334b Safari/531.21.10', 25 | 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/33.0.1750.517 Safari/537.36', 26 | 'Mozilla/5.0 (Windows NT 6.2; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/32.0.1667.0 Safari/537.36', 27 | 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/32.0.1664.3 Safari/537.36', 28 | 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/32.0.1664.3 Safari/537.36', 29 | 'Mozilla/5.0 (Windows NT 5.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/31.0.1650.16 Safari/537.36', 30 | 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/31.0.1623.0 Safari/537.36', 31 | 'Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/30.0.1599.17 Safari/537.36', 32 | 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/29.0.1547.62 Safari/537.36', 33 | 'Mozilla/5.0 (X11; CrOS i686 4319.74.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/29.0.1547.57 Safari/537.36', 34 | 'Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/29.0.1547.2 Safari/537.36', 35 | 'Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/28.0.1468.0 Safari/537.36', 36 | 'Mozilla/5.0 (Windows NT 6.2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/28.0.1467.0 Safari/537.36', 37 | 'Mozilla/5.0 (Windows NT 6.2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/28.0.1464.0 Safari/537.36', 38 | 'Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/27.0.1500.55 Safari/537.36', 39 | 'Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/27.0.1453.93 Safari/537.36', 40 | 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/27.0.1453.93 Safari/537.36', 41 | 'Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/27.0.1453.93 Safari/537.36', 42 | 'Mozilla/5.0 (Windows NT 5.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/27.0.1453.93 Safari/537.36', 43 | 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/27.0.1453.93 Safari/537.36', 44 | 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/27.0.1453.93 Safari/537.36', 45 | 'Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/27.0.1453.90 Safari/537.36', 46 | 'Mozilla/5.0 (X11; NetBSD) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/27.0.1453.116 Safari/537.36', 47 | 'Mozilla/5.0 (X11; CrOS i686 3912.101.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/27.0.1453.116 Safari/537.36', 48 | 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.17 (KHTML, like Gecko) Chrome/24.0.1312.60 Safari/537.17', 49 | 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_2) AppleWebKit/537.17 (KHTML, like Gecko) Chrome/24.0.1309.0 Safari/537.17', 50 | 'Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/537.15 (KHTML, like Gecko) Chrome/24.0.1295.0 Safari/537.15', 51 | 'Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/537.14 (KHTML, like Gecko) Chrome/24.0.1292.0 Safari/537.14', 52 | ]; 53 | export const defaultUa = 54 | 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36'; 55 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | import { UA } from './ua'; 2 | 3 | export function randomRange(min: number, max: number) { 4 | return Math.floor(Math.random() * (max - min)) + min; 5 | } 6 | 7 | export function randomUA() { 8 | return UA[randomRange(0, UA.length)]; 9 | } 10 | 11 | export function sleep(delay: number) { 12 | return new Promise(res => { 13 | setTimeout(() => { 14 | res(true); 15 | }, delay); 16 | }); 17 | } 18 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iceycc/tools_maotai/80b033dd8c0b475b5620d73669f0867375d69349/test.js -------------------------------------------------------------------------------- /test/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { INestApplication } from '@nestjs/common'; 3 | import * as request from 'supertest'; 4 | import { AppModule } from './../src/app.module'; 5 | 6 | describe('AppController (e2e)', () => { 7 | let app: INestApplication; 8 | 9 | beforeEach(async () => { 10 | const moduleFixture: TestingModule = await Test.createTestingModule({ 11 | imports: [AppModule], 12 | }).compile(); 13 | 14 | app = moduleFixture.createNestApplication(); 15 | await app.init(); 16 | }); 17 | 18 | it('/ (GET)', () => { 19 | return request(app.getHttpServer()) 20 | .get('/') 21 | .expect(200) 22 | .expect('Hello World!'); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /test/jest-e2e.json: -------------------------------------------------------------------------------- 1 | { 2 | "moduleFileExtensions": ["js", "json", "ts"], 3 | "rootDir": ".", 4 | "testEnvironment": "node", 5 | "testRegex": ".e2e-spec.ts$", 6 | "transform": { 7 | "^.+\\.(t|j)s$": "ts-jest" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": ["node_modules", "test", "dist", "**/*spec.ts"] 4 | } 5 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "declaration": true, 5 | "removeComments": true, 6 | "emitDecoratorMetadata": true, 7 | "experimentalDecorators": true, 8 | "allowSyntheticDefaultImports": true, 9 | "target": "es2017", 10 | "sourceMap": true, 11 | "outDir": "./dist", 12 | "baseUrl": "./", 13 | "incremental": true 14 | } 15 | } 16 | --------------------------------------------------------------------------------