This commit is contained in:
colter 2023-12-30 12:49:04 +08:00
commit 247c87a16b
21 changed files with 9424 additions and 0 deletions

25
.eslintrc.js Normal file
View File

@ -0,0 +1,25 @@
module.exports = {
parser: '@typescript-eslint/parser',
parserOptions: {
project: 'tsconfig.json',
tsconfigRootDir: __dirname,
sourceType: 'module',
},
plugins: ['@typescript-eslint/eslint-plugin'],
extends: [
'plugin:@typescript-eslint/recommended',
'plugin:prettier/recommended',
],
root: true,
env: {
node: true,
jest: true,
},
ignorePatterns: ['.eslintrc.js'],
rules: {
'@typescript-eslint/interface-name-prefix': 'off',
'@typescript-eslint/explicit-function-return-type': 'off',
'@typescript-eslint/explicit-module-boundary-types': 'off',
'@typescript-eslint/no-explicit-any': 'off',
},
};

35
.gitignore vendored Normal file
View File

@ -0,0 +1,35 @@
# compiled output
/dist
/node_modules
# Logs
logs
*.log
npm-debug.log*
pnpm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
# OS
.DS_Store
# Tests
/coverage
/.nyc_output
# IDEs and editors
/.idea
.project
.classpath
.c9/
*.launch
.settings/
*.sublime-workspace
# IDE - VSCode
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json

4
.prettierrc Normal file
View File

@ -0,0 +1,4 @@
{
"singleQuote": true,
"trailingComma": "all"
}

73
README.md Normal file
View File

