Compare commits

...

8 Commits
main ... master

Author SHA1 Message Date
colter
9af780d21f . 2024-01-02 20:51:42 +08:00
colter
33152abf71 增加白名单过滤 2024-01-02 20:51:08 +08:00
colter
68fe869945 报警增加代金券消耗和累计充值 2024-01-02 15:31:01 +08:00
colter
daeac2e263 报警增加代金券消耗和累计充值 2024-01-02 15:22:15 +08:00
colter
9e2efe6320 . 2023-12-30 13:27:01 +08:00
colter
533505465d . 2023-12-30 12:55:43 +08:00
colter
0a6b7a822a . 2023-12-30 12:55:12 +08:00
colter
1a15888de0 . 2023-12-30 12:50:49 +08:00
10 changed files with 185 additions and 39 deletions

5
.gitignore vendored
View File

@ -2,6 +2,9 @@
/dist
/node_modules
/temp/notify.txt
/temp/whiteUids.txt
# Logs
logs
*.log
@ -32,4 +35,4 @@ lerna-debug.log*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
!.vscode/extensions.json

37
ecosystem.config.js Normal file
View File

@ -0,0 +1,37 @@
const logDateFormat = 'YYYY-MM-DD HH:mm:ss';
const argv = process.argv;
const apps = [];
const init = (name, id = 0, instances = 1) => {
if (!name) throw 'process name is required';
const defaults = {
// args: [name, id],
log_date_format: logDateFormat,
min_uptime: '60s',
max_restarts: 2,
name: `tools_${name}`
};
const opt = {};
if (name !== 'cron') {
Object.assign(opt, {
exec_mode: 'cluster', // 进程模式可选值cluster/fork
instances, // 启动的进程实例数,如果值 5 ,则会启动一个进程数为 5 的进程集群,等效 `pm2 start app -i 5`
merge_logs: true // 集群模式,是否合并一个集群的进程日志
})
}
apps.push({
...defaults,
script: `dist/${name}.js`,
...opt
});
};
init(argv[5], argv[6], argv[7]);
module.exports = {
apps
};

View File

@ -9,8 +9,7 @@
"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",
"start": "pm2 start ecosystem.config.js --",
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
"test": "jest",
"test:watch": "jest --watch",

View File

@ -13,3 +13,5 @@ export const taConfig = {
};
export const taUrl = 'http://47.112.98.161:8992';
export const fromDate = '2023-12-28';

View File

@ -0,0 +1,5 @@
export enum EResourceId {
DJQ = 2226,
XIAN_YU = 202,
}

View File

