Compare commits
8 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
9af780d21f | ||
![]() |
33152abf71 | ||
![]() |
68fe869945 | ||
![]() |
daeac2e263 | ||
![]() |
9e2efe6320 | ||
![]() |
533505465d | ||
![]() |
0a6b7a822a | ||
![]() |
1a15888de0 |
5
.gitignore
vendored
5
.gitignore
vendored
@ -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
37
ecosystem.config.js
Normal 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
|
||||
};
|
@ -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",
|
||||
|
@ -13,3 +13,5 @@ export const taConfig = {
|
||||
};
|
||||
|
||||
export const taUrl = 'http://47.112.98.161:8992';
|
||||
|
||||
export const fromDate = '2023-12-28';
|
||||
|
5
src/server/ta/constant.ts
Normal file
5
src/server/ta/constant.ts
Normal file
@ -0,0 +1,5 @@
|
||||
|
||||
export enum EResourceId {
|
||||
DJQ = 2226,
|
||||
XIAN_YU = 202,
|
||||
}
|
@ -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;
|
||||
|
@ -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) {
|
||||
|
@ -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
3
src/server/ta/util.ts
Normal 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
1
temp/whiteUids.txt
Normal file
@ -0,0 +1 @@
|
||||
{"硬核":[],"官包":[]}
|
Loading…
Reference in New Issue
Block a user