@ -0,0 +1,73 @@
<p align="center">
<a href="http://nestjs.com/" target="blank"><img src="https://nestjs.com/img/logo-small.svg" width="200" alt="Nest Logo" /></a>
</p>
[circleci-image]: https://img.shields.io/circleci/build/github/nestjs/nest/master?token=abc123def456
[circleci-url]: https://circleci.com/gh/nestjs/nest
<p align="center">A progressive <a href="http://nodejs.org" target="_blank">Node.js</a> framework for building efficient and scalable server-side applications.</p>
<p align="center">
<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/v/@nestjs/core.svg" alt="NPM Version" /></a>
<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/l/@nestjs/core.svg" alt="Package License" /></a>
<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/dm/@nestjs/common.svg" alt="NPM Downloads" /></a>
<a href="https://circleci.com/gh/nestjs/nest" target="_blank"><img src="https://img.shields.io/circleci/build/github/nestjs/nest/master" alt="CircleCI" /></a>
<a href="https://coveralls.io/github/nestjs/nest?branch=master" target="_blank"><img src="https://coveralls.io/repos/github/nestjs/nest/badge.svg?branch=master#9" alt="Coverage" /></a>
<a href="https://discord.gg/G7Qnnhy" target="_blank"><img src="https://img.shields.io/badge/discord-online-brightgreen.svg" alt="Discord"/></a>
<a href="https://opencollective.com/nest#backer" target="_blank"><img src="https://opencollective.com/nest/backers/badge.svg" alt="Backers on Open Collective" /></a>
<a href="https://opencollective.com/nest#sponsor" target="_blank"><img src="https://opencollective.com/nest/sponsors/badge.svg" alt="Sponsors on Open Collective" /></a>
<a href="https://paypal.me/kamilmysliwiec" target="_blank"><img src="https://img.shields.io/badge/Donate-PayPal-ff3f59.svg"/></a>
<a href="https://opencollective.com/nest#sponsor" target="_blank"><img src="https://img.shields.io/badge/Support%20us-Open%20Collective-41B883.svg" alt="Support us"></a>
<a href="https://twitter.com/nestframework" target="_blank"><img src="https://img.shields.io/twitter/follow/nestframework.svg?style=social&label=Follow"></a>
</p>
<!--[![Backers on Open Collective](https://opencollective.com/nest/backers/badge.svg)](https://opencollective.com/nest#backer)
[![Sponsors on Open Collective](https://opencollective.com/nest/sponsors/badge.svg)](https://opencollective.com/nest#sponsor)-->
## Description
[Nest](https://github.com/nestjs/nest) framework TypeScript starter repository.
## Installation
```bash
$ npm install
```
## Running the app
```bash
# development
$ npm run start
# watch mode
$ npm run start:dev
# production mode
$ npm run start:prod
```
## Test
```bash
# unit tests
$ npm run test
# e2e tests
$ npm run test:e2e
# test coverage
$ npm run test:cov
```
## Support
Nest is an MIT-licensed open source project. It can grow thanks to the sponsors and support by the amazing backers. If you'd like to join them, please [read more here](https://docs.nestjs.com/support).
## Stay in touch
- Author - [Kamil Myśliwiec](https://kamilmysliwiec.com)
- Website - [https://nestjs.com](https://nestjs.com/)
- Twitter - [@nestframework](https://twitter.com/nestframework)
## License
Nest is [MIT licensed](LICENSE).

8
nest-cli.json Normal file
View File

@ -0,0 +1,8 @@
{
"$schema": "https://json.schemastore.org/nest-cli",
"collection": "@nestjs/schematics",
"sourceRoot": "src",
"compilerOptions": {
"deleteOutDir": true
}
}

8849
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

72
package.json Normal file
View File

@ -0,0 +1,72 @@
{
"name": "tools",
"version": "0.0.1",
"description": "",
"author": "",
"private": true,
"license": "UNLICENSED",
"scripts": {
"build": "nest build",
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
"dev": "nest start --watch",
"start:debug": "nest start --debug --watch",
"start:prod": "node dist/main",
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
"test": "jest",
"test:watch": "jest --watch",
"test:cov": "jest --coverage",
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
"test:e2e": "jest --config ./test/jest-e2e.json"
},
"dependencies": {
"@nestjs/common": "^10.0.0",
"@nestjs/core": "^10.0.0",
"@nestjs/platform-express": "^10.0.0",
"@nestjs/schedule": "^4.0.0",
"axios": "^1.6.3",
"moment": "^2.30.1",
"qs": "^6.11.2",
"reflect-metadata": "^0.1.13",
"rxjs": "^7.8.1"
},
"devDependencies": {
"@nestjs/cli": "^10.0.0",
"@nestjs/schematics": "^10.0.0",
"@nestjs/testing": "^10.0.0",
"@types/express": "^4.17.17",
"@types/jest": "^29.5.2",
"@types/node": "^20.3.1",
"@types/supertest": "^2.0.12",
"@typescript-eslint/eslint-plugin": "^6.0.0",
"@typescript-eslint/parser": "^6.0.0",
"eslint": "^8.42.0",
"eslint-config-prettier": "^9.0.0",
"eslint-plugin-prettier": "^5.0.0",
"jest": "^29.5.0",
"prettier": "^3.0.0",
"source-map-support": "^0.5.21",
"supertest": "^6.3.3",
"ts-jest": "^29.1.0",
"ts-loader": "^9.4.3",
"ts-node": "^10.9.1",
"tsconfig-paths": "^4.2.0",
"typescript": "^5.1.3"
},
"jest": {
"moduleFileExtensions": [
"js",
"json",
"ts"
],
"rootDir": "src",
"testRegex": ".*\\.spec\\.ts$",
"transform": {
"^.+\\.(t|j)s$": "ts-jest"
},
"collectCoverageFrom": [
"**/*.(t|j)s"
],
"coverageDirectory": "../coverage",
"testEnvironment": "node"
}
}

20
src/app.module.ts Normal file
View File

@ -0,0 +1,20 @@
import { Module } from '@nestjs/common';
import { ScheduleModule } from '@nestjs/schedule';
import { modules } from './server';
const imports = [
...modules
];
@Module({
imports: [],
controllers: [],
providers: [],
})
export class AppModule {}
@Module({
imports: [...imports, ScheduleModule.forRoot()]
})
export class CronModule {
}

3
src/config/config.ts Normal file
View File

@ -0,0 +1,3 @@
import path from 'path';
export const __ROOT__ = path.resolve(process.cwd());

15
src/config/ta.config.ts Normal file
View File

@ -0,0 +1,15 @@
export const taConfig = {
yh: {
apiSecret: 'H8XtphlNRGeQqsHHGkgy3eHm01XcHVkcXdJ1bLoL7088fSKg0ysXyDlLbccMWjrU',
eventDb: 'ta.v_event_20',
channel: '硬核'
},
gb: {
apiSecret: 'vChwtzzB4b7dD0XztmYJRGnScQ0d1uKe40edJjwQOjVcM5afPU93ySMiIeJlzybg',
eventDb: 'ta.v_event_19',
channel: '官包'
}
};
export const taUrl = 'http://47.112.98.161:8992';

17
src/lib/dd.ts Normal file
View File

@ -0,0 +1,17 @@
import axios from 'axios';
export class DingDing {
constructor(private appKey: string, private appSecret: string) {}
async send() {
const token = 'cb2e1831716c39bbb24b467a881be84a';
const url = 'https://oapi.dingtalk.com/robot/send?access_token=078c25a07c51570684c833d10cc04cfadf147ef6d9d2fc382d04f28e1271664e';
const { data } = await axios.post(url, {
msgtype: 'text',
text: {
content: '哈哈哈哈'
}
});
console.log('data', data);
}
}

11
src/main.ts Normal file
View File

@ -0,0 +1,11 @@
import { NestFactory } from '@nestjs/core';
import { AppModule, CronModule } from './app.module';
(async () => {
await NestFactory.createApplicationContext(CronModule);
const app = await NestFactory.create(AppModule);
app.enableCors();
await app.listen(3000);
console.log(`Application is running on: ${await app.getUrl()}`);
})();

9
src/server/index.ts Normal file
View File

@ -0,0 +1,9 @@
import path from 'path';
import { readDir } from '../util/file.util';
export const modules = [];
readDir(path.resolve(__dirname))
.filter(f => path.basename(f, path.extname(f)).endsWith('module'))
// @ts-ignore
.map(f => modules.push(...Object.values(require(f))));

View File

@ -0,0 +1,21 @@
export interface ITaConfig {
apiSecret: string;
eventDb: string;
channel: string;
}
export interface IRecordInfo {
uid: string;
num: number;
}
export interface INotifyConfig {
useXianYu: number;
getXianYu: number;
recharge: number;
useItem: number[][];
getItem: number[][];
}
export type EChangeType = 'use' | 'get';

View File

@ -0,0 +1,63 @@
/**
*
*/
import axios from 'axios';
import { Injectable } from '@nestjs/common';
import { EChangeType, IRecordInfo } from './interface';
@Injectable()
export class NotifyService {
private _itemTemplate = '[channel][uids]等玩家的[itemId][type量]已超过预警值,请注意处理';
private _rechargeTemplate = '[channel][uids]等玩家的充值已超过预警值,请注意处理';
async notifyXianYu(data: IRecordInfo[], channel: string, type: EChangeType) {
if (data.length === 0) return;
const uids = data.map(d => d.uid).join(',');
const content = this.formatItem(channel, uids, '仙玉', type);
console.log(content, data);
await this.sendDD(content);
}
async notifyItem(data: IRecordInfo[], channel: string, itemId: number, type: EChangeType) {
if (data.length === 0) return;
const uids = data.map(d => d.uid).join(',');
const content = this.formatItem(channel, uids, itemId, type);
console.log(content, data);
await this.sendDD(content);
}
async notifyRecharge(data: IRecordInfo[], channel: string) {
if (data.length === 0) return;
const uids = data.map(d => d.uid).join(',');
const content = this.formatRecharge(channel, uids);
console.log(content, data);
await this.sendDD(content);
}
private formatItem(channel: string, uids: string, itemId: string | number, type: EChangeType) {
return this._itemTemplate
.replace('channel', channel)
.replace('uids', uids)
.replace('itemId', itemId as string)
.replace('type', type === 'use' ? '消耗' : '获取')
}
private formatRecharge(channel: string, uids: string) {
return this._rechargeTemplate
.replace('channel', channel)
.replace('uids', uids);
}
private async sendDD(content: string) {
const url = 'https://oapi.dingtalk.com/robot/send?access_token=078c25a07c51570684c833d10cc04cfadf147ef6d9d2fc382d04f28e1271664e';
const { data } = await axios.post(url, {
msgtype: 'text',
text: {
content
}
});
console.log('sendDd res', data);
}
}

View File

@ -0,0 +1,12 @@
import { Module } from '@nestjs/common';
import { NotifyService } from './notify.service';
import { TaService } from './ta.service';
@Module({
imports: [
],
controllers: [],
providers: [NotifyService, TaService]
})
export class TaModule {
}

139
src/server/ta/ta.service.ts Normal file
View File

@ -0,0 +1,139 @@
import fs from 'fs';
import path from 'path';
import qs from 'qs';
import moment from 'moment';
import axios, { AxiosInstance } from 'axios';
import { Injectable } from '@nestjs/common';
import { Cron, CronExpression } from '@nestjs/schedule';
import { taConfig, taUrl } from '../../config/ta.config';
import { NotifyService } from './notify.service';
import { INotifyConfig, IRecordInfo, ITaConfig } from './interface';
import { __ROOT__ } from '../../config/config';
@Injectable()
export class TaService {
private axiosInstance: AxiosInstance;
constructor(private _notifyService: NotifyService) {
this.axiosInstance = axios.create({
baseURL: taUrl
});
}
@Cron(CronExpression.EVERY_HOUR)
async checkAll() {
const date = moment().format('YYYY-MM-DD');
const config = this.readConfig();
Object.values(taConfig).forEach(c => {
this.checkXianYu(c, date, 'get', config.getXianYu).then();
this.checkXianYu(c, date, 'use', config.useXianYu).then();
this.checkPay(c, date, config.recharge).then();
config.getItem.map(([itemId, limit]) => this.checkItem(c, date, itemId, 'get', limit));
config.useItem.map(([itemId, limit]) => this.checkItem(c, date, itemId, 'use', limit));
});
}
private async checkPay(config: ITaConfig, date: string, limit: number) {
if (!limit) {
console.log('none checkPay config', limit);
return;
}
const { eventDb, apiSecret, channel } = config;
const sql = this.rechargeSql(eventDb, date, limit);
const res = await this.readData(apiSecret, sql);
await this._notifyService.notifyRecharge(res, channel);
}
private async checkXianYu(config: ITaConfig, date: string, type: 'use' | 'get', limit: number) {
if (!limit) {
console.log(`none checkGetXianYu config, limit: ${limit}`);
return;
}
const { eventDb, apiSecret, channel } = config;
const eventName = type === 'use' ? 'use_xian_yu' : 'get_xian_yu';
const sql = this.xianYuChangeSql(eventDb, date, eventName, limit);
const res = await this.readData(apiSecret, sql);
await this._notifyService.notifyXianYu(res, channel, type);
}
private async checkItem(config: ITaConfig, date: string, itemId: number, type: 'use' | 'get', limit: number) {
if (!limit) {
console.log(`none checkItem config, type: ${type}, limit: ${limit}`);
return;
}
const { eventDb, apiSecret, channel } = config;
const eventName = type === 'use' ? 'use_item' : 'get_item';
const sql = this.itemChangeSql(eventDb, date, itemId, eventName, limit);
const res = await this.readData(apiSecret, sql);
await this._notifyService.notifyItem(res, channel, itemId, type);
}
private readConfig() {
const file = path.resolve(__ROOT__, 'temp/notify.txt');
const str = fs.readFileSync(file).toString();
return JSON.parse(str) as INotifyConfig;
}
private async readData(token: string, sql: string) {
const { data } = await this.axiosInstance.post(`querySql?token=${token}`, qs.stringify({
sql,
format: 'csv_header'
}), {
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
}
});
// console.log('data', data);
return this.formatData(data);
}
private formatData(str: string) {
const res: IRecordInfo[] = [];
const arr = str.replaceAll('"', '').split('\n');
const header = arr.shift().split(',');
for (const s of arr) {
if (!s) continue;
const ss = s.split(',');
const temp: IRecordInfo = {} as IRecordInfo;
for (const idx in ss) {
temp[header[idx]] = isNaN(+ss[idx]) ? ss[idx] : +ss[idx];
}
res.push(temp);
}
return res;
}
private rechargeSql(db: string, date: string, limit = 1000) {
return `
SELECT "#account_id" AS "uid", sum ("pay_amount") AS "num"
FROM ${db}
WHERE "$part_date" = '${date}' AND "#event_name" = 'order_finish'
GROUP BY "#account_id" HAVING sum ("pay_amount") > ${limit}
`;
}
private xianYuChangeSql(db: string, date: string, eventName: string, limit = 10 * 1000) {
return `
SELECT "#account_id" AS "uid", sum ("change_num") AS "num"
FROM ${db}
WHERE "$part_date" = '${date}' AND "#event_name" = '${eventName}'
GROUP BY "#account_id" HAVING sum ("change_num") > ${limit}
`;
}
private itemChangeSql(db: string, date: string, itemId: number, eventName: string, limit = 100000) {
return `
SELECT "#account_id" AS "uid", sum ("change_num") AS "num"
FROM ${db}
WHERE "$part_date" = '${date}' AND "#event_name" = '${eventName}' AND "item_id" = ${itemId}
GROUP BY "#account_id" HAVING sum ("change_num") > ${limit}
`;
}
}

21
src/util/file.util.ts Normal file
View File

@ -0,0 +1,21 @@
import fs from 'fs';
import path from 'path';
export const readDir = (dir: string, fn?: (filename: string) => void): string[] => {
let arr: string[] = [];
if (!fs.existsSync(dir)) return arr;
fs.readdirSync(dir).map(d => {
const filename = path.resolve(dir, d);
const stat = fs.statSync(filename);
if (stat.isFile()) {
fn && fn(filename);
arr.push(filename);
} else {
arr = arr.concat(readDir(filename, fn));
}
});
return arr;
};

1
temp/notify.txt Normal file
View File

@ -0,0 +1 @@
{"useXianYu":0,"getXianYu":100000,"recharge":10000,"useItem":[],"getItem":[[1002, 1000],[2226,10000]]}

4
tsconfig.build.json Normal file
View File

@ -0,0 +1,4 @@
{
"extends": "./tsconfig.json",
"exclude": ["node_modules", "test", "dist", "**/*spec.ts"]
}

22
tsconfig.json Normal file
View File

@ -0,0 +1,22 @@
{
"compilerOptions": {
"module": "commonjs",
"declaration": true,
"removeComments": true,
"esModuleInterop": true,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"allowSyntheticDefaultImports": true,
"target": "ES2021",
"sourceMap": true,
"outDir": "./dist",
"baseUrl": "./",
"incremental": true,
"skipLibCheck": true,
"strictNullChecks": false,
"noImplicitAny": false,
"strictBindCallApply": false,
"forceConsistentCasingInFileNames": false,
"noFallthroughCasesInSwitch": false
}
}