@ -10,6 +10,13 @@ export interface IRecordInfo {
num: number;
}
export interface INotifyInfo {
uid: string;
num: number;
recharge: number;
djq: number;
}
export interface INotifyConfig {
useXianYu: number;
getXianYu: number;

View File

@ -2,52 +2,78 @@
*
*/
import fs from 'fs';
import path from 'path';
import axios from 'axios';
import { Injectable } from '@nestjs/common';
import { EChangeType, IRecordInfo } from './interface';
import { EChangeType, INotifyInfo, IRecordInfo } from './interface';
import { __ROOT__ } from '../../config/config';
@Injectable()
export class NotifyService {
private _itemTemplate = '[channel][uids]等玩家的[itemId][type量]已超过预警值,请注意处理';
private _rechargeTemplate = '[channel][uids]等玩家的充值已超过预警值,请注意处理';
private _itemTemplate = '[channel][itemId][type量]已触发报警,请注意处理\n';
private _rechargeTemplate = '[channel]充值已触发报警,请注意处理\n';
private records: Map<string, string[]> = new Map();
async notifyXianYu(data: IRecordInfo[], channel: string, type: EChangeType) {
async notifyRecharge(data: INotifyInfo[], channel: string) {
if (data.length === 0) return;
const uids = data.map(d => d.uid).join(',');
const content = this.formatItem(channel, uids, '仙玉', type);
console.log(content, data);
const key = `${channel}-recharge`;
const old = this.records.has(key) ? this.records.get(key) : [];
const uids = data.map(d => d.uid).filter(uid => !old.includes(uid));
if (uids.length === 0) return;
const content = this.formatRecharge(channel, data);
console.log(content);
await this.sendDD(content);
// old.push(...uids);
// this.records.set(key, old);
}
async notifyItem(data: IRecordInfo[], channel: string, itemId: number, type: EChangeType) {
async notifyItemChange(data: INotifyInfo[], channel: string, itemId: number | string, type: EChangeType) {
const whites = this.readWhiteUids();
data = data.filter(d => !whites[channel].includes(d.uid));
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);
const key = `${channel}-${type}`;
const old = this.records.has(key) ? this.records.get(key) : [];
const uids = data.map(d => d.uid).filter(uid => !old.includes(uid));
if (uids.length === 0) return;
const content = this.formatItem(channel, data, itemId, type);
console.log(content);
await this.sendDD(content);
// old.push(...uids);
// this.records.set(key, old);
}
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 readWhiteUids() {
const file = path.resolve(__ROOT__, 'temp/whiteUids.txt');
const str = fs.readFileSync(file).toString();
return JSON.parse(str) as Record<string, string[]>;
}
private formatItem(channel: string, uids: string, itemId: string | number, type: EChangeType) {
private formatItem(channel: string, data: INotifyInfo[], itemId: string | number, type: EChangeType) {
return this._itemTemplate
.replace('channel', channel)
.replace('uids', uids)
.replace('itemId', itemId as string)
.replace('type', type === 'use' ? '消耗' : '获取')
.replace('type', type === 'use' ? '消耗' : '获取') + this.formatItemNotifyInfo(data, type);
}
private formatRecharge(channel: string, uids: string) {
private formatRecharge(channel: string, data: INotifyInfo[]) {
return this._rechargeTemplate
.replace('channel', channel)
.replace('uids', uids);
.replace('channel', channel) + this.formatNotifyInfo(data);
}
private formatItemNotifyInfo(data: INotifyInfo[], type: EChangeType) {
return data.map(d => `[${d.uid}][${type === 'get' ? '获取' : '消耗'}: ${d.num}][总代金券消耗: ${d.djq}][总充值: ${d.recharge}]`).join('\n');
}
private formatNotifyInfo(data: INotifyInfo[]) {
return data.map(d => `[${d.uid}][总代金券消耗: ${d.djq}][总充值: ${d.recharge}]`).join('\n');
}
private async sendDD(content: string) {

View File

@ -5,10 +5,12 @@ 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 { fromDate, taConfig, taUrl } from '../../config/ta.config';
import { NotifyService } from './notify.service';
import { INotifyConfig, IRecordInfo, ITaConfig } from './interface';
import { EChangeType, INotifyConfig, INotifyInfo, IRecordInfo, ITaConfig } from './interface';
import { __ROOT__ } from '../../config/config';
import { EResourceId } from './constant';
import { getEventName } from './util';
@Injectable()
export class TaService {
@ -41,10 +43,13 @@ export class TaService {
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);
if (res.length === 0) return;
const ni = await this.getNotifyInfo(apiSecret, eventDb, res);
await this._notifyService.notifyRecharge(ni, channel);
}
private async checkXianYu(config: ITaConfig, date: string, type: 'use' | 'get', limit: number) {
private async checkXianYu(config: ITaConfig, date: string, type: EChangeType, limit: number) {
if (!limit) {
console.log(`none checkGetXianYu config, limit: ${limit}`);
return;
@ -54,10 +59,13 @@ export class TaService {
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);
if (res.length === 0) return;
const ni = await this.getNotifyInfo(apiSecret, eventDb, res);
await this._notifyService.notifyItemChange(ni, channel, '仙玉', type);
}
private async checkItem(config: ITaConfig, date: string, itemId: number, type: 'use' | 'get', limit: number) {
private async checkItem(config: ITaConfig, date: string, itemId: number, type: EChangeType, limit: number) {
if (!limit) {
console.log(`none checkItem config, type: ${type}, limit: ${limit}`);
return;
@ -67,8 +75,41 @@ export class TaService {
const eventName = type === 'use' ? 'use_item' : 'get_item';
const sql = this.itemChangeSql(eventDb, date, itemId, eventName, limit);
const res = await this.readData(apiSecret, sql);
if (res.length === 0) return;
await this._notifyService.notifyItem(res, channel, itemId, type);
const ni = await this.getNotifyInfo(apiSecret, eventDb, res);
await this._notifyService.notifyItemChange(ni, channel, itemId, type);
}
private async getNotifyInfo(apiSecret: string, eventDb: string, infos: IRecordInfo[]) {
const uids = infos.map(r => r.uid);
const totalRecharge = await this.getTotalRecharge(apiSecret, eventDb, uids);
const totalDjq = await this.getTotalDjq(apiSecret, eventDb, uids, 'use');
const res: INotifyInfo[] = [];
for (const info of infos) {
const rechargeRecord = totalRecharge.find(t => t.uid === info.uid);
const djqRecord = totalDjq.find(t => t.uid === info.uid);
res.push({
...info,
djq: djqRecord?.num || 0,
recharge: rechargeRecord?.num || 0
});
}
return res;
}
private async getTotalRecharge(apiSecret: string, db: string, uids: string[]) {
const sql = this.totalRechargeSql(db, uids, fromDate);
return this.readData(apiSecret, sql);
}
private async getTotalDjq(apiSecret: string, db: string, uids: string[], type: EChangeType) {
const sql = this.itemTotalSql(db, EResourceId.DJQ, getEventName(type), uids, fromDate);
return this.readData(apiSecret, sql);
}
private readConfig() {
@ -86,7 +127,7 @@ export class TaService {
'Content-Type': 'application/x-www-form-urlencoded'
}
});
// console.log('data', data);
// console.log('data', data)
return this.formatData(data);
}
@ -118,8 +159,18 @@ export class TaService {
GROUP BY "#account_id" HAVING sum ("pay_amount") > ${limit}
`;
}
private totalRechargeSql(db: string, uids: string[], fromDate: string) {
return `
SELECT "#account_id" AS "uid", sum("pay_amount") AS "num"
FROM ${db}
WHERE "$part_date" >= '${fromDate}'
AND "#event_name" = 'order_finish'
AND "#account_id" IN (${uids.map(u => `'${u}'`).join(',')})
GROUP BY "#account_id";
`;
}
private xianYuChangeSql(db: string, date: string, eventName: string, limit = 10 * 1000) {
private xianYuChangeSql(db: string, date: string, eventName: string, limit: number) {
return `
SELECT "#account_id" AS "uid", sum ("change_num") AS "num"
FROM ${db}
@ -128,12 +179,24 @@ export class TaService {
`;
}
private itemChangeSql(db: string, date: string, itemId: number, eventName: string, limit = 100000) {
private itemTotalSql(db: string, itemId: number, eventName: string, uids: string[], fromDate: string) {
return `
SELECT "#account_id" AS "uid", sum("change_num") AS "num"
FROM ${db}
WHERE "$part_date" >= '${fromDate}'
AND "#event_name" = '${eventName}'
AND "item_id" = ${itemId}
AND "#account_id" IN (${uids.map(u => `'${u}'`).join(',')})
GROUP BY "#account_id";
`;
}
private itemChangeSql(db: string, date: string, itemId: number, eventName: string, limit: number, dateC = '=') {
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}
WHERE "$part_date" ${dateC} '${date}' AND "#event_name" = '${eventName}' AND "item_id" = ${itemId}
GROUP BY "#account_id" HAVING sum ("change_num") >= ${limit}
`;
}
}

3
src/server/ta/util.ts Normal file
View File

@ -0,0 +1,3 @@
import { EChangeType } from './interface';
export const getEventName = (type: EChangeType) => type === 'get' ? 'get_item' : 'use_item';

1
temp/whiteUids.txt Normal file
View File

@ -0,0 +1 @@
{"硬核":[],"官包":[]}