Initial commit: Telegram Management System
Some checks failed
Deploy / deploy (push) Has been cancelled
Full-stack web application for Telegram management - Frontend: Vue 3 + Vben Admin - Backend: NestJS - Features: User management, group broadcast, statistics 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
19
backend/.dockerignore
Normal file
@@ -0,0 +1,19 @@
|
||||
node_modules
|
||||
npm-debug.log
|
||||
.git
|
||||
.gitignore
|
||||
.env
|
||||
.env.local
|
||||
.env.production
|
||||
Dockerfile
|
||||
.dockerignore
|
||||
README.md
|
||||
.eslintrc
|
||||
.eslintignore
|
||||
logs/*.log
|
||||
*.log
|
||||
.DS_Store
|
||||
coverage
|
||||
.nyc_output
|
||||
.idea
|
||||
.vscode
|
||||
5
backend/.gitignore
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
/node_modules/
|
||||
/admin/node_modules/
|
||||
/admin/dist/
|
||||
/package-lock.json
|
||||
/logs/
|
||||
1
backend/.htaccess
Normal file
@@ -0,0 +1 @@
|
||||
# 请将伪静态规则或自定义Apache配置填写到此处
|
||||
BIN
backend/01_logged_in.png
Normal file
|
After Width: | Height: | Size: 131 KiB |
BIN
backend/02_name_management_page.png
Normal file
|
After Width: | Height: | Size: 130 KiB |
BIN
backend/07_final_demo.png
Normal file
|
After Width: | Height: | Size: 130 KiB |
BIN
backend/1-login-page.png
Normal file
|
After Width: | Height: | Size: 1.3 MiB |
24
backend/Dockerfile
Normal file
@@ -0,0 +1,24 @@
|
||||
FROM node:16-alpine
|
||||
|
||||
# Install dependencies for native modules
|
||||
RUN apk add --no-cache python3 make g++
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Copy package files
|
||||
COPY package*.json ./
|
||||
|
||||
# Install dependencies
|
||||
RUN npm install --only=production
|
||||
|
||||
# Copy application files
|
||||
COPY . .
|
||||
|
||||
# Create uploads directory
|
||||
RUN mkdir -p /app/uploads
|
||||
|
||||
# Expose ports
|
||||
EXPOSE 3000 3001
|
||||
|
||||
# Start the application
|
||||
CMD ["node", "src/Server.js"]
|
||||
200
backend/PromisedNetSockets.js
Normal file
7
backend/README.md
Normal file
@@ -0,0 +1,7 @@
|
||||
- 创建剧本导入目录
|
||||
- 关闭数据库的严格模式
|
||||
- 设置跨域源
|
||||
|
||||
|
||||
|
||||
|
||||
BIN
backend/after_login.png
Normal file
|
After Width: | Height: | Size: 107 KiB |
59
backend/check_22111_shop.js
Normal file
@@ -0,0 +1,59 @@
|
||||
const { chromium } = require('playwright');
|
||||
|
||||
(async () => {
|
||||
const browser = await chromium.launch({ headless: false });
|
||||
const context = await browser.newContext({
|
||||
ignoreHTTPSErrors: true
|
||||
});
|
||||
const page = await context.newPage();
|
||||
|
||||
// 监听控制台消息
|
||||
page.on('console', msg => {
|
||||
console.log(`Console ${msg.type()}: ${msg.text()}`);
|
||||
});
|
||||
|
||||
// 监听页面错误
|
||||
page.on('pageerror', error => {
|
||||
console.log(`Page error: ${error.message}`);
|
||||
});
|
||||
|
||||
// 监听请求失败
|
||||
page.on('requestfailed', request => {
|
||||
console.log(`Request failed: ${request.url()} - ${request.failure().errorText}`);
|
||||
});
|
||||
|
||||
// 监听响应
|
||||
page.on('response', response => {
|
||||
if (response.status() >= 400) {
|
||||
console.log(`Response error: ${response.url()} - Status: ${response.status()}`);
|
||||
}
|
||||
});
|
||||
|
||||
console.log('访问 https://www.22111.shop/ ...');
|
||||
|
||||
try {
|
||||
const response = await page.goto('https://www.22111.shop/', {
|
||||
waitUntil: 'networkidle',
|
||||
timeout: 30000
|
||||
});
|
||||
|
||||
console.log(`页面状态码: ${response.status()}`);
|
||||
console.log(`页面URL: ${page.url()}`);
|
||||
|
||||
// 获取页面内容
|
||||
const content = await page.content();
|
||||
console.log(`页面内容长度: ${content.length}`);
|
||||
|
||||
// 截图
|
||||
await page.screenshot({ path: '/tmp/22111_shop_screenshot.png' });
|
||||
console.log('截图已保存到 /tmp/22111_shop_screenshot.png');
|
||||
|
||||
// 等待几秒查看更多错误
|
||||
await page.waitForTimeout(5000);
|
||||
|
||||
} catch (error) {
|
||||
console.error('页面访问错误:', error.message);
|
||||
}
|
||||
|
||||
await browser.close();
|
||||
})();
|
||||
291
backend/complete_demo.js
Normal file
@@ -0,0 +1,291 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* 完整演示新姓名管理系统的所有功能
|
||||
*/
|
||||
|
||||
const { chromium } = require('playwright');
|
||||
|
||||
async function completeDemo() {
|
||||
console.log('🎭 完整演示新姓名管理系统功能...\n');
|
||||
|
||||
let browser;
|
||||
let page;
|
||||
|
||||
try {
|
||||
browser = await chromium.launch({
|
||||
headless: false,
|
||||
slowMo: 2000, // 更慢,便于观察
|
||||
devtools: false
|
||||
});
|
||||
|
||||
const context = await browser.newContext({
|
||||
viewport: { width: 1600, height: 1000 }
|
||||
});
|
||||
|
||||
page = await context.newPage();
|
||||
|
||||
// 监听所有API调用
|
||||
page.on('request', request => {
|
||||
const url = request.url();
|
||||
if (url.includes('/nameTemplate/') || url.includes('/firstname/') || url.includes('/lastname/')) {
|
||||
console.log(`🔗 ${request.method()} ${url}`);
|
||||
}
|
||||
});
|
||||
|
||||
page.on('response', response => {
|
||||
const url = response.url();
|
||||
if (url.includes('/nameTemplate/') || url.includes('/firstname/') || url.includes('/lastname/')) {
|
||||
console.log(`📡 ${response.status()} ${url}`);
|
||||
}
|
||||
});
|
||||
|
||||
// ==================== 第1步:登录 ====================
|
||||
console.log('🚀 第1步:登录系统...');
|
||||
await page.goto('http://localhost:8891');
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
await page.fill('input[type="text"]', 'admin');
|
||||
await page.fill('input[type="password"]', '111111');
|
||||
await page.click('button:has-text("登录")');
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
console.log('✅ 登录成功');
|
||||
await page.screenshot({ path: 'demo_01_login.png', fullPage: true });
|
||||
|
||||
// ==================== 第2步:直接访问姓名管理页面 ====================
|
||||
console.log('\n📍 第2步:直接访问姓名管理页面...');
|
||||
await page.goto('http://localhost:8891/#/nameManage/firstnameList');
|
||||
await page.waitForLoadState('networkidle');
|
||||
await page.waitForTimeout(3000); // 等待数据加载
|
||||
|
||||
console.log('✅ 进入姓名管理页面');
|
||||
await page.screenshot({ path: 'demo_02_name_page.png', fullPage: true });
|
||||
|
||||
// ==================== 第3步:展示新API功能 ====================
|
||||
console.log('\n🔧 第3步:演示新开发的API接口...');
|
||||
|
||||
// 调用无需认证的新API
|
||||
console.log('📡 调用 supportedOptions API...');
|
||||
const apiResult1 = await page.evaluate(async () => {
|
||||
const response = await fetch('http://localhost:3000/nameTemplate/supportedOptions');
|
||||
const data = await response.json();
|
||||
return data;
|
||||
});
|
||||
|
||||
console.log('✅ 新API支持的选项:');
|
||||
console.log(` 🌐 平台: ${apiResult1.data.platforms.join(', ')}`);
|
||||
console.log(` 🌍 文化: ${apiResult1.data.cultures.join(', ')}`);
|
||||
console.log(` 👤 性别: ${apiResult1.data.genders.join(', ')}`);
|
||||
console.log(` 📊 数据源: ${apiResult1.data.sources.join(', ')}`);
|
||||
|
||||
console.log('\n📡 调用 generatorStatus API...');
|
||||
const apiResult2 = await page.evaluate(async () => {
|
||||
const response = await fetch('http://localhost:3000/nameTemplate/generatorStatus');
|
||||
const data = await response.json();
|
||||
return data;
|
||||
});
|
||||
|
||||
console.log('✅ 4层级生成器状态:');
|
||||
Object.entries(apiResult2.data).forEach(([name, info]) => {
|
||||
console.log(` ${info.available ? '✅' : '❌'} ${name}: ${info.description} (优先级: ${info.priority})`);
|
||||
});
|
||||
|
||||
// ==================== 第4步:测试现有数据显示 ====================
|
||||
console.log('\n📋 第4步:检查现有姓名数据...');
|
||||
|
||||
const tableInfo = await page.evaluate(() => {
|
||||
const table = document.querySelector('table');
|
||||
if (!table) return { hasTable: false };
|
||||
|
||||
const rows = table.querySelectorAll('tbody tr');
|
||||
const headers = Array.from(table.querySelectorAll('thead th')).map(th => th.textContent?.trim());
|
||||
|
||||
return {
|
||||
hasTable: true,
|
||||
rowCount: rows.length,
|
||||
headers: headers,
|
||||
sampleData: Array.from(rows).slice(0, 3).map(row =>
|
||||
Array.from(row.cells).map(cell => cell.textContent?.trim())
|
||||
)
|
||||
};
|
||||
});
|
||||
|
||||
if (tableInfo.hasTable) {
|
||||
console.log(`✅ 找到数据表格:`);
|
||||
console.log(` 📊 数据行数: ${tableInfo.rowCount}`);
|
||||
console.log(` 📋 表格列: ${tableInfo.headers.join(' | ')}`);
|
||||
|
||||
if (tableInfo.sampleData.length > 0) {
|
||||
console.log(' 📝 样本数据:');
|
||||
tableInfo.sampleData.forEach((row, index) => {
|
||||
console.log(` ${index + 1}. ${row.join(' | ')}`);
|
||||
});
|
||||
}
|
||||
} else {
|
||||
console.log('❌ 未找到数据表格');
|
||||
}
|
||||
|
||||
// ==================== 第5步:测试添加功能 ====================
|
||||
console.log('\n➕ 第5步:演示添加新姓名功能...');
|
||||
|
||||
const addButton = await page.locator('button:has-text("添加")');
|
||||
if (await addButton.isVisible()) {
|
||||
console.log('✅ 找到添加按钮');
|
||||
await addButton.click();
|
||||
await page.waitForTimeout(2000);
|
||||
|
||||
console.log('🖱️ 点击添加按钮');
|
||||
await page.screenshot({ path: 'demo_03_add_modal.png', fullPage: true });
|
||||
|
||||
// 检查弹窗内容
|
||||
const modalInfo = await page.evaluate(() => {
|
||||
const modal = document.querySelector('.ivu-modal, .modal, [class*="modal"]');
|
||||
if (!modal) return { hasModal: false };
|
||||
|
||||
const inputs = modal.querySelectorAll('input');
|
||||
const buttons = modal.querySelectorAll('button');
|
||||
|
||||
return {
|
||||
hasModal: true,
|
||||
title: modal.querySelector('.ivu-modal-header, .modal-header')?.textContent?.trim(),
|
||||
inputCount: inputs.length,
|
||||
inputPlaceholders: Array.from(inputs).map(input => input.placeholder || input.type),
|
||||
buttons: Array.from(buttons).map(btn => btn.textContent?.trim())
|
||||
};
|
||||
});
|
||||
|
||||
if (modalInfo.hasModal) {
|
||||
console.log('✅ 添加弹窗打开:');
|
||||
console.log(` 📝 标题: ${modalInfo.title}`);
|
||||
console.log(` 📝 输入框: ${modalInfo.inputPlaceholders.join(', ')}`);
|
||||
console.log(` 🔘 按钮: ${modalInfo.buttons.join(', ')}`);
|
||||
|
||||
// 填写测试数据
|
||||
const nameInput = await page.locator('input[placeholder*="姓"]').first();
|
||||
if (await nameInput.isVisible()) {
|
||||
await nameInput.fill('演示姓氏');
|
||||
console.log('✏️ 填写测试数据: 演示姓氏');
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
await page.screenshot({ path: 'demo_04_filled_form.png', fullPage: true });
|
||||
|
||||
// 不实际提交,点击取消
|
||||
const cancelButton = await page.locator('button:has-text("取消")');
|
||||
if (await cancelButton.isVisible()) {
|
||||
await cancelButton.click();
|
||||
console.log('❌ 取消添加(演示完成)');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 第6步:测试搜索功能 ====================
|
||||
console.log('\n🔍 第6步:演示搜索功能...');
|
||||
|
||||
const searchInput = await page.locator('input[placeholder*="姓"]').first();
|
||||
if (await searchInput.isVisible()) {
|
||||
await searchInput.fill('李');
|
||||
console.log('🔍 输入搜索关键词: 李');
|
||||
|
||||
const searchButton = await page.locator('button:has-text("搜索")');
|
||||
if (await searchButton.isVisible()) {
|
||||
await searchButton.click();
|
||||
await page.waitForTimeout(2000);
|
||||
console.log('🔍 执行搜索');
|
||||
|
||||
await page.screenshot({ path: 'demo_05_search.png', fullPage: true });
|
||||
|
||||
// 清空搜索
|
||||
await searchInput.clear();
|
||||
await searchButton.click();
|
||||
await page.waitForTimeout(1000);
|
||||
console.log('🧹 清空搜索');
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 第7步:访问名字管理页面 ====================
|
||||
console.log('\n📍 第7步:切换到名字管理页面...');
|
||||
await page.goto('http://localhost:8891/#/nameManage/lastnameList');
|
||||
await page.waitForLoadState('networkidle');
|
||||
await page.waitForTimeout(3000);
|
||||
|
||||
console.log('✅ 进入名字管理页面');
|
||||
await page.screenshot({ path: 'demo_06_lastname_page.png', fullPage: true });
|
||||
|
||||
// ==================== 第8步:测试新API调用(需要认证) ====================
|
||||
console.log('\n🔐 第8步:尝试调用需要认证的API...');
|
||||
|
||||
// 通过浏览器的网络请求来测试(会自动带上认证信息)
|
||||
const authApiTest = await page.evaluate(async () => {
|
||||
try {
|
||||
// 尝试获取模板列表
|
||||
const response = await fetch('/nameTemplate/list', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({})
|
||||
});
|
||||
|
||||
return {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
data: response.status === 200 ? await response.json() : null
|
||||
};
|
||||
} catch (error) {
|
||||
return { error: error.message };
|
||||
}
|
||||
});
|
||||
|
||||
if (authApiTest.status === 200) {
|
||||
console.log('✅ 认证API调用成功');
|
||||
console.log(` 📊 返回数据: ${JSON.stringify(authApiTest.data).substring(0, 100)}...`);
|
||||
} else {
|
||||
console.log(`⚠️ 认证API需要token: ${authApiTest.status} ${authApiTest.statusText}`);
|
||||
}
|
||||
|
||||
// ==================== 第9步:最终展示 ====================
|
||||
console.log('\n🎉 第9步:功能展示总结...');
|
||||
|
||||
await page.screenshot({ path: 'demo_07_final.png', fullPage: true });
|
||||
|
||||
console.log('\n🏆 新姓名管理系统演示完成!');
|
||||
console.log('\n📂 生成的演示截图:');
|
||||
console.log(' demo_01_login.png - 系统登录');
|
||||
console.log(' demo_02_name_page.png - 姓氏管理页面');
|
||||
console.log(' demo_03_add_modal.png - 添加功能弹窗');
|
||||
console.log(' demo_04_filled_form.png - 表单填写');
|
||||
console.log(' demo_05_search.png - 搜索功能');
|
||||
console.log(' demo_06_lastname_page.png - 名字管理页面');
|
||||
console.log(' demo_07_final.png - 最终展示');
|
||||
|
||||
console.log('\n✨ 新功能特点总结:');
|
||||
console.log(' 🔧 新API架构: /nameTemplate/* 替代旧的 /firstname/* 和 /lastname/*');
|
||||
console.log(' 🌐 多平台支持: 8个主流通讯平台');
|
||||
console.log(' 🌍 多文化支持: 14种文化和语言');
|
||||
console.log(' 🎯 4层级生成: AI → 规则 → 模板 → 算法');
|
||||
console.log(' 🔐 智能认证: 公共API无需认证,管理API需要认证');
|
||||
console.log(' 📊 前端集成: Vue组件正确调用新API');
|
||||
|
||||
console.log('\n⏰ 浏览器将保持打开15秒供最后观察...');
|
||||
await page.waitForTimeout(15000);
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ 演示失败:', error.message);
|
||||
if (page) {
|
||||
await page.screenshot({ path: 'demo_error.png', fullPage: true });
|
||||
}
|
||||
} finally {
|
||||
if (browser) {
|
||||
await browser.close();
|
||||
}
|
||||
console.log('🏁 演示结束');
|
||||
}
|
||||
}
|
||||
|
||||
if (require.main === module) {
|
||||
completeDemo().catch(console.error);
|
||||
}
|
||||
|
||||
module.exports = completeDemo;
|
||||
366
backend/comprehensive_test.js
Normal file
@@ -0,0 +1,366 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* 全面测试统一姓名管理系统的所有功能
|
||||
*/
|
||||
|
||||
const { chromium } = require('playwright');
|
||||
|
||||
async function comprehensiveTest() {
|
||||
console.log('🔧 开始全面测试统一姓名管理系统...\n');
|
||||
|
||||
let browser;
|
||||
let page;
|
||||
let testResults = {
|
||||
statusPanel: false,
|
||||
generateFunction: false,
|
||||
addTemplate: false,
|
||||
searchFilter: false,
|
||||
tableOperations: false,
|
||||
editDelete: false,
|
||||
apiEndpoints: {}
|
||||
};
|
||||
|
||||
try {
|
||||
browser = await chromium.launch({
|
||||
headless: false,
|
||||
slowMo: 1000
|
||||
});
|
||||
|
||||
const context = await browser.newContext({
|
||||
viewport: { width: 1400, height: 900 }
|
||||
});
|
||||
|
||||
page = await context.newPage();
|
||||
|
||||
// 监听API调用和错误
|
||||
const apiCalls = [];
|
||||
page.on('request', request => {
|
||||
const url = request.url();
|
||||
if (url.includes('/nameTemplate/')) {
|
||||
apiCalls.push({
|
||||
method: request.method(),
|
||||
url: url,
|
||||
timestamp: new Date()
|
||||
});
|
||||
console.log(`🔗 ${request.method()} ${url}`);
|
||||
}
|
||||
});
|
||||
|
||||
page.on('response', response => {
|
||||
const url = response.url();
|
||||
if (url.includes('/nameTemplate/')) {
|
||||
console.log(`📡 ${response.status()} ${url}`);
|
||||
|
||||
// 记录API测试结果
|
||||
const endpoint = url.split('/nameTemplate/')[1].split('?')[0];
|
||||
testResults.apiEndpoints[endpoint] = {
|
||||
status: response.status(),
|
||||
success: response.status() >= 200 && response.status() < 300
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
page.on('console', msg => {
|
||||
if (msg.type() === 'error') {
|
||||
console.log(`❌ 前端错误: ${msg.text()}`);
|
||||
}
|
||||
});
|
||||
|
||||
// ==================== 登录 ====================
|
||||
console.log('🚀 登录系统...');
|
||||
await page.goto('http://localhost:8891');
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
await page.fill('input[type="text"]', 'admin');
|
||||
await page.fill('input[type="password"]', '111111');
|
||||
await page.click('button:has-text("登录")');
|
||||
await page.waitForLoadState('networkidle');
|
||||
console.log('✅ 登录成功\n');
|
||||
|
||||
// ==================== 测试1:系统状态面板 ====================
|
||||
console.log('📊 测试1:系统状态面板加载...');
|
||||
await page.goto('http://localhost:8891/#/nameManage/unified');
|
||||
await page.waitForLoadState('networkidle');
|
||||
await page.waitForTimeout(5000);
|
||||
|
||||
const statusPanelTest = await page.evaluate(() => {
|
||||
const statusCards = document.querySelectorAll('.status-card');
|
||||
const generatorItems = document.querySelectorAll('.generator-item');
|
||||
const platformTags = document.querySelectorAll('.platform-tags .ivu-tag');
|
||||
const cultureTags = document.querySelectorAll('.culture-tags .ivu-tag');
|
||||
|
||||
return {
|
||||
hasStatusPanel: !!document.querySelector('.status-panel'),
|
||||
statusCardsCount: statusCards.length,
|
||||
generatorCount: generatorItems.length,
|
||||
platformCount: platformTags.length,
|
||||
cultureCount: cultureTags.length,
|
||||
generatorStates: Array.from(generatorItems).map(item => ({
|
||||
name: item.querySelector('.generator-name')?.textContent,
|
||||
available: item.querySelector('.status-success') !== null
|
||||
}))
|
||||
};
|
||||
});
|
||||
|
||||
testResults.statusPanel = statusPanelTest.hasStatusPanel &&
|
||||
statusPanelTest.statusCardsCount === 3 &&
|
||||
statusPanelTest.generatorCount === 4;
|
||||
|
||||
console.log(`${testResults.statusPanel ? '✅' : '❌'} 状态面板测试:`);
|
||||
console.log(` - 状态面板存在: ${statusPanelTest.hasStatusPanel}`);
|
||||
console.log(` - 状态卡片: ${statusPanelTest.statusCardsCount}/3`);
|
||||
console.log(` - 生成器: ${statusPanelTest.generatorCount}/4`);
|
||||
console.log(` - 平台标签: ${statusPanelTest.platformCount}`);
|
||||
console.log(` - 文化标签: ${statusPanelTest.cultureCount}`);
|
||||
statusPanelTest.generatorStates.forEach(gen => {
|
||||
console.log(` - ${gen.name}: ${gen.available ? '可用' : '不可用'}`);
|
||||
});
|
||||
|
||||
await page.screenshot({ path: 'test_01_status_panel.png', fullPage: true });
|
||||
|
||||
// ==================== 测试2:智能生成功能 ====================
|
||||
console.log('\n🎲 测试2:智能生成功能...');
|
||||
|
||||
const generateTest = await page.evaluate(async () => {
|
||||
const generateBtn = document.querySelector('button:has-text("生成姓名")');
|
||||
const platformSelect = document.querySelector('.generate-panel .ivu-select');
|
||||
|
||||
if (!generateBtn || !platformSelect) {
|
||||
return { hasGeneratePanel: false };
|
||||
}
|
||||
|
||||
// 点击生成按钮
|
||||
generateBtn.click();
|
||||
|
||||
// 等待一下看是否有结果
|
||||
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||
|
||||
const resultItems = document.querySelectorAll('.name-result-item');
|
||||
|
||||
return {
|
||||
hasGeneratePanel: true,
|
||||
hasGenerateButton: !!generateBtn,
|
||||
resultsCount: resultItems.length,
|
||||
buttonDisabled: generateBtn.disabled
|
||||
};
|
||||
});
|
||||
|
||||
testResults.generateFunction = generateTest.hasGeneratePanel && generateTest.hasGenerateButton;
|
||||
|
||||
console.log(`${testResults.generateFunction ? '✅' : '❌'} 智能生成测试:`);
|
||||
console.log(` - 生成面板存在: ${generateTest.hasGeneratePanel}`);
|
||||
console.log(` - 生成按钮存在: ${generateTest.hasGenerateButton}`);
|
||||
console.log(` - 生成结果数量: ${generateTest.resultsCount}`);
|
||||
|
||||
await page.screenshot({ path: 'test_02_generate_function.png', fullPage: true });
|
||||
|
||||
// ==================== 测试3:添加模板功能 ====================
|
||||
console.log('\n➕ 测试3:添加姓名模板功能...');
|
||||
|
||||
const addButton = await page.locator('button:has-text("添加姓名模板")');
|
||||
if (await addButton.isVisible()) {
|
||||
await addButton.click();
|
||||
await page.waitForTimeout(2000);
|
||||
|
||||
const addModalTest = await page.evaluate(() => {
|
||||
const modal = document.querySelector('.ivu-modal');
|
||||
if (!modal) return { hasModal: false };
|
||||
|
||||
const inputs = modal.querySelectorAll('input');
|
||||
const selects = modal.querySelectorAll('.ivu-select');
|
||||
|
||||
return {
|
||||
hasModal: true,
|
||||
title: modal.querySelector('.ivu-modal-header')?.textContent?.trim(),
|
||||
inputCount: inputs.length,
|
||||
selectCount: selects.length,
|
||||
hasLastNameInput: !!modal.querySelector('input[placeholder*="姓氏"]'),
|
||||
hasFirstNameInput: !!modal.querySelector('input[placeholder*="名字"]'),
|
||||
hasDisplayNameInput: !!modal.querySelector('input[placeholder*="显示名称"]')
|
||||
};
|
||||
});
|
||||
|
||||
testResults.addTemplate = addModalTest.hasModal &&
|
||||
addModalTest.hasLastNameInput &&
|
||||
addModalTest.hasFirstNameInput;
|
||||
|
||||
console.log(`${testResults.addTemplate ? '✅' : '❌'} 添加模板测试:`);
|
||||
console.log(` - 弹窗打开: ${addModalTest.hasModal}`);
|
||||
console.log(` - 弹窗标题: ${addModalTest.title}`);
|
||||
console.log(` - 输入框数量: ${addModalTest.inputCount}`);
|
||||
console.log(` - 下拉选择数量: ${addModalTest.selectCount}`);
|
||||
console.log(` - 姓氏输入框: ${addModalTest.hasLastNameInput}`);
|
||||
console.log(` - 名字输入框: ${addModalTest.hasFirstNameInput}`);
|
||||
console.log(` - 显示名输入框: ${addModalTest.hasDisplayNameInput}`);
|
||||
|
||||
await page.screenshot({ path: 'test_03_add_modal.png', fullPage: true });
|
||||
|
||||
// 关闭弹窗
|
||||
const cancelButton = await page.locator('button:has-text("取消")');
|
||||
if (await cancelButton.isVisible()) {
|
||||
await cancelButton.click();
|
||||
await page.waitForTimeout(1000);
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 测试4:搜索和过滤功能 ====================
|
||||
console.log('\n🔍 测试4:搜索和过滤功能...');
|
||||
|
||||
const searchTest = await page.evaluate(() => {
|
||||
const keywordInput = document.querySelector('input[placeholder*="搜索姓名"]');
|
||||
const cultureSelect = document.querySelectorAll('form .ivu-select')[1];
|
||||
const platformSelect = document.querySelectorAll('form .ivu-select')[2];
|
||||
const qualitySelect = document.querySelectorAll('form .ivu-select')[3];
|
||||
const searchBtn = document.querySelector('button:has-text("搜索")');
|
||||
const resetBtn = document.querySelector('button:has-text("重置")');
|
||||
|
||||
return {
|
||||
hasKeywordInput: !!keywordInput,
|
||||
hasCultureSelect: !!cultureSelect,
|
||||
hasPlatformSelect: !!platformSelect,
|
||||
hasQualitySelect: !!qualitySelect,
|
||||
hasSearchButton: !!searchBtn,
|
||||
hasResetButton: !!resetBtn
|
||||
};
|
||||
});
|
||||
|
||||
testResults.searchFilter = searchTest.hasKeywordInput &&
|
||||
searchTest.hasSearchButton &&
|
||||
searchTest.hasResetButton;
|
||||
|
||||
console.log(`${testResults.searchFilter ? '✅' : '❌'} 搜索过滤测试:`);
|
||||
console.log(` - 关键词输入框: ${searchTest.hasKeywordInput}`);
|
||||
console.log(` - 文化选择器: ${searchTest.hasCultureSelect}`);
|
||||
console.log(` - 平台选择器: ${searchTest.hasPlatformSelect}`);
|
||||
console.log(` - 质量选择器: ${searchTest.hasQualitySelect}`);
|
||||
console.log(` - 搜索按钮: ${searchTest.hasSearchButton}`);
|
||||
console.log(` - 重置按钮: ${searchTest.hasResetButton}`);
|
||||
|
||||
// ==================== 测试5:数据表格 ====================
|
||||
console.log('\n📋 测试5:数据表格显示和操作...');
|
||||
|
||||
const tableTest = await page.evaluate(() => {
|
||||
const table = document.querySelector('table');
|
||||
if (!table) return { hasTable: false };
|
||||
|
||||
const headers = Array.from(table.querySelectorAll('thead th')).map(th => th.textContent?.trim());
|
||||
const rows = table.querySelectorAll('tbody tr');
|
||||
|
||||
const expectedColumns = ['ID', '显示名称', '姓氏', '名字', '文化', '平台', '性别', '质量', '权重', '使用次数', '创建时间', '操作'];
|
||||
const hasAdvancedColumns = expectedColumns.every(col => headers.includes(col));
|
||||
|
||||
return {
|
||||
hasTable: true,
|
||||
headerCount: headers.length,
|
||||
headers: headers,
|
||||
rowCount: rows.length,
|
||||
hasAdvancedColumns: hasAdvancedColumns,
|
||||
expectedColumns: expectedColumns
|
||||
};
|
||||
});
|
||||
|
||||
testResults.tableOperations = tableTest.hasTable && tableTest.hasAdvancedColumns;
|
||||
|
||||
console.log(`${testResults.tableOperations ? '✅' : '❌'} 数据表格测试:`);
|
||||
console.log(` - 表格存在: ${tableTest.hasTable}`);
|
||||
console.log(` - 列数: ${tableTest.headerCount}`);
|
||||
console.log(` - 数据行: ${tableTest.rowCount}`);
|
||||
console.log(` - 高级列完整: ${tableTest.hasAdvancedColumns}`);
|
||||
if (!tableTest.hasAdvancedColumns) {
|
||||
console.log(` - 实际列: ${tableTest.headers.join(', ')}`);
|
||||
console.log(` - 期望列: ${tableTest.expectedColumns.join(', ')}`);
|
||||
}
|
||||
|
||||
await page.screenshot({ path: 'test_04_data_table.png', fullPage: true });
|
||||
|
||||
// ==================== 测试6:API端点 ====================
|
||||
console.log('\n🌐 测试6:API端点访问...');
|
||||
|
||||
// 测试公共API端点
|
||||
const publicApiTests = await Promise.all([
|
||||
fetch('http://localhost:3000/nameTemplate/supportedOptions').then(r => ({ endpoint: 'supportedOptions', status: r.status, ok: r.ok })),
|
||||
fetch('http://localhost:3000/nameTemplate/generatorStatus').then(r => ({ endpoint: 'generatorStatus', status: r.status, ok: r.ok }))
|
||||
].map(p => p.catch(err => ({ error: err.message }))));
|
||||
|
||||
console.log('📡 公共API测试结果:');
|
||||
publicApiTests.forEach(result => {
|
||||
if (result.error) {
|
||||
console.log(` ❌ ${result.endpoint || 'Unknown'}: ${result.error}`);
|
||||
} else {
|
||||
console.log(` ${result.ok ? '✅' : '❌'} ${result.endpoint}: ${result.status}`);
|
||||
testResults.apiEndpoints[result.endpoint] = { status: result.status, success: result.ok };
|
||||
}
|
||||
});
|
||||
|
||||
// ==================== 汇总测试结果 ====================
|
||||
console.log('\n📊 测试结果汇总:');
|
||||
console.log('==========================================');
|
||||
|
||||
const allTests = [
|
||||
{ name: '系统状态面板', result: testResults.statusPanel },
|
||||
{ name: '智能生成功能', result: testResults.generateFunction },
|
||||
{ name: '添加模板功能', result: testResults.addTemplate },
|
||||
{ name: '搜索过滤功能', result: testResults.searchFilter },
|
||||
{ name: '数据表格操作', result: testResults.tableOperations }
|
||||
];
|
||||
|
||||
let passedTests = 0;
|
||||
allTests.forEach(test => {
|
||||
console.log(`${test.result ? '✅' : '❌'} ${test.name}: ${test.result ? '通过' : '失败'}`);
|
||||
if (test.result) passedTests++;
|
||||
});
|
||||
|
||||
console.log('\n🌐 API端点测试:');
|
||||
Object.entries(testResults.apiEndpoints).forEach(([endpoint, result]) => {
|
||||
console.log(`${result.success ? '✅' : '❌'} ${endpoint}: ${result.status}`);
|
||||
});
|
||||
|
||||
const successRate = Math.round((passedTests / allTests.length) * 100);
|
||||
console.log(`\n📈 总体测试通过率: ${passedTests}/${allTests.length} (${successRate}%)`);
|
||||
|
||||
if (successRate < 100) {
|
||||
console.log('\n⚠️ 需要修复的问题:');
|
||||
allTests.forEach(test => {
|
||||
if (!test.result) {
|
||||
console.log(` - ${test.name}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
await page.screenshot({ path: 'test_05_final_result.png', fullPage: true });
|
||||
|
||||
console.log('\n📂 测试截图已保存:');
|
||||
console.log(' - test_01_status_panel.png');
|
||||
console.log(' - test_02_generate_function.png');
|
||||
console.log(' - test_03_add_modal.png');
|
||||
console.log(' - test_04_data_table.png');
|
||||
console.log(' - test_05_final_result.png');
|
||||
|
||||
console.log('\n⏰ 浏览器将保持打开10秒供检查...');
|
||||
await page.waitForTimeout(10000);
|
||||
|
||||
return testResults;
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ 测试失败:', error.message);
|
||||
if (page) {
|
||||
await page.screenshot({ path: 'test_error.png', fullPage: true });
|
||||
}
|
||||
return testResults;
|
||||
} finally {
|
||||
if (browser) {
|
||||
await browser.close();
|
||||
}
|
||||
console.log('🏁 全面测试结束');
|
||||
}
|
||||
}
|
||||
|
||||
// 导出模块和直接运行
|
||||
if (require.main === module) {
|
||||
comprehensiveTest().then(results => {
|
||||
console.log('\n🎯 最终测试结果:', JSON.stringify(results, null, 2));
|
||||
}).catch(console.error);
|
||||
}
|
||||
|
||||
module.exports = comprehensiveTest;
|
||||
228
backend/debug_frontend.js
Normal file
@@ -0,0 +1,228 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* 调试前端页面,查看具体的页面内容和问题
|
||||
*/
|
||||
|
||||
const { chromium } = require('playwright');
|
||||
|
||||
async function debugFrontend() {
|
||||
console.log('🔍 开始调试前端页面...\n');
|
||||
|
||||
let browser;
|
||||
let page;
|
||||
|
||||
try {
|
||||
browser = await chromium.launch({
|
||||
headless: false,
|
||||
slowMo: 1000
|
||||
});
|
||||
|
||||
const context = await browser.newContext({
|
||||
viewport: { width: 1400, height: 900 }
|
||||
});
|
||||
|
||||
page = await context.newPage();
|
||||
|
||||
// 登录
|
||||
console.log('🚀 登录系统...');
|
||||
await page.goto('http://localhost:8891');
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
await page.fill('input[type="text"]', 'admin');
|
||||
await page.fill('input[type="password"]', '111111');
|
||||
await page.click('button:has-text("登录")');
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
// 进入姓名管理
|
||||
console.log('🔍 进入姓名管理页面...');
|
||||
const nameMenu = await page.locator('text=名字管理').first();
|
||||
await nameMenu.click();
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
// 检查当前URL
|
||||
const currentUrl = page.url();
|
||||
console.log(`📍 当前URL: ${currentUrl}`);
|
||||
|
||||
// 获取页面HTML内容
|
||||
console.log('\n📄 页面HTML结构分析...');
|
||||
const pageContent = await page.evaluate(() => {
|
||||
// 获取主要内容区域
|
||||
const mainContent = document.querySelector('main, .main-content, .content, #app > div');
|
||||
if (mainContent) {
|
||||
return {
|
||||
hasContent: true,
|
||||
innerHTML: mainContent.innerHTML.substring(0, 1000), // 前1000字符
|
||||
classes: mainContent.className,
|
||||
children: Array.from(mainContent.children).map(child => ({
|
||||
tagName: child.tagName,
|
||||
className: child.className,
|
||||
id: child.id,
|
||||
textContent: child.textContent ? child.textContent.substring(0, 100) : ''
|
||||
}))
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
hasContent: false,
|
||||
bodyHTML: document.body.innerHTML.substring(0, 1000)
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
console.log('📋 页面内容分析结果:');
|
||||
if (pageContent.hasContent) {
|
||||
console.log(` ✅ 找到主内容区域`);
|
||||
console.log(` 📦 容器类名: ${pageContent.classes}`);
|
||||
console.log(` 🔢 子元素数量: ${pageContent.children.length}`);
|
||||
|
||||
pageContent.children.forEach((child, index) => {
|
||||
console.log(` ${index + 1}. <${child.tagName}> class="${child.className}" - ${child.textContent.substring(0, 50)}...`);
|
||||
});
|
||||
} else {
|
||||
console.log(` ❌ 未找到主内容区域`);
|
||||
console.log(` 📄 Body内容预览: ${pageContent.bodyHTML}...`);
|
||||
}
|
||||
|
||||
// 检查是否有Vue组件
|
||||
console.log('\n🔍 Vue组件检查...');
|
||||
const vueInfo = await page.evaluate(() => {
|
||||
// 检查Vue实例
|
||||
const app = document.getElementById('app');
|
||||
if (app && app.__vue__) {
|
||||
return {
|
||||
hasVue: true,
|
||||
vueVersion: 'Vue 2',
|
||||
componentName: app.__vue__.$options.name || 'unknown'
|
||||
};
|
||||
} else if (app && app._vnode) {
|
||||
return {
|
||||
hasVue: true,
|
||||
vueVersion: 'Vue 3',
|
||||
componentData: 'Vue 3 detected'
|
||||
};
|
||||
} else {
|
||||
return { hasVue: false };
|
||||
}
|
||||
});
|
||||
|
||||
console.log('🔧 Vue状态:');
|
||||
if (vueInfo.hasVue) {
|
||||
console.log(` ✅ Vue检测成功: ${vueInfo.vueVersion}`);
|
||||
if (vueInfo.componentName) {
|
||||
console.log(` 📦 组件名称: ${vueInfo.componentName}`);
|
||||
}
|
||||
} else {
|
||||
console.log(` ❌ 未检测到Vue实例`);
|
||||
}
|
||||
|
||||
// 检查网络请求
|
||||
console.log('\n🌐 检查网络请求...');
|
||||
|
||||
// 手动触发一些API调用来看看数据
|
||||
const apiTest = await page.evaluate(async () => {
|
||||
try {
|
||||
// 测试姓名模板列表API(需要认证)
|
||||
const listResponse = await fetch('http://localhost:3000/nameTemplate/list', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
// 这里需要token,但我们先看看是否会返回401
|
||||
},
|
||||
body: JSON.stringify({})
|
||||
});
|
||||
|
||||
return {
|
||||
listAPI: {
|
||||
status: listResponse.status,
|
||||
statusText: listResponse.statusText,
|
||||
needsAuth: listResponse.status === 401
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
return { error: error.message };
|
||||
}
|
||||
});
|
||||
|
||||
console.log('📡 API测试结果:');
|
||||
if (apiTest.listAPI) {
|
||||
console.log(` - 列表API状态: ${apiTest.listAPI.status} ${apiTest.listAPI.statusText}`);
|
||||
console.log(` - 需要认证: ${apiTest.listAPI.needsAuth ? '是' : '否'}`);
|
||||
}
|
||||
|
||||
// 查找所有可能的页面路由
|
||||
console.log('\n🗺️ 检查路由结构...');
|
||||
const routeInfo = await page.evaluate(() => {
|
||||
// 检查Vue Router
|
||||
const links = Array.from(document.querySelectorAll('a[href*="#"]')).map(a => ({
|
||||
href: a.href,
|
||||
text: a.textContent?.trim()
|
||||
}));
|
||||
|
||||
return {
|
||||
hashLinks: links.slice(0, 10), // 前10个
|
||||
currentHash: window.location.hash
|
||||
};
|
||||
});
|
||||
|
||||
console.log('🔗 路由信息:');
|
||||
console.log(` 📍 当前Hash: ${routeInfo.currentHash}`);
|
||||
console.log(' 🔗 可用路由:');
|
||||
routeInfo.hashLinks.forEach((link, index) => {
|
||||
if (link.text) {
|
||||
console.log(` ${index + 1}. ${link.text} -> ${link.href}`);
|
||||
}
|
||||
});
|
||||
|
||||
// 尝试直接访问firstName页面
|
||||
console.log('\n🎯 尝试直接访问firstName页面...');
|
||||
try {
|
||||
await page.goto('http://localhost:8891/#/nameManage/firstnameList');
|
||||
await page.waitForTimeout(3000);
|
||||
|
||||
await page.screenshot({ path: 'firstname_page_debug.png', fullPage: true });
|
||||
console.log('📸 截图保存: firstname_page_debug.png');
|
||||
|
||||
// 再次检查页面内容
|
||||
const firstnamePageContent = await page.evaluate(() => {
|
||||
const tables = document.querySelectorAll('table');
|
||||
const buttons = document.querySelectorAll('button');
|
||||
const inputs = document.querySelectorAll('input');
|
||||
|
||||
return {
|
||||
tables: tables.length,
|
||||
buttons: Array.from(buttons).map(b => b.textContent?.trim()).filter(t => t),
|
||||
inputs: Array.from(inputs).map(i => i.placeholder || i.type).filter(t => t),
|
||||
hasViewCard: !!document.querySelector('.view-card, view-card'),
|
||||
hasTableList: !!document.querySelector('.table-list, table-list')
|
||||
};
|
||||
});
|
||||
|
||||
console.log('📊 firstName页面内容:');
|
||||
console.log(` - 表格数量: ${firstnamePageContent.tables}`);
|
||||
console.log(` - 按钮: ${firstnamePageContent.buttons.join(', ')}`);
|
||||
console.log(` - 输入框: ${firstnamePageContent.inputs.join(', ')}`);
|
||||
console.log(` - view-card组件: ${firstnamePageContent.hasViewCard ? '存在' : '不存在'}`);
|
||||
console.log(` - table-list组件: ${firstnamePageContent.hasTableList ? '存在' : '不存在'}`);
|
||||
|
||||
} catch (error) {
|
||||
console.log(`❌ 访问firstName页面失败: ${error.message}`);
|
||||
}
|
||||
|
||||
console.log('\n⏰ 浏览器将保持打开10秒供观察...');
|
||||
await page.waitForTimeout(10000);
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ 调试失败:', error.message);
|
||||
} finally {
|
||||
if (browser) {
|
||||
await browser.close();
|
||||
}
|
||||
console.log('🏁 调试结束');
|
||||
}
|
||||
}
|
||||
|
||||
if (require.main === module) {
|
||||
debugFrontend().catch(console.error);
|
||||
}
|
||||
|
||||
module.exports = debugFrontend;
|
||||
29
backend/debug_test.js
Normal file
@@ -0,0 +1,29 @@
|
||||
const axios = require('axios');
|
||||
const qs = require('qs');
|
||||
|
||||
async function testEndpoint() {
|
||||
try {
|
||||
const data = {
|
||||
taskId: 1,
|
||||
targets: [{
|
||||
targetType: 1,
|
||||
targetValue: 'kt66778899',
|
||||
displayName: '测试用户'
|
||||
}]
|
||||
};
|
||||
|
||||
console.log('Data to send:', JSON.stringify(data, null, 2));
|
||||
|
||||
const response = await axios.post('http://localhost:3000/directMessageTarget/test-batchAdd', qs.stringify(data), {
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded'
|
||||
}
|
||||
});
|
||||
|
||||
console.log('Response:', response.data);
|
||||
} catch (error) {
|
||||
console.error('Error:', error.response?.data || error.message);
|
||||
}
|
||||
}
|
||||
|
||||
testEndpoint();
|
||||
BIN
backend/demo_01_login.png
Normal file
|
After Width: | Height: | Size: 140 KiB |
BIN
backend/demo_02_name_page.png
Normal file
|
After Width: | Height: | Size: 69 KiB |
BIN
backend/demo_03_add_modal.png
Normal file
|
After Width: | Height: | Size: 69 KiB |
BIN
backend/demo_04_filled_form.png
Normal file
|
After Width: | Height: | Size: 70 KiB |
BIN
backend/demo_05_search.png
Normal file
|
After Width: | Height: | Size: 69 KiB |
BIN
backend/demo_06_lastname_page.png
Normal file
|
After Width: | Height: | Size: 96 KiB |
BIN
backend/demo_07_final.png
Normal file
|
After Width: | Height: | Size: 96 KiB |
295
backend/demo_new_features.js
Normal file
@@ -0,0 +1,295 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* 新姓名管理系统功能演示脚本
|
||||
* 逐个展示所有新开发的功能
|
||||
*/
|
||||
|
||||
const { chromium } = require('playwright');
|
||||
|
||||
async function demoNewFeatures() {
|
||||
console.log('🎭 开始演示新开发的姓名管理系统功能...\n');
|
||||
|
||||
let browser;
|
||||
let page;
|
||||
|
||||
try {
|
||||
browser = await chromium.launch({
|
||||
headless: false,
|
||||
slowMo: 1500, // 慢一点,便于观察
|
||||
devtools: false
|
||||
});
|
||||
|
||||
const context = await browser.newContext({
|
||||
viewport: { width: 1400, height: 900 }
|
||||
});
|
||||
|
||||
page = await context.newPage();
|
||||
|
||||
// 监听所有姓名相关的API调用
|
||||
page.on('request', request => {
|
||||
const url = request.url();
|
||||
if (url.includes('/nameTemplate/') || url.includes('/firstname/') || url.includes('/lastname/')) {
|
||||
console.log(`🔗 API请求: ${request.method()} ${url}`);
|
||||
}
|
||||
});
|
||||
|
||||
page.on('response', response => {
|
||||
const url = response.url();
|
||||
if (url.includes('/nameTemplate/') || url.includes('/firstname/') || url.includes('/lastname/')) {
|
||||
console.log(`📡 API响应: ${response.status()} ${url}`);
|
||||
if (response.status() !== 200) {
|
||||
console.log(` ⚠️ 状态码: ${response.status()} ${response.statusText()}`);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// ==================== 第1步:登录系统 ====================
|
||||
console.log('🚀 第1步:访问系统并登录...');
|
||||
await page.goto('http://localhost:8891');
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
// 登录
|
||||
await page.fill('input[type="text"]', 'admin');
|
||||
await page.fill('input[type="password"]', '111111');
|
||||
await page.click('button:has-text("登录")');
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
console.log('✅ 登录成功!');
|
||||
await page.screenshot({ path: '01_logged_in.png', fullPage: true });
|
||||
console.log('📸 保存截图: 01_logged_in.png');
|
||||
|
||||
// ==================== 第2步:找到姓名管理菜单 ====================
|
||||
console.log('\n🔍 第2步:找到并点击姓名管理菜单...');
|
||||
|
||||
// 等待菜单加载
|
||||
await page.waitForTimeout(2000);
|
||||
|
||||
// 查找姓名管理菜单
|
||||
const nameMenu = await page.locator('text=名字管理').first();
|
||||
if (await nameMenu.isVisible()) {
|
||||
console.log('✅ 找到"名字管理"菜单');
|
||||
await nameMenu.click();
|
||||
await page.waitForLoadState('networkidle');
|
||||
console.log('🖱️ 点击进入姓名管理');
|
||||
} else {
|
||||
// 尝试其他可能的菜单名称
|
||||
const alternatives = ['text=姓名管理', 'text=姓名', 'text=名字', '[href*="firstname"]'];
|
||||
for (const alt of alternatives) {
|
||||
try {
|
||||
const menu = await page.locator(alt).first();
|
||||
if (await menu.isVisible({ timeout: 1000 })) {
|
||||
console.log(`✅ 找到菜单: ${alt}`);
|
||||
await menu.click();
|
||||
await page.waitForLoadState('networkidle');
|
||||
break;
|
||||
}
|
||||
} catch (e) {
|
||||
// 继续尝试
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await page.screenshot({ path: '02_name_management_page.png', fullPage: true });
|
||||
console.log('📸 保存截图: 02_name_management_page.png');
|
||||
|
||||
// ==================== 第3步:演示新API的调用 ====================
|
||||
console.log('\n🔧 第3步:演示新开发的API接口...');
|
||||
|
||||
// 在浏览器控制台中调用新的API
|
||||
console.log('📡 调用新的supportedOptions API...');
|
||||
const supportedOptions = await page.evaluate(async () => {
|
||||
try {
|
||||
const response = await fetch('http://localhost:3000/nameTemplate/supportedOptions');
|
||||
const data = await response.json();
|
||||
return { status: response.status, data };
|
||||
} catch (error) {
|
||||
return { error: error.message };
|
||||
}
|
||||
});
|
||||
|
||||
console.log('✅ supportedOptions API响应:');
|
||||
console.log(' - 支持的平台:', supportedOptions.data?.data?.platforms?.length || 0, '个');
|
||||
console.log(' - 支持的文化:', supportedOptions.data?.data?.cultures?.length || 0, '个');
|
||||
console.log(' - 数据源类型:', supportedOptions.data?.data?.sources?.length || 0, '个');
|
||||
|
||||
// 调用生成器状态API
|
||||
console.log('\n📡 调用新的generatorStatus API...');
|
||||
const generatorStatus = await page.evaluate(async () => {
|
||||
try {
|
||||
const response = await fetch('http://localhost:3000/nameTemplate/generatorStatus');
|
||||
const data = await response.json();
|
||||
return { status: response.status, data };
|
||||
} catch (error) {
|
||||
return { error: error.message };
|
||||
}
|
||||
});
|
||||
|
||||
console.log('✅ generatorStatus API响应:');
|
||||
if (generatorStatus.data?.data) {
|
||||
Object.keys(generatorStatus.data.data).forEach(generator => {
|
||||
const info = generatorStatus.data.data[generator];
|
||||
console.log(` - ${generator}: ${info.available ? '✅可用' : '❌不可用'} (优先级: ${info.priority})`);
|
||||
});
|
||||
}
|
||||
|
||||
// ==================== 第4步:测试页面功能 ====================
|
||||
console.log('\n🧪 第4步:测试页面功能...');
|
||||
|
||||
// 检查页面元素
|
||||
const hasTable = await page.locator('table').isVisible();
|
||||
const hasAddButton = await page.locator('button:has-text("添加")').isVisible();
|
||||
const hasSearchBox = await page.locator('input[placeholder*="搜索"], input[placeholder*="姓"]').isVisible();
|
||||
|
||||
console.log('📊 页面功能检查:');
|
||||
console.log(` - 数据表格: ${hasTable ? '✅存在' : '❌缺失'}`);
|
||||
console.log(` - 添加按钮: ${hasAddButton ? '✅存在' : '❌缺失'}`);
|
||||
console.log(` - 搜索框: ${hasSearchBox ? '✅存在' : '❌缺失'}`);
|
||||
|
||||
// ==================== 第5步:尝试添加新姓名模板 ====================
|
||||
if (hasAddButton) {
|
||||
console.log('\n➕ 第5步:演示添加新姓名模板功能...');
|
||||
|
||||
try {
|
||||
await page.click('button:has-text("添加")');
|
||||
await page.waitForTimeout(2000);
|
||||
|
||||
// 检查是否有弹窗
|
||||
const hasModal = await page.locator('.modal, .dialog, [class*="modal"]').isVisible();
|
||||
console.log(` - 添加弹窗: ${hasModal ? '✅打开' : '❌未找到'}`);
|
||||
|
||||
if (hasModal) {
|
||||
await page.screenshot({ path: '03_add_modal.png', fullPage: true });
|
||||
console.log('📸 保存截图: 03_add_modal.png');
|
||||
|
||||
// 尝试填写测试数据
|
||||
const nameInput = await page.locator('input[placeholder*="姓"], input[v-model*="name"]').first();
|
||||
if (await nameInput.isVisible()) {
|
||||
await nameInput.fill('测试姓氏');
|
||||
console.log('✏️ 填写测试姓氏');
|
||||
|
||||
await page.waitForTimeout(1000);
|
||||
await page.screenshot({ path: '04_filled_form.png', fullPage: true });
|
||||
console.log('📸 保存截图: 04_filled_form.png');
|
||||
|
||||
// 取消或关闭弹窗(不实际提交)
|
||||
const cancelButton = await page.locator('button:has-text("取消"), button:has-text("关闭")').first();
|
||||
if (await cancelButton.isVisible()) {
|
||||
await cancelButton.click();
|
||||
console.log('❌ 取消添加操作(演示完成)');
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(` ⚠️ 添加功能测试失败: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 第6步:检查数据列表 ====================
|
||||
console.log('\n📋 第6步:检查现有数据列表...');
|
||||
|
||||
if (hasTable) {
|
||||
// 统计表格行数
|
||||
const tableRows = await page.locator('table tbody tr').count();
|
||||
console.log(` - 数据行数: ${tableRows}`);
|
||||
|
||||
if (tableRows > 0) {
|
||||
// 获取表头信息
|
||||
const headers = await page.locator('table thead th').allTextContents();
|
||||
console.log(' - 表格列:', headers.filter(h => h.trim()).join(', '));
|
||||
|
||||
await page.screenshot({ path: '05_data_table.png', fullPage: true });
|
||||
console.log('📸 保存截图: 05_data_table.png');
|
||||
} else {
|
||||
console.log(' ℹ️ 表格为空,可能需要先添加数据');
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 第7步:测试搜索功能 ====================
|
||||
if (hasSearchBox) {
|
||||
console.log('\n🔍 第7步:测试搜索功能...');
|
||||
|
||||
try {
|
||||
const searchInput = await page.locator('input[placeholder*="搜索"], input[placeholder*="姓"]').first();
|
||||
await searchInput.fill('测试');
|
||||
console.log('🔍 输入搜索关键词: 测试');
|
||||
|
||||
// 触发搜索
|
||||
const searchButton = await page.locator('button:has-text("搜索")').first();
|
||||
if (await searchButton.isVisible()) {
|
||||
await searchButton.click();
|
||||
console.log('🔍 点击搜索按钮');
|
||||
await page.waitForTimeout(2000);
|
||||
|
||||
await page.screenshot({ path: '06_search_result.png', fullPage: true });
|
||||
console.log('📸 保存截图: 06_search_result.png');
|
||||
}
|
||||
|
||||
// 清空搜索
|
||||
await searchInput.clear();
|
||||
console.log('🧹 清空搜索条件');
|
||||
} catch (error) {
|
||||
console.log(` ⚠️ 搜索功能测试失败: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 第8步:检查控制台错误 ====================
|
||||
console.log('\n🔍 第8步:检查页面控制台错误...');
|
||||
|
||||
// 收集控制台错误
|
||||
const consoleErrors = [];
|
||||
page.on('console', msg => {
|
||||
if (msg.type() === 'error') {
|
||||
consoleErrors.push(msg.text());
|
||||
}
|
||||
});
|
||||
|
||||
await page.waitForTimeout(2000);
|
||||
|
||||
if (consoleErrors.length > 0) {
|
||||
console.log('⚠️ 发现控制台错误:');
|
||||
consoleErrors.slice(-3).forEach((error, index) => {
|
||||
console.log(` ${index + 1}. ${error}`);
|
||||
});
|
||||
} else {
|
||||
console.log('✅ 无控制台错误');
|
||||
}
|
||||
|
||||
// ==================== 第9步:最终总结截图 ====================
|
||||
console.log('\n📸 第9步:生成最终演示截图...');
|
||||
await page.screenshot({ path: '07_final_demo.png', fullPage: true });
|
||||
console.log('📸 保存截图: 07_final_demo.png');
|
||||
|
||||
// ==================== 演示完成 ====================
|
||||
console.log('\n🎉 新功能演示完成!');
|
||||
console.log('📁 生成的截图文件:');
|
||||
console.log(' 01_logged_in.png - 登录后页面');
|
||||
console.log(' 02_name_management_page.png - 姓名管理页面');
|
||||
console.log(' 03_add_modal.png - 添加模态窗口');
|
||||
console.log(' 04_filled_form.png - 填写表单');
|
||||
console.log(' 05_data_table.png - 数据表格');
|
||||
console.log(' 06_search_result.png - 搜索结果');
|
||||
console.log(' 07_final_demo.png - 最终演示');
|
||||
|
||||
console.log('\n⏰ 浏览器将保持打开10秒供最后观察...');
|
||||
await page.waitForTimeout(10000);
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ 演示失败:', error.message);
|
||||
if (page) {
|
||||
await page.screenshot({ path: 'demo_error.png', fullPage: true });
|
||||
console.log('📸 错误截图: demo_error.png');
|
||||
}
|
||||
} finally {
|
||||
if (browser) {
|
||||
await browser.close();
|
||||
}
|
||||
console.log('🏁 演示结束');
|
||||
}
|
||||
}
|
||||
|
||||
if (require.main === module) {
|
||||
demoNewFeatures().catch(console.error);
|
||||
}
|
||||
|
||||
module.exports = demoNewFeatures;
|
||||
146
backend/direct_name_test.js
Normal file
@@ -0,0 +1,146 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* 直接访问姓名管理页面的测试
|
||||
*/
|
||||
|
||||
const { chromium } = require('playwright');
|
||||
|
||||
async function runDirectNameTest() {
|
||||
console.log('🎭 直接访问姓名管理页面测试...\n');
|
||||
|
||||
let browser;
|
||||
let page;
|
||||
|
||||
try {
|
||||
browser = await chromium.launch({
|
||||
headless: false,
|
||||
slowMo: 1000
|
||||
});
|
||||
|
||||
const context = await browser.newContext({
|
||||
viewport: { width: 1280, height: 800 }
|
||||
});
|
||||
|
||||
page = await context.newPage();
|
||||
|
||||
// 监听API调用
|
||||
page.on('request', request => {
|
||||
const url = request.url();
|
||||
if (url.includes('/nameTemplate/') || url.includes('/firstname/') || url.includes('/lastname/')) {
|
||||
console.log(`📤 API请求: ${request.method()} ${url}`);
|
||||
}
|
||||
});
|
||||
|
||||
page.on('response', response => {
|
||||
const url = response.url();
|
||||
if (url.includes('/nameTemplate/') || url.includes('/firstname/') || url.includes('/lastname/')) {
|
||||
console.log(`📥 API响应: ${response.status()} ${url} - ${response.statusText()}`);
|
||||
}
|
||||
});
|
||||
|
||||
// 直接尝试访问可能的姓名管理页面URL
|
||||
const possibleUrls = [
|
||||
'http://localhost:8891/#/nameManage/firstnameList',
|
||||
'http://localhost:8891/#/nameManage/lastnameList',
|
||||
'http://localhost:8891/#/firstname',
|
||||
'http://localhost:8891/#/lastname',
|
||||
'http://localhost:8891/#/name',
|
||||
'http://localhost:8891/nameManage/firstnameList',
|
||||
'http://localhost:8891/nameManage/lastnameList'
|
||||
];
|
||||
|
||||
for (const url of possibleUrls) {
|
||||
try {
|
||||
console.log(`🌐 尝试访问: ${url}`);
|
||||
await page.goto(url, {
|
||||
waitUntil: 'networkidle',
|
||||
timeout: 10000
|
||||
});
|
||||
|
||||
// 等待页面加载
|
||||
await page.waitForTimeout(3000);
|
||||
|
||||
// 检查页面内容
|
||||
const title = await page.title();
|
||||
const hasError = await page.$('text=404') !== null || await page.$('text=Not Found') !== null;
|
||||
const hasLogin = await page.$('text=登录') !== null || await page.$('text=Login') !== null;
|
||||
const hasNameContent = await page.$('text=姓氏') !== null || await page.$('text=姓名') !== null || await page.$('text=名字') !== null;
|
||||
|
||||
console.log(`📝 页面检查结果:`);
|
||||
console.log(` 标题: ${title}`);
|
||||
console.log(` 有错误页面: ${hasError}`);
|
||||
console.log(` 需要登录: ${hasLogin}`);
|
||||
console.log(` 包含姓名内容: ${hasNameContent}`);
|
||||
|
||||
if (!hasError && !hasLogin && hasNameContent) {
|
||||
console.log('✅ 成功找到姓名管理页面!');
|
||||
|
||||
// 截取页面
|
||||
await page.screenshot({
|
||||
path: `name_page_${Date.now()}.png`,
|
||||
fullPage: true
|
||||
});
|
||||
|
||||
// 检查页面具体内容
|
||||
const hasTable = await page.$('table') !== null;
|
||||
const hasAddButton = await page.$('button:has-text("添加")') !== null;
|
||||
const hasSearchBox = await page.$('input[placeholder*="姓"]') !== null;
|
||||
|
||||
console.log(`📊 页面功能检查:`);
|
||||
console.log(` - 数据表格: ${hasTable}`);
|
||||
console.log(` - 添加按钮: ${hasAddButton}`);
|
||||
console.log(` - 搜索框: ${hasSearchBox}`);
|
||||
|
||||
break;
|
||||
} else if (hasLogin) {
|
||||
console.log('⚠️ 页面需要登录');
|
||||
} else if (hasError) {
|
||||
console.log('❌ 页面不存在');
|
||||
} else {
|
||||
console.log('🔍 页面内容不明确,继续尝试其他URL');
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.log(`❌ 访问失败: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// 尝试直接调用API
|
||||
console.log('\n🔧 直接测试API调用...');
|
||||
|
||||
try {
|
||||
// 尝试调用无需认证的API
|
||||
const response = await page.evaluate(async () => {
|
||||
try {
|
||||
const res = await fetch('http://localhost:3000/nameTemplate/supportedOptions');
|
||||
const data = await res.json();
|
||||
return { status: res.status, data };
|
||||
} catch (error) {
|
||||
return { error: error.message };
|
||||
}
|
||||
});
|
||||
|
||||
console.log('📡 API调用结果:', JSON.stringify(response, null, 2));
|
||||
} catch (apiError) {
|
||||
console.log(`❌ API调用失败: ${apiError.message}`);
|
||||
}
|
||||
|
||||
// 等待观察
|
||||
await page.waitForTimeout(5000);
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ 测试失败:', error.message);
|
||||
} finally {
|
||||
if (browser) {
|
||||
await browser.close();
|
||||
}
|
||||
console.log('\n🎭 直接访问测试完成');
|
||||
}
|
||||
}
|
||||
|
||||
if (require.main === module) {
|
||||
runDirectNameTest().catch(console.error);
|
||||
}
|
||||
|
||||
module.exports = runDirectNameTest;
|
||||
BIN
backend/final_page.png
Normal file
|
After Width: | Height: | Size: 106 KiB |
BIN
backend/firstname_page_debug.png
Normal file
|
After Width: | Height: | Size: 64 KiB |
56
backend/init-proxy-platforms.js
Normal file
@@ -0,0 +1,56 @@
|
||||
require('module-alias/register');
|
||||
const Db = require("@src/config/Db");
|
||||
const Redis = require("redis");
|
||||
const RedisUtil = require("@src/util/RedisUtil");
|
||||
const Config = require("@src/config/Config");
|
||||
|
||||
const init = async () => {
|
||||
try {
|
||||
console.log("初始化代理平台数据...");
|
||||
|
||||
// 初始化数据库连接
|
||||
await Db.getInstance();
|
||||
|
||||
// 初始化Redis连接
|
||||
const redisClient = Redis.createClient({
|
||||
host: 'localhost',
|
||||
port: 6379,
|
||||
db: 6,
|
||||
password: Config.isDev ? undefined : Config.redisPassword
|
||||
});
|
||||
|
||||
redisClient.on('error', (err) => {
|
||||
console.error('Redis连接错误:', err);
|
||||
});
|
||||
|
||||
// 设置Redis实例
|
||||
RedisUtil.getInstance(redisClient);
|
||||
|
||||
// 等待连接稳定
|
||||
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||
|
||||
// 动态加载服务,确保Redis已经初始化
|
||||
const MProxyPlatformService = require("@src/service/MProxyPlatformService");
|
||||
const proxyPlatformService = MProxyPlatformService.getInstance();
|
||||
|
||||
// 确保表存在
|
||||
await proxyPlatformService.getModel().sync({ alter: true });
|
||||
|
||||
// 初始化默认平台数据
|
||||
await proxyPlatformService.initializeDefaultPlatforms();
|
||||
|
||||
console.log("代理平台数据初始化完成!");
|
||||
|
||||
// 关闭Redis连接
|
||||
redisClient.quit();
|
||||
|
||||
setTimeout(() => {
|
||||
process.exit(0);
|
||||
}, 1000);
|
||||
} catch (error) {
|
||||
console.error("初始化失败:", error);
|
||||
process.exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
init();
|
||||
75
backend/init-rola-ip-platform.js
Normal file
@@ -0,0 +1,75 @@
|
||||
/**
|
||||
* 初始化Rola-IP代理平台数据
|
||||
* 为系统添加Rola-IP平台配置示例
|
||||
*/
|
||||
|
||||
const sequelize = require('./src/config/sequelize')
|
||||
const MProxyPlatform = require('./src/modes/MProxyPlatform')
|
||||
|
||||
async function initRolaIPPlatform() {
|
||||
try {
|
||||
console.log('开始初始化Rola-IP代理平台数据...')
|
||||
|
||||
// 检查是否已存在Rola-IP平台配置
|
||||
const existingPlatform = await MProxyPlatform.findOne({
|
||||
where: { platform: 'rola-ip' }
|
||||
})
|
||||
|
||||
if (existingPlatform) {
|
||||
console.log('Rola-IP平台配置已存在,跳过初始化')
|
||||
return
|
||||
}
|
||||
|
||||
// 创建Rola-IP平台配置示例
|
||||
const rolaipConfig = {
|
||||
platform: 'rola-ip',
|
||||
displayName: 'Rola-IP代理平台',
|
||||
description: '专业代理IP服务平台,支持住宅IP、数据中心IP、移动IP等多种类型',
|
||||
apiUrl: 'https://admin.rola-ip.co',
|
||||
authType: 'userPass',
|
||||
username: '', // 需要用户填写
|
||||
password: '', // 需要用户填写
|
||||
apiKey: '',
|
||||
token: '',
|
||||
proxyTypes: 'residential,datacenter,mobile,static_residential,ipv6',
|
||||
countries: 'US,UK,DE,FR,JP,KR,AU,CA,BR,IN,SG,HK,TW,RU,NL',
|
||||
concurrentLimit: 100,
|
||||
rotationInterval: 300, // 5分钟
|
||||
connectionTimeout: 30000,
|
||||
retryCount: 3,
|
||||
remark: '支持多种代理类型和15个国家/地区,提供住宅IP、数据中心IP、移动IP、静态住宅IP和IPv6代理服务',
|
||||
isEnabled: false, // 默认禁用,需要用户配置后启用
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date()
|
||||
}
|
||||
|
||||
await MProxyPlatform.create(rolaipConfig)
|
||||
|
||||
console.log('✅ Rola-IP代理平台初始化完成')
|
||||
console.log('📝 平台信息:')
|
||||
console.log(` - 平台名称: ${rolaipConfig.displayName}`)
|
||||
console.log(` - API地址: ${rolaipConfig.apiUrl}`)
|
||||
console.log(` - 支持类型: ${rolaipConfig.proxyTypes}`)
|
||||
console.log(` - 支持地区: ${rolaipConfig.countries}`)
|
||||
console.log('⚠️ 注意: 需要在前端界面配置用户名和密码后启用')
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ 初始化Rola-IP平台数据失败:', error.message)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// 如果直接运行此脚本
|
||||
if (require.main === module) {
|
||||
initRolaIPPlatform()
|
||||
.then(() => {
|
||||
console.log('🎉 Rola-IP平台初始化脚本执行完成')
|
||||
process.exit(0)
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('💥 初始化脚本执行失败:', error)
|
||||
process.exit(1)
|
||||
})
|
||||
}
|
||||
|
||||
module.exports = initRolaIPPlatform
|
||||
9
backend/jsconfig.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"baseUrl": "./",
|
||||
"paths": {
|
||||
"@src/*": ["src/*"]
|
||||
}
|
||||
},
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
129
backend/live_test.js
Normal file
@@ -0,0 +1,129 @@
|
||||
const { chromium } = require('playwright');
|
||||
|
||||
async function liveTest() {
|
||||
console.log('🚀 启动实时测试...');
|
||||
|
||||
const browser = await chromium.launch({
|
||||
headless: false,
|
||||
slowMo: 500, // 稍微慢一点,便于观察
|
||||
devtools: true // 打开开发者工具
|
||||
});
|
||||
|
||||
const page = await browser.newPage();
|
||||
|
||||
// 监听控制台输出
|
||||
page.on('console', msg => console.log('🔍 页面日志:', msg.text()));
|
||||
|
||||
try {
|
||||
console.log('📱 访问前端页面...');
|
||||
await page.goto('http://localhost:8891');
|
||||
|
||||
console.log('⏱️ 等待页面加载...');
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
console.log('📸 截取登录页面...');
|
||||
await page.screenshot({ path: 'login_page.png' });
|
||||
|
||||
console.log('🔍 查找登录元素...');
|
||||
|
||||
// 查找用户名输入框
|
||||
const usernameInput = await page.locator('input[type="text"], input[placeholder*="用户"], input[placeholder*="账号"]').first();
|
||||
if (await usernameInput.isVisible()) {
|
||||
console.log('✅ 找到用户名输入框');
|
||||
await usernameInput.fill('admin');
|
||||
console.log('✏️ 填写用户名: admin');
|
||||
}
|
||||
|
||||
// 查找密码输入框
|
||||
const passwordInput = await page.locator('input[type="password"]').first();
|
||||
if (await passwordInput.isVisible()) {
|
||||
console.log('✅ 找到密码输入框');
|
||||
await passwordInput.fill('111111');
|
||||
console.log('🔐 填写密码: 111111');
|
||||
}
|
||||
|
||||
// 查找登录按钮
|
||||
const loginButton = await page.locator('button:has-text("登录"), .login-btn, button[type="submit"]').first();
|
||||
if (await loginButton.isVisible()) {
|
||||
console.log('✅ 找到登录按钮,准备点击...');
|
||||
await loginButton.click();
|
||||
console.log('🖱️ 点击登录按钮');
|
||||
}
|
||||
|
||||
console.log('⏱️ 等待登录处理...');
|
||||
await page.waitForTimeout(5000);
|
||||
|
||||
console.log('📸 截取登录后页面...');
|
||||
await page.screenshot({ path: 'after_login.png' });
|
||||
|
||||
// 检查是否登录成功
|
||||
const currentUrl = page.url();
|
||||
console.log(`📍 当前URL: ${currentUrl}`);
|
||||
|
||||
if (!currentUrl.includes('login')) {
|
||||
console.log('🎉 登录成功!');
|
||||
|
||||
console.log('🔍 查找姓名管理菜单...');
|
||||
|
||||
// 尝试多种选择器
|
||||
const menuSelectors = [
|
||||
'text=姓名管理',
|
||||
'text=名字管理',
|
||||
'text=姓名',
|
||||
'text=名字',
|
||||
'a[href*="firstname"]',
|
||||
'a[href*="lastname"]',
|
||||
'.menu-item:has-text("姓名")',
|
||||
'.menu-item:has-text("名字")'
|
||||
];
|
||||
|
||||
let found = false;
|
||||
for (const selector of menuSelectors) {
|
||||
try {
|
||||
const element = page.locator(selector).first();
|
||||
if (await element.isVisible({ timeout: 1000 })) {
|
||||
console.log(`✅ 找到菜单项: ${selector}`);
|
||||
await element.click();
|
||||
console.log('🖱️ 点击菜单项');
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
} catch (e) {
|
||||
// 继续尝试下一个
|
||||
}
|
||||
}
|
||||
|
||||
if (!found) {
|
||||
console.log('🔍 未找到明确的姓名管理菜单,查找所有菜单项...');
|
||||
const allMenus = await page.locator('a, .menu-item').all();
|
||||
console.log(`📋 找到 ${allMenus.length} 个菜单项`);
|
||||
|
||||
for (let i = 0; i < Math.min(allMenus.length, 10); i++) {
|
||||
const text = await allMenus[i].textContent();
|
||||
if (text && text.trim()) {
|
||||
console.log(` ${i+1}. "${text.trim()}"`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await page.waitForTimeout(3000);
|
||||
console.log('📸 截取最终页面...');
|
||||
await page.screenshot({ path: 'final_page.png' });
|
||||
|
||||
} else {
|
||||
console.log('❌ 登录可能失败,仍在登录页面');
|
||||
}
|
||||
|
||||
console.log('🎭 测试完成,浏览器将保持打开5秒供观察...');
|
||||
await page.waitForTimeout(5000);
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ 测试过程中出错:', error.message);
|
||||
await page.screenshot({ path: 'error_page.png' });
|
||||
} finally {
|
||||
await browser.close();
|
||||
console.log('🏁 测试结束');
|
||||
}
|
||||
}
|
||||
|
||||
liveTest();
|
||||
BIN
backend/login_page.png
Normal file
|
After Width: | Height: | Size: 1.3 MiB |
365
backend/migrations/20241228_create_account_pool_tables.js
Normal file
@@ -0,0 +1,365 @@
|
||||
// 创建账号池相关表的迁移脚本
|
||||
const { DataTypes, QueryInterface } = require('sequelize');
|
||||
|
||||
module.exports = {
|
||||
up: async (queryInterface, Sequelize) => {
|
||||
// 创建账号池主表
|
||||
await queryInterface.createTable('accounts_pool', {
|
||||
id: {
|
||||
type: DataTypes.INTEGER,
|
||||
primaryKey: true,
|
||||
allowNull: false,
|
||||
autoIncrement: true,
|
||||
comment: "主键ID"
|
||||
},
|
||||
accountId: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
unique: true,
|
||||
comment: "关联的TG账号ID (accounts表)"
|
||||
},
|
||||
phone: {
|
||||
type: DataTypes.STRING(50),
|
||||
allowNull: false,
|
||||
unique: true,
|
||||
comment: "手机号码"
|
||||
},
|
||||
status: {
|
||||
type: DataTypes.ENUM('active', 'warning', 'limited', 'banned', 'cooling', 'inactive'),
|
||||
defaultValue: 'active',
|
||||
comment: "账号状态"
|
||||
},
|
||||
tier: {
|
||||
type: DataTypes.ENUM('new', 'warming', 'normal', 'trusted', 'vip'),
|
||||
defaultValue: 'new',
|
||||
comment: "账号分级"
|
||||
},
|
||||
dailyLimit: {
|
||||
type: DataTypes.INTEGER,
|
||||
defaultValue: 30,
|
||||
comment: "每日发送限制"
|
||||
},
|
||||
hourlyLimit: {
|
||||
type: DataTypes.INTEGER,
|
||||
defaultValue: 5,
|
||||
comment: "每小时发送限制"
|
||||
},
|
||||
intervalSeconds: {
|
||||
type: DataTypes.INTEGER,
|
||||
defaultValue: 120,
|
||||
comment: "发送间隔(秒)"
|
||||
},
|
||||
lastActiveTime: {
|
||||
type: DataTypes.DATE,
|
||||
comment: "最后活跃时间"
|
||||
},
|
||||
joinedGroupCount: {
|
||||
type: DataTypes.INTEGER,
|
||||
defaultValue: 0,
|
||||
comment: "已加入群组数"
|
||||
},
|
||||
totalSentCount: {
|
||||
type: DataTypes.INTEGER,
|
||||
defaultValue: 0,
|
||||
comment: "累计发送数"
|
||||
},
|
||||
todaySentCount: {
|
||||
type: DataTypes.INTEGER,
|
||||
defaultValue: 0,
|
||||
comment: "今日发送数"
|
||||
},
|
||||
consecutiveFailures: {
|
||||
type: DataTypes.INTEGER,
|
||||
defaultValue: 0,
|
||||
comment: "连续失败次数"
|
||||
},
|
||||
riskScore: {
|
||||
type: DataTypes.FLOAT,
|
||||
defaultValue: 0,
|
||||
comment: "风险评分(0-100)"
|
||||
},
|
||||
priority: {
|
||||
type: DataTypes.INTEGER,
|
||||
defaultValue: 50,
|
||||
comment: "调度优先级(0-100)"
|
||||
},
|
||||
tags: {
|
||||
type: DataTypes.JSON,
|
||||
defaultValue: [],
|
||||
comment: "标签数组"
|
||||
},
|
||||
metadata: {
|
||||
type: DataTypes.JSON,
|
||||
defaultValue: {},
|
||||
comment: "扩展元数据"
|
||||
},
|
||||
isActive: {
|
||||
type: DataTypes.BOOLEAN,
|
||||
defaultValue: true,
|
||||
comment: "是否启用"
|
||||
},
|
||||
createdAt: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: false
|
||||
},
|
||||
updatedAt: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: false
|
||||
}
|
||||
});
|
||||
|
||||
// 创建索引
|
||||
await queryInterface.addIndex('accounts_pool', ['status', 'isActive']);
|
||||
await queryInterface.addIndex('accounts_pool', ['tier']);
|
||||
await queryInterface.addIndex('accounts_pool', ['riskScore']);
|
||||
await queryInterface.addIndex('accounts_pool', ['lastActiveTime']);
|
||||
|
||||
// 创建账号健康度表
|
||||
await queryInterface.createTable('accounts_health', {
|
||||
id: {
|
||||
type: DataTypes.INTEGER,
|
||||
primaryKey: true,
|
||||
allowNull: false,
|
||||
autoIncrement: true,
|
||||
comment: "主键ID"
|
||||
},
|
||||
accountPoolId: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
comment: "关联的账号池ID"
|
||||
},
|
||||
healthScore: {
|
||||
type: DataTypes.FLOAT,
|
||||
defaultValue: 100,
|
||||
comment: "健康度评分(0-100)"
|
||||
},
|
||||
responseTime: {
|
||||
type: DataTypes.FLOAT,
|
||||
comment: "平均响应时间(ms)"
|
||||
},
|
||||
successRate: {
|
||||
type: DataTypes.FLOAT,
|
||||
defaultValue: 100,
|
||||
comment: "成功率百分比"
|
||||
},
|
||||
errorCount: {
|
||||
type: DataTypes.INTEGER,
|
||||
defaultValue: 0,
|
||||
comment: "错误次数"
|
||||
},
|
||||
warningCount: {
|
||||
type: DataTypes.INTEGER,
|
||||
defaultValue: 0,
|
||||
comment: "警告次数"
|
||||
},
|
||||
dailyUsageRate: {
|
||||
type: DataTypes.FLOAT,
|
||||
defaultValue: 0,
|
||||
comment: "日使用率百分比"
|
||||
},
|
||||
weeklyUsageRate: {
|
||||
type: DataTypes.FLOAT,
|
||||
defaultValue: 0,
|
||||
comment: "周使用率百分比"
|
||||
},
|
||||
restDuration: {
|
||||
type: DataTypes.INTEGER,
|
||||
defaultValue: 0,
|
||||
comment: "休息时长(分钟)"
|
||||
},
|
||||
spamReportCount: {
|
||||
type: DataTypes.INTEGER,
|
||||
defaultValue: 0,
|
||||
comment: "垃圾信息举报次数"
|
||||
},
|
||||
blockCount: {
|
||||
type: DataTypes.INTEGER,
|
||||
defaultValue: 0,
|
||||
comment: "被拉黑次数"
|
||||
},
|
||||
restrictionCount: {
|
||||
type: DataTypes.INTEGER,
|
||||
defaultValue: 0,
|
||||
comment: "被限制次数"
|
||||
},
|
||||
anomalyScore: {
|
||||
type: DataTypes.FLOAT,
|
||||
defaultValue: 0,
|
||||
comment: "异常行为评分"
|
||||
},
|
||||
activeHours: {
|
||||
type: DataTypes.JSON,
|
||||
defaultValue: [],
|
||||
comment: "活跃时段分布"
|
||||
},
|
||||
engagementRate: {
|
||||
type: DataTypes.FLOAT,
|
||||
defaultValue: 0,
|
||||
comment: "互动率"
|
||||
},
|
||||
evaluationDetails: {
|
||||
type: DataTypes.JSON,
|
||||
defaultValue: {},
|
||||
comment: "评估详细信息"
|
||||
},
|
||||
lastEvaluationTime: {
|
||||
type: DataTypes.DATE,
|
||||
defaultValue: Sequelize.NOW,
|
||||
comment: "最后评估时间"
|
||||
},
|
||||
trend: {
|
||||
type: DataTypes.ENUM('improving', 'stable', 'declining'),
|
||||
defaultValue: 'stable',
|
||||
comment: "健康度趋势"
|
||||
},
|
||||
recommendations: {
|
||||
type: DataTypes.JSON,
|
||||
defaultValue: [],
|
||||
comment: "改善建议"
|
||||
},
|
||||
createdAt: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: false
|
||||
},
|
||||
updatedAt: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: false
|
||||
}
|
||||
});
|
||||
|
||||
// 创建索引
|
||||
await queryInterface.addIndex('accounts_health', ['accountPoolId']);
|
||||
await queryInterface.addIndex('accounts_health', ['healthScore']);
|
||||
await queryInterface.addIndex('accounts_health', ['lastEvaluationTime']);
|
||||
|
||||
// 创建账号使用记录表
|
||||
await queryInterface.createTable('accounts_usage_log', {
|
||||
id: {
|
||||
type: DataTypes.INTEGER,
|
||||
primaryKey: true,
|
||||
allowNull: false,
|
||||
autoIncrement: true,
|
||||
comment: "主键ID"
|
||||
},
|
||||
accountPoolId: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
comment: "关联的账号池ID"
|
||||
},
|
||||
taskId: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
comment: "关联的任务ID"
|
||||
},
|
||||
taskType: {
|
||||
type: DataTypes.ENUM('group_send', 'private_send', 'join_group', 'leave_group', 'other'),
|
||||
defaultValue: 'group_send',
|
||||
comment: "任务类型"
|
||||
},
|
||||
groupId: {
|
||||
type: DataTypes.INTEGER,
|
||||
comment: "群组ID(如果是群发任务)"
|
||||
},
|
||||
messageContent: {
|
||||
type: DataTypes.TEXT,
|
||||
comment: "发送的消息内容"
|
||||
},
|
||||
status: {
|
||||
type: DataTypes.ENUM('pending', 'success', 'failed', 'timeout', 'cancelled'),
|
||||
defaultValue: 'pending',
|
||||
comment: "执行状态"
|
||||
},
|
||||
errorCode: {
|
||||
type: DataTypes.STRING(50),
|
||||
comment: "错误代码"
|
||||
},
|
||||
errorMessage: {
|
||||
type: DataTypes.STRING(500),
|
||||
comment: "错误信息"
|
||||
},
|
||||
retryCount: {
|
||||
type: DataTypes.INTEGER,
|
||||
defaultValue: 0,
|
||||
comment: "重试次数"
|
||||
},
|
||||
startTime: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: false,
|
||||
comment: "开始时间"
|
||||
},
|
||||
endTime: {
|
||||
type: DataTypes.DATE,
|
||||
comment: "结束时间"
|
||||
},
|
||||
duration: {
|
||||
type: DataTypes.INTEGER,
|
||||
comment: "执行时长(毫秒)"
|
||||
},
|
||||
riskLevel: {
|
||||
type: DataTypes.ENUM('low', 'medium', 'high', 'critical'),
|
||||
defaultValue: 'low',
|
||||
comment: "风险级别"
|
||||
},
|
||||
behaviorSimulation: {
|
||||
type: DataTypes.JSON,
|
||||
defaultValue: {},
|
||||
comment: "行为模拟参数"
|
||||
},
|
||||
ipAddress: {
|
||||
type: DataTypes.STRING(50),
|
||||
comment: "使用的IP地址"
|
||||
},
|
||||
deviceFingerprint: {
|
||||
type: DataTypes.STRING(100),
|
||||
comment: "设备指纹"
|
||||
},
|
||||
recipientCount: {
|
||||
type: DataTypes.INTEGER,
|
||||
defaultValue: 0,
|
||||
comment: "接收者数量"
|
||||
},
|
||||
readCount: {
|
||||
type: DataTypes.INTEGER,
|
||||
defaultValue: 0,
|
||||
comment: "已读数量"
|
||||
},
|
||||
replyCount: {
|
||||
type: DataTypes.INTEGER,
|
||||
defaultValue: 0,
|
||||
comment: "回复数量"
|
||||
},
|
||||
reportCount: {
|
||||
type: DataTypes.INTEGER,
|
||||
defaultValue: 0,
|
||||
comment: "举报数量"
|
||||
},
|
||||
metadata: {
|
||||
type: DataTypes.JSON,
|
||||
defaultValue: {},
|
||||
comment: "其他元数据"
|
||||
},
|
||||
createdAt: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: false
|
||||
},
|
||||
updatedAt: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: false
|
||||
}
|
||||
});
|
||||
|
||||
// 创建索引
|
||||
await queryInterface.addIndex('accounts_usage_log', ['accountPoolId']);
|
||||
await queryInterface.addIndex('accounts_usage_log', ['taskId']);
|
||||
await queryInterface.addIndex('accounts_usage_log', ['startTime', 'endTime']);
|
||||
await queryInterface.addIndex('accounts_usage_log', ['status']);
|
||||
await queryInterface.addIndex('accounts_usage_log', ['riskLevel']);
|
||||
},
|
||||
|
||||
down: async (queryInterface, Sequelize) => {
|
||||
// 删除表(按相反顺序)
|
||||
await queryInterface.dropTable('accounts_usage_log');
|
||||
await queryInterface.dropTable('accounts_health');
|
||||
await queryInterface.dropTable('accounts_pool');
|
||||
}
|
||||
};
|
||||
14
backend/package-telegram-web.json
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"name": "telegram-web-server",
|
||||
"version": "1.0.0",
|
||||
"description": "Telegram Web Server for integration",
|
||||
"main": "telegram-web-server.js",
|
||||
"scripts": {
|
||||
"start": "node telegram-web-server.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"express": "^4.18.2",
|
||||
"http-proxy-middleware": "^2.0.6",
|
||||
"body-parser": "^1.20.2"
|
||||
}
|
||||
}
|
||||
68
backend/package.json
Normal file
@@ -0,0 +1,68 @@
|
||||
{
|
||||
"name": "tgmanage",
|
||||
"version": "1.6.6",
|
||||
"description": "666",
|
||||
"main": "src/Server.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1",
|
||||
"start": "node src/Server.js",
|
||||
"watch": "onchange -i -k '**/*.js' -- npm run start"
|
||||
},
|
||||
"author": "cool guy",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@alicloud/alimt20181012": "^1.0.3",
|
||||
"@alicloud/openapi-client": "^0.4.15",
|
||||
"@cryptography/aes": "^0.1.1",
|
||||
"@hapi/hapi": "^21.4.0",
|
||||
"amqplib": "^0.8.0",
|
||||
"async-mutex": "^0.5.0",
|
||||
"axios": "^1.11.0",
|
||||
"big-integer": "^1.6.52",
|
||||
"browser-or-node": "^1.3.0",
|
||||
"bull": "^4.16.5",
|
||||
"cheerio": "^1.0.0-rc.10",
|
||||
"cron": "^2.4.4",
|
||||
"compressing": "^1.5.1",
|
||||
"crypto": "^1.0.1",
|
||||
"hapi-mongodb": "^10.0.1",
|
||||
"hapi-redis2": "^3.0.1",
|
||||
"htmlparser2": "^10.0.0",
|
||||
"joi": "^17.6.0",
|
||||
"js-cookie": "^3.0.5",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"log4js": "^6.4.6",
|
||||
"module-alias": "^2.2.2",
|
||||
"mysql2": "^3.14.2",
|
||||
"node-schedule": "^2.1.0",
|
||||
"onchange": "^7.1.0",
|
||||
"pako": "^2.1.0",
|
||||
"playwright": "^1.54.2",
|
||||
"qrcode": "^1.5.4",
|
||||
"qs": "^6.14.0",
|
||||
"sequelize": "^6.19.0",
|
||||
"socket.io": "^4.5.0",
|
||||
"socks-proxy-agent": "^8.0.5",
|
||||
"store2": "^2.12.0",
|
||||
"telegram": "^2.26.22",
|
||||
"tencentcloud-sdk-nodejs": "^4.0.334",
|
||||
"ts-custom-error": "^3.2.0",
|
||||
"ts-mixer": "^5.4.1",
|
||||
"uuid": "^11.1.0",
|
||||
"wait-until": "^0.0.2",
|
||||
"wangeditor": "^4.7.15",
|
||||
"websocket": "^1.0.34",
|
||||
"xlsx": "^0.18.5"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"bufferutil": "^4.0.3",
|
||||
"utf-8-validate": "^5.0.5"
|
||||
},
|
||||
"directories": {
|
||||
"test": "test"
|
||||
},
|
||||
"keywords": [],
|
||||
"_moduleAliases": {
|
||||
"@src": "src"
|
||||
}
|
||||
}
|
||||
249
backend/playwright_name_test.js
Normal file
@@ -0,0 +1,249 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Playwright 姓名管理专项测试 - 重点检查姓名管理功能
|
||||
*/
|
||||
|
||||
const { chromium } = require('playwright');
|
||||
|
||||
async function runNameManagementTest() {
|
||||
console.log('🎭 启动姓名管理专项测试...\n');
|
||||
|
||||
let browser;
|
||||
let page;
|
||||
|
||||
try {
|
||||
// 启动浏览器
|
||||
browser = await chromium.launch({
|
||||
headless: false, // 设置为false以便观察
|
||||
slowMo: 1000 // 减慢操作速度以便观察
|
||||
});
|
||||
|
||||
const context = await browser.newContext({
|
||||
viewport: { width: 1280, height: 800 }
|
||||
});
|
||||
|
||||
page = await context.newPage();
|
||||
|
||||
// 设置请求监听,捕获API调用
|
||||
page.on('request', request => {
|
||||
const url = request.url();
|
||||
if (url.includes('/nameTemplate/') || url.includes('/firstname/') || url.includes('/lastname/')) {
|
||||
console.log(`📤 姓名API请求: ${request.method()} ${url}`);
|
||||
}
|
||||
});
|
||||
|
||||
page.on('response', response => {
|
||||
const url = response.url();
|
||||
if (url.includes('/nameTemplate/') || url.includes('/firstname/') || url.includes('/lastname/')) {
|
||||
console.log(`📥 姓名API响应: ${response.status()} ${url}`);
|
||||
}
|
||||
});
|
||||
|
||||
// 访问前端登录页面
|
||||
console.log('🌐 正在访问前端登录页面...');
|
||||
await page.goto('http://localhost:8891', {
|
||||
waitUntil: 'networkidle',
|
||||
timeout: 15000
|
||||
});
|
||||
console.log('✅ 前端页面加载成功');
|
||||
|
||||
// 等待页面稳定
|
||||
await page.waitForTimeout(3000);
|
||||
|
||||
// 查找登录表单
|
||||
console.log('🔍 查找登录表单...');
|
||||
const hasLoginForm = await page.$('form') !== null;
|
||||
const hasUsernameInput = await page.$('input[type="text"], input[placeholder*="用户"], input[placeholder*="账号"]') !== null;
|
||||
const hasPasswordInput = await page.$('input[type="password"], input[placeholder*="密码"]') !== null;
|
||||
|
||||
console.log(`📝 登录表单检查:`);
|
||||
console.log(` - 包含表单: ${hasLoginForm}`);
|
||||
console.log(` - 包含用户名输入: ${hasUsernameInput}`);
|
||||
console.log(` - 包含密码输入: ${hasPasswordInput}`);
|
||||
|
||||
if (hasUsernameInput && hasPasswordInput) {
|
||||
try {
|
||||
console.log('🔐 尝试登录...');
|
||||
|
||||
// 填写登录信息(使用系统默认admin账户)
|
||||
const usernameInput = await page.$('input[type="text"], input[placeholder*="用户"], input[placeholder*="账号"]');
|
||||
const passwordInput = await page.$('input[type="password"], input[placeholder*="密码"]');
|
||||
|
||||
if (usernameInput && passwordInput) {
|
||||
await usernameInput.fill('admin');
|
||||
await passwordInput.fill('111111'); // 根据CLAUDE.md中的系统账号信息
|
||||
|
||||
// 查找并点击登录按钮
|
||||
const loginButton = await page.$('button[type="submit"], button:has-text("登录"), .login-btn, [class*="login"]');
|
||||
if (loginButton) {
|
||||
await loginButton.click();
|
||||
console.log('✅ 点击登录按钮');
|
||||
|
||||
// 等待登录完成
|
||||
await page.waitForTimeout(5000);
|
||||
|
||||
// 检查是否登录成功(通过URL变化或页面内容变化)
|
||||
const currentUrl = page.url();
|
||||
const isLoggedIn = !currentUrl.includes('login') && !currentUrl.includes('Login');
|
||||
|
||||
if (isLoggedIn) {
|
||||
console.log('✅ 登录成功!');
|
||||
console.log(`📍 当前页面: ${currentUrl}`);
|
||||
|
||||
// 截取登录后的页面
|
||||
await page.screenshot({
|
||||
path: 'after_login.png',
|
||||
fullPage: true
|
||||
});
|
||||
console.log('📸 登录后页面截图已保存: after_login.png');
|
||||
|
||||
// 查找姓名管理相关的菜单项
|
||||
console.log('\n🔍 查找姓名管理菜单...');
|
||||
|
||||
// 等待菜单加载
|
||||
await page.waitForTimeout(3000);
|
||||
|
||||
// 查找包含"姓名"、"名字"、"firstname"、"lastname"的菜单项
|
||||
const menuSelectors = [
|
||||
'text=姓名管理',
|
||||
'text=名字管理',
|
||||
'text=姓名',
|
||||
'text=名字',
|
||||
'[href*="firstname"]',
|
||||
'[href*="lastname"]',
|
||||
'[href*="name"]'
|
||||
];
|
||||
|
||||
let menuFound = false;
|
||||
|
||||
for (const selector of menuSelectors) {
|
||||
try {
|
||||
const menuItem = await page.$(selector);
|
||||
if (menuItem) {
|
||||
console.log(`✅ 找到姓名相关菜单: ${selector}`);
|
||||
|
||||
// 点击菜单项
|
||||
await menuItem.click();
|
||||
console.log('🖱️ 点击姓名管理菜单');
|
||||
|
||||
// 等待页面加载
|
||||
await page.waitForTimeout(5000);
|
||||
|
||||
// 检查页面内容
|
||||
const namePageUrl = page.url();
|
||||
console.log(`📍 姓名管理页面: ${namePageUrl}`);
|
||||
|
||||
// 截取姓名管理页面
|
||||
await page.screenshot({
|
||||
path: 'name_management_page.png',
|
||||
fullPage: true
|
||||
});
|
||||
console.log('📸 姓名管理页面截图已保存: name_management_page.png');
|
||||
|
||||
// 检查页面数据
|
||||
const hasTable = await page.$('table') !== null;
|
||||
const hasAddButton = await page.$('button:has-text("添加"), .add-btn, [class*="add"]') !== null;
|
||||
const hasSearchInput = await page.$('input[type="text"], input[placeholder*="搜索"], input[placeholder*="姓"]') !== null;
|
||||
|
||||
console.log(`📊 姓名管理页面内容:`);
|
||||
console.log(` - 包含表格: ${hasTable}`);
|
||||
console.log(` - 包含添加按钮: ${hasAddButton}`);
|
||||
console.log(` - 包含搜索框: ${hasSearchInput}`);
|
||||
|
||||
// 尝试测试添加功能
|
||||
if (hasAddButton) {
|
||||
console.log('\n🧪 测试添加姓名功能...');
|
||||
const addButton = await page.$('button:has-text("添加"), .add-btn, [class*="add"]');
|
||||
await addButton.click();
|
||||
|
||||
// 等待弹窗或新页面
|
||||
await page.waitForTimeout(2000);
|
||||
|
||||
// 检查是否有弹窗或表单
|
||||
const hasModal = await page.$('.modal, .dialog, .popup') !== null;
|
||||
const hasForm = await page.$('form') !== null;
|
||||
|
||||
console.log(` - 弹出添加窗口: ${hasModal || hasForm}`);
|
||||
|
||||
if (hasModal || hasForm) {
|
||||
// 截取添加窗口
|
||||
await page.screenshot({
|
||||
path: 'add_name_modal.png',
|
||||
fullPage: true
|
||||
});
|
||||
console.log('📸 添加姓名窗口截图已保存: add_name_modal.png');
|
||||
}
|
||||
}
|
||||
|
||||
menuFound = true;
|
||||
break;
|
||||
}
|
||||
} catch (error) {
|
||||
// 继续尝试下一个选择器
|
||||
}
|
||||
}
|
||||
|
||||
if (!menuFound) {
|
||||
console.log('❌ 未找到姓名管理菜单项');
|
||||
|
||||
// 尝试查找所有菜单项
|
||||
console.log('🔍 查找所有可用菜单项...');
|
||||
const allMenus = await page.$$eval('a, .menu-item, [class*="menu"], li', elements => {
|
||||
return elements
|
||||
.filter(el => el.textContent && el.textContent.trim().length > 0)
|
||||
.map(el => ({
|
||||
text: el.textContent?.trim(),
|
||||
href: el.href || null,
|
||||
className: el.className
|
||||
}))
|
||||
.slice(0, 20); // 限制结果数量
|
||||
});
|
||||
|
||||
console.log('📋 可用菜单项:');
|
||||
allMenus.forEach((menu, index) => {
|
||||
console.log(` ${index + 1}. "${menu.text}" (href: ${menu.href})`);
|
||||
});
|
||||
}
|
||||
|
||||
} else {
|
||||
console.log('❌ 登录可能失败,检查页面状态...');
|
||||
const pageContent = await page.$eval('body', el => el.textContent?.substring(0, 200));
|
||||
console.log(`📄 页面内容: ${pageContent}...`);
|
||||
}
|
||||
} else {
|
||||
console.log('❌ 未找到登录按钮');
|
||||
}
|
||||
} else {
|
||||
console.log('❌ 未找到用户名或密码输入框');
|
||||
}
|
||||
} catch (loginError) {
|
||||
console.log(`❌ 登录过程失败: ${loginError.message}`);
|
||||
}
|
||||
} else {
|
||||
console.log('❌ 未找到完整的登录表单');
|
||||
}
|
||||
|
||||
// 等待观察
|
||||
await page.waitForTimeout(3000);
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ 测试失败:', error.message);
|
||||
} finally {
|
||||
if (browser) {
|
||||
await browser.close();
|
||||
}
|
||||
console.log('\n🎭 姓名管理测试完成');
|
||||
}
|
||||
}
|
||||
|
||||
// 运行测试
|
||||
if (require.main === module) {
|
||||
runNameManagementTest()
|
||||
.catch(error => {
|
||||
console.error('❌ 测试运行失败:', error.message);
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = runNameManagementTest;
|
||||
225
backend/playwright_test.js
Normal file
@@ -0,0 +1,225 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Playwright 前端测试脚本 - 检查姓名管理页面
|
||||
*/
|
||||
|
||||
const { chromium } = require('playwright');
|
||||
|
||||
async function runPlaywrightTest() {
|
||||
console.log('🎭 启动Playwright测试 - 检查姓名管理页面...\n');
|
||||
|
||||
let browser;
|
||||
let page;
|
||||
|
||||
try {
|
||||
// 启动浏览器
|
||||
browser = await chromium.launch({
|
||||
headless: false, // 设置为false以便观察
|
||||
slowMo: 500 // 减慢操作速度以便观察
|
||||
});
|
||||
|
||||
const context = await browser.newContext({
|
||||
viewport: { width: 1280, height: 800 }
|
||||
});
|
||||
|
||||
page = await context.newPage();
|
||||
|
||||
// 设置请求监听,捕获API调用
|
||||
page.on('request', request => {
|
||||
if (request.url().includes('/nameTemplate/') || request.url().includes('/firstname/') || request.url().includes('/lastname/')) {
|
||||
console.log(`📤 API请求: ${request.method()} ${request.url()}`);
|
||||
}
|
||||
});
|
||||
|
||||
page.on('response', response => {
|
||||
if (response.url().includes('/nameTemplate/') || response.url().includes('/firstname/') || response.url().includes('/lastname/')) {
|
||||
console.log(`📥 API响应: ${response.status()} ${response.url()}`);
|
||||
}
|
||||
});
|
||||
|
||||
// 访问前端页面(运行在8891端口)
|
||||
console.log('🌐 正在访问前端页面...');
|
||||
try {
|
||||
await page.goto('http://localhost:8891', {
|
||||
waitUntil: 'networkidle',
|
||||
timeout: 10000
|
||||
});
|
||||
console.log('✅ 前端页面加载成功');
|
||||
} catch (error) {
|
||||
console.log('❌ 前端页面访问失败,尝试其他端口...');
|
||||
|
||||
// 尝试其他常见端口
|
||||
const ports = [8080, 8081, 3000, 3001, 4000, 5000, 8890, 8892];
|
||||
let connected = false;
|
||||
|
||||
for (const port of ports) {
|
||||
try {
|
||||
await page.goto(`http://localhost:${port}`, {
|
||||
waitUntil: 'networkidle',
|
||||
timeout: 5000
|
||||
});
|
||||
console.log(`✅ 在端口 ${port} 找到前端页面`);
|
||||
connected = true;
|
||||
break;
|
||||
} catch (e) {
|
||||
console.log(`❌ 端口 ${port} 无法访问`);
|
||||
}
|
||||
}
|
||||
|
||||
if (!connected) {
|
||||
throw new Error('无法找到运行中的前端服务');
|
||||
}
|
||||
}
|
||||
|
||||
// 等待页面内容加载
|
||||
await page.waitForTimeout(2000);
|
||||
|
||||
// 检查页面标题
|
||||
const title = await page.title();
|
||||
console.log(`📝 页面标题: ${title}`);
|
||||
|
||||
// 尝试查找姓名管理相关的导航或页面元素
|
||||
console.log('\n🔍 查找姓名管理相关元素...');
|
||||
|
||||
// 查找可能的导航菜单
|
||||
const menuItems = await page.$$eval('*', elements => {
|
||||
return elements
|
||||
.filter(el => {
|
||||
const text = el.textContent || '';
|
||||
return text.includes('姓名') || text.includes('名字') || text.includes('firstname') || text.includes('lastname');
|
||||
})
|
||||
.map(el => ({
|
||||
tagName: el.tagName,
|
||||
text: el.textContent?.trim(),
|
||||
className: el.className,
|
||||
href: el.href || null
|
||||
}))
|
||||
.slice(0, 10); // 限制结果数量
|
||||
});
|
||||
|
||||
if (menuItems.length > 0) {
|
||||
console.log('✅ 找到姓名管理相关元素:');
|
||||
menuItems.forEach((item, index) => {
|
||||
console.log(` ${index + 1}. ${item.tagName}: "${item.text}" (class: ${item.className})`);
|
||||
});
|
||||
|
||||
// 尝试点击第一个相关元素
|
||||
try {
|
||||
const firstItem = menuItems[0];
|
||||
if (firstItem.text) {
|
||||
console.log(`\n🖱️ 尝试点击: "${firstItem.text}"`);
|
||||
await page.click(`text=${firstItem.text}`);
|
||||
await page.waitForTimeout(2000);
|
||||
|
||||
console.log('✅ 成功点击元素');
|
||||
|
||||
// 检查是否有数据加载
|
||||
await page.waitForTimeout(3000);
|
||||
|
||||
// 查找表格或列表数据
|
||||
const hasTable = await page.$('table') !== null;
|
||||
const hasList = await page.$('[class*="list"]') !== null;
|
||||
const hasCards = await page.$('[class*="card"]') !== null;
|
||||
|
||||
console.log(`📊 页面内容检查:`);
|
||||
console.log(` - 包含表格: ${hasTable}`);
|
||||
console.log(` - 包含列表: ${hasList}`);
|
||||
console.log(` - 包含卡片: ${hasCards}`);
|
||||
|
||||
// 截取屏幕截图
|
||||
await page.screenshot({
|
||||
path: 'name_management_page.png',
|
||||
fullPage: true
|
||||
});
|
||||
console.log('📸 屏幕截图已保存: name_management_page.png');
|
||||
}
|
||||
} catch (clickError) {
|
||||
console.log(`❌ 点击失败: ${clickError.message}`);
|
||||
}
|
||||
} else {
|
||||
console.log('❌ 未找到姓名管理相关元素');
|
||||
|
||||
// 获取页面的主要内容
|
||||
const bodyText = await page.$eval('body', el => el.textContent?.substring(0, 500));
|
||||
console.log(`📄 页面主要内容预览:\n${bodyText}...`);
|
||||
}
|
||||
|
||||
// 检查控制台错误
|
||||
console.log('\n🔍 检查控制台错误...');
|
||||
page.on('console', msg => {
|
||||
if (msg.type() === 'error') {
|
||||
console.log(`❌ 控制台错误: ${msg.text()}`);
|
||||
}
|
||||
});
|
||||
|
||||
// 检查网络错误
|
||||
page.on('requestfailed', request => {
|
||||
console.log(`❌ 网络请求失败: ${request.url()} - ${request.failure()?.errorText}`);
|
||||
});
|
||||
|
||||
// 等待一段时间以观察页面
|
||||
await page.waitForTimeout(5000);
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Playwright测试失败:', error.message);
|
||||
|
||||
if (page) {
|
||||
// 尝试截取错误页面
|
||||
try {
|
||||
await page.screenshot({
|
||||
path: 'error_page.png',
|
||||
fullPage: true
|
||||
});
|
||||
console.log('📸 错误页面截图已保存: error_page.png');
|
||||
} catch (screenshotError) {
|
||||
console.log('❌ 无法保存错误截图');
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
if (browser) {
|
||||
await browser.close();
|
||||
}
|
||||
console.log('\n🎭 Playwright测试完成');
|
||||
}
|
||||
}
|
||||
|
||||
// 检查Playwright是否已安装
|
||||
async function checkPlaywrightInstallation() {
|
||||
try {
|
||||
require('playwright');
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.log('❌ Playwright未安装,正在安装...');
|
||||
|
||||
const { spawn } = require('child_process');
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const install = spawn('npm', ['install', 'playwright'], {
|
||||
stdio: 'inherit',
|
||||
cwd: process.cwd()
|
||||
});
|
||||
|
||||
install.on('close', (code) => {
|
||||
if (code === 0) {
|
||||
console.log('✅ Playwright安装成功');
|
||||
resolve(true);
|
||||
} else {
|
||||
reject(new Error(`Playwright安装失败,退出码: ${code}`));
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 主函数
|
||||
if (require.main === module) {
|
||||
checkPlaywrightInstallation()
|
||||
.then(() => runPlaywrightTest())
|
||||
.catch(error => {
|
||||
console.error('❌ 测试运行失败:', error.message);
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = runPlaywrightTest;
|
||||
235
backend/quick_test.js
Normal file
@@ -0,0 +1,235 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* 快速测试修复后的功能
|
||||
*/
|
||||
|
||||
const { chromium } = require('playwright');
|
||||
|
||||
async function quickTest() {
|
||||
console.log('🔧 快速测试修复后的功能...\n');
|
||||
|
||||
let browser;
|
||||
let page;
|
||||
|
||||
try {
|
||||
browser = await chromium.launch({
|
||||
headless: false,
|
||||
slowMo: 1000
|
||||
});
|
||||
|
||||
const context = await browser.newContext({
|
||||
viewport: { width: 1400, height: 900 }
|
||||
});
|
||||
|
||||
page = await context.newPage();
|
||||
|
||||
// 监听API调用
|
||||
page.on('request', request => {
|
||||
const url = request.url();
|
||||
if (url.includes('/nameTemplate/')) {
|
||||
console.log(`🔗 ${request.method()} ${url}`);
|
||||
}
|
||||
});
|
||||
|
||||
page.on('response', response => {
|
||||
const url = response.url();
|
||||
if (url.includes('/nameTemplate/')) {
|
||||
console.log(`📡 ${response.status()} ${url}`);
|
||||
}
|
||||
});
|
||||
|
||||
page.on('console', msg => {
|
||||
if (msg.type() === 'error') {
|
||||
console.log(`❌ 前端错误: ${msg.text()}`);
|
||||
}
|
||||
});
|
||||
|
||||
// 登录
|
||||
console.log('🚀 登录系统...');
|
||||
await page.goto('http://localhost:8891');
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
await page.fill('input[type="text"]', 'admin');
|
||||
await page.fill('input[type="password"]', '111111');
|
||||
await page.click('button:has-text("登录")');
|
||||
await page.waitForLoadState('networkidle');
|
||||
console.log('✅ 登录成功\n');
|
||||
|
||||
// 访问统一管理页面
|
||||
console.log('🎯 访问统一姓名管理页面...');
|
||||
await page.goto('http://localhost:8891/#/nameManage/unified');
|
||||
await page.waitForLoadState('networkidle');
|
||||
await page.waitForTimeout(5000);
|
||||
console.log('✅ 页面加载完成\n');
|
||||
|
||||
// 测试1:检查页面元素
|
||||
console.log('📊 测试1:检查页面基本元素...');
|
||||
const pageElements = await page.evaluate(() => {
|
||||
return {
|
||||
hasStatusPanel: !!document.querySelector('.status-panel'),
|
||||
hasGeneratePanel: !!document.querySelector('.generate-panel'),
|
||||
hasGenerateButton: !!document.querySelector('button'),
|
||||
buttonTexts: Array.from(document.querySelectorAll('button')).map(b => b.textContent?.trim()),
|
||||
hasTable: !!document.querySelector('table'),
|
||||
headerCount: document.querySelectorAll('thead th').length
|
||||
};
|
||||
});
|
||||
|
||||
console.log('✅ 页面元素检查:');
|
||||
console.log(` - 状态面板: ${pageElements.hasStatusPanel}`);
|
||||
console.log(` - 生成面板: ${pageElements.hasGeneratePanel}`);
|
||||
console.log(` - 按钮: ${pageElements.buttonTexts.join(', ')}`);
|
||||
console.log(` - 数据表格: ${pageElements.hasTable} (${pageElements.headerCount}列)`);
|
||||
|
||||
await page.screenshot({ path: 'quick_test_01_page_loaded.png', fullPage: true });
|
||||
|
||||
// 测试2:测试智能生成
|
||||
console.log('\n🎲 测试2:测试智能生成功能...');
|
||||
|
||||
// 先设置参数
|
||||
try {
|
||||
// 选择平台
|
||||
const platformSelect = page.locator('.generate-panel .ivu-select').first();
|
||||
await platformSelect.click();
|
||||
await page.waitForTimeout(500);
|
||||
const telegramOption = page.locator('.ivu-select-dropdown li').filter({ hasText: 'Telegram' }).first();
|
||||
if (await telegramOption.isVisible()) {
|
||||
await telegramOption.click();
|
||||
console.log('🔧 选择平台: Telegram');
|
||||
}
|
||||
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
// 点击生成按钮
|
||||
const generateButton = page.locator('button').filter({ hasText: '生成姓名' });
|
||||
if (await generateButton.isVisible()) {
|
||||
await generateButton.click();
|
||||
console.log('🎯 点击生成姓名按钮');
|
||||
await page.waitForTimeout(3000);
|
||||
|
||||
// 检查是否有生成结果
|
||||
const hasResults = await page.evaluate(() => {
|
||||
return document.querySelectorAll('.name-result-item').length > 0;
|
||||
});
|
||||
|
||||
console.log(`${hasResults ? '✅' : '⚠️'} 生成结果: ${hasResults ? '有结果' : '无结果'}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(`⚠️ 生成测试遇到错误: ${error.message}`);
|
||||
}
|
||||
|
||||
await page.screenshot({ path: 'quick_test_02_generate_test.png', fullPage: true });
|
||||
|
||||
// 测试3:测试添加功能
|
||||
console.log('\n➕ 测试3:测试添加姓名模板...');
|
||||
|
||||
try {
|
||||
const addButton = page.locator('button').filter({ hasText: '添加姓名模板' });
|
||||
if (await addButton.isVisible()) {
|
||||
await addButton.click();
|
||||
console.log('🖱️ 点击添加按钮');
|
||||
await page.waitForTimeout(2000);
|
||||
|
||||
const hasModal = await page.evaluate(() => {
|
||||
return !!document.querySelector('.ivu-modal');
|
||||
});
|
||||
|
||||
console.log(`${hasModal ? '✅' : '❌'} 添加弹窗: ${hasModal ? '已打开' : '未打开'}`);
|
||||
|
||||
if (hasModal) {
|
||||
// 填写一些测试数据
|
||||
const lastNameInput = page.locator('.ivu-modal input').first();
|
||||
await lastNameInput.fill('测试');
|
||||
console.log('✏️ 填写测试数据');
|
||||
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
// 取消而不提交
|
||||
const cancelButton = page.locator('.ivu-modal button').filter({ hasText: '取消' });
|
||||
if (await cancelButton.isVisible()) {
|
||||
await cancelButton.click();
|
||||
console.log('❌ 取消添加');
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(`⚠️ 添加测试遇到错误: ${error.message}`);
|
||||
}
|
||||
|
||||
await page.screenshot({ path: 'quick_test_03_add_test.png', fullPage: true });
|
||||
|
||||
// 测试4:API直接测试
|
||||
console.log('\n🌐 测试4:API端点直接测试...');
|
||||
|
||||
const apiTests = [
|
||||
{ name: 'supportedOptions', url: 'http://localhost:3000/nameTemplate/supportedOptions' },
|
||||
{ name: 'generatorStatus', url: 'http://localhost:3000/nameTemplate/generatorStatus' }
|
||||
];
|
||||
|
||||
for (const test of apiTests) {
|
||||
try {
|
||||
const response = await fetch(test.url);
|
||||
console.log(`${response.ok ? '✅' : '❌'} ${test.name}: ${response.status}`);
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
if (test.name === 'supportedOptions') {
|
||||
console.log(` - 平台数量: ${data.data?.platforms?.length || 0}`);
|
||||
console.log(` - 文化数量: ${data.data?.cultures?.length || 0}`);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(`❌ ${test.name}: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// 测试生成API
|
||||
try {
|
||||
const generateResponse = await fetch('http://localhost:3000/nameTemplate/generate', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
platform: 'telegram',
|
||||
culture: 'cn',
|
||||
gender: 'neutral',
|
||||
batch: true,
|
||||
count: 3
|
||||
})
|
||||
});
|
||||
console.log(`${generateResponse.ok ? '✅' : '❌'} generate API: ${generateResponse.status}`);
|
||||
if (generateResponse.ok) {
|
||||
const data = await generateResponse.json();
|
||||
console.log(` - 生成成功: ${data.success}`);
|
||||
console.log(` - 结果数量: ${Array.isArray(data.data) ? data.data.length : 1}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(`❌ generate API: ${error.message}`);
|
||||
}
|
||||
|
||||
console.log('\n🎉 快速测试完成!');
|
||||
console.log('\n📂 测试截图:');
|
||||
console.log(' - quick_test_01_page_loaded.png');
|
||||
console.log(' - quick_test_02_generate_test.png');
|
||||
console.log(' - quick_test_03_add_test.png');
|
||||
|
||||
console.log('\n⏰ 浏览器将保持打开10秒...');
|
||||
await page.waitForTimeout(10000);
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ 测试失败:', error.message);
|
||||
if (page) {
|
||||
await page.screenshot({ path: 'quick_test_error.png', fullPage: true });
|
||||
}
|
||||
} finally {
|
||||
if (browser) {
|
||||
await browser.close();
|
||||
}
|
||||
console.log('🏁 测试结束');
|
||||
}
|
||||
}
|
||||
|
||||
if (require.main === module) {
|
||||
quickTest().catch(console.error);
|
||||
}
|
||||
|
||||
module.exports = quickTest;
|
||||
BIN
backend/quick_test_01_page_loaded.png
Normal file
|
After Width: | Height: | Size: 137 KiB |
BIN
backend/quick_test_02_generate_test.png
Normal file
|
After Width: | Height: | Size: 145 KiB |
BIN
backend/quick_test_03_add_test.png
Normal file
|
After Width: | Height: | Size: 145 KiB |
286
backend/src/Server.js
Normal file
@@ -0,0 +1,286 @@
|
||||
require('module-alias/register');
|
||||
const fs = require("fs");
|
||||
const path=require("path");
|
||||
const Hapi = require('@hapi/hapi');
|
||||
const Db=require("@src/config/Db");
|
||||
const bearerAccessToken=require("@src/lib/hapi-auth-bearer-token");
|
||||
const LoginVerify=require("@src/verify/LoginVerify");
|
||||
const MAdminService = require("@src/service/MAdminService");
|
||||
const Redis = require("@src/util/RedisUtil");
|
||||
const logger=require("@src/util/Log4jUtil");
|
||||
const Config=require("@src/config/Config");
|
||||
const MD5Util = require("@src/util/MD5Util");
|
||||
const JwtUtil = require("@src/util/JwtUtil");
|
||||
|
||||
//这种括号变量名,为什么还可以同时定义两个?
|
||||
const {serverPort , redisPassword} = require("@src/config/Config");
|
||||
|
||||
const SocketBus= require("@src/socket/SocketBus");
|
||||
const ProxyUtil=require("@src/util/ProxyUtil");
|
||||
const AmqpBus = require("@src/amqp/AmqpBus");
|
||||
const MongodbUtil = require("@src/util/MongodbUtil");
|
||||
const RedisUtil = require("@src/util/RedisUtil");
|
||||
|
||||
const init = async () => {
|
||||
|
||||
//初始化数据库
|
||||
await Db.getInstance();
|
||||
|
||||
// 初始化模型关联关系
|
||||
const initAssociations = require("@src/modes/initAssociations");
|
||||
initAssociations();
|
||||
|
||||
|
||||
//启动服务
|
||||
const server = Hapi.server({
|
||||
port: serverPort,
|
||||
host: '0.0.0.0',
|
||||
routes: {
|
||||
cors: {
|
||||
origin:["*"],
|
||||
headers:["token","x-requested-with","Content-Type","Cache-Control","Accept-Language","Accept-Encoding","Connection","Content-Length"]
|
||||
},
|
||||
payload: {
|
||||
parse: true,
|
||||
allow: ['application/json', 'application/x-www-form-urlencoded', 'text/plain']
|
||||
}
|
||||
},
|
||||
});
|
||||
//注册token验证插件
|
||||
await server.register(bearerAccessToken);
|
||||
//redis
|
||||
let redisSetting={
|
||||
host: 'localhost',
|
||||
port: 6379,
|
||||
db:6,
|
||||
}
|
||||
if(!Config.isDev){
|
||||
redisSetting.password=redisPassword;
|
||||
}
|
||||
|
||||
await server.register({
|
||||
plugin: require('hapi-redis2'),
|
||||
options:{
|
||||
settings: redisSetting,
|
||||
decorate: true
|
||||
}
|
||||
});
|
||||
|
||||
// 临时禁用MongoDB以便系统启动 - 主要功能使用MySQL
|
||||
/*
|
||||
await server.register({
|
||||
plugin: require('hapi-mongodb'),
|
||||
options: {
|
||||
url: Config.isDev?Config.mongodb.dev.url:Config.mongodb.pro.url,
|
||||
settings: {
|
||||
maxPoolSize: 30
|
||||
},
|
||||
decorate: true
|
||||
}
|
||||
});
|
||||
*/
|
||||
|
||||
//设置Redis
|
||||
Redis.getInstance(server.redis.client);
|
||||
// MongodbUtil.getInstance(server.mongo.db); // 临时禁用
|
||||
|
||||
|
||||
|
||||
//登录验证插件
|
||||
new LoginVerify(server);
|
||||
|
||||
//服务启动
|
||||
await server.start();
|
||||
|
||||
// 初始化WebSocket服务
|
||||
const SmsWebSocketHandler = require('@src/websocket/SmsWebSocketHandler');
|
||||
const httpServer = server.listener;
|
||||
SmsWebSocketHandler.getInstance(httpServer);
|
||||
|
||||
//等待初始数据库,创建默认admin账户
|
||||
setTimeout(()=>{
|
||||
MAdminService.getInstance().initDefaultAdmin();
|
||||
// 初始化短信平台数据
|
||||
const MSmsPlatformService = require("@src/service/MSmsPlatformService");
|
||||
MSmsPlatformService.getInstance().initDefaultPlatforms();
|
||||
// 初始化代理平台数据
|
||||
const MProxyPlatformService = require("@src/service/MProxyPlatformService");
|
||||
MProxyPlatformService.getInstance().initializeDefaultPlatforms();
|
||||
},5000);
|
||||
|
||||
//初始化socket
|
||||
SocketBus.getInstance();
|
||||
|
||||
//初始化代理ip
|
||||
ProxyUtil.getInstance();
|
||||
|
||||
//初始化代理平台数据
|
||||
setTimeout(async () => {
|
||||
try {
|
||||
const MProxyPlatformService = require('@src/service/MProxyPlatformService');
|
||||
await MProxyPlatformService.getInstance().initializeDefaultPlatforms();
|
||||
console.log("代理平台初始化完成");
|
||||
} catch (error) {
|
||||
console.error("代理平台初始化失败:", error);
|
||||
}
|
||||
}, 3000);
|
||||
|
||||
// 初始化实时监控WebSocket服务
|
||||
setTimeout(() => {
|
||||
try {
|
||||
const RealtimeMonitor = require('@src/service/RealtimeMonitor');
|
||||
const realtimePort = process.env.REALTIME_MONITOR_PORT
|
||||
? parseInt(process.env.REALTIME_MONITOR_PORT, 10)
|
||||
: 8081;
|
||||
RealtimeMonitor.getInstance().start(realtimePort);
|
||||
console.log(`实时监控WebSocket服务启动成功,端口: ${realtimePort}`);
|
||||
} catch (error) {
|
||||
console.error("实时监控WebSocket服务启动失败:", error);
|
||||
}
|
||||
}, 6000);
|
||||
|
||||
//初始化炒群,炒群的自动启动等
|
||||
|
||||
//初始化拉人,拉人的自动启动等
|
||||
|
||||
//初始化自动汇报,自动汇报的自动启动等
|
||||
|
||||
|
||||
|
||||
server.route({
|
||||
method: 'GET',
|
||||
path: '/',
|
||||
handler: (request, h) => {
|
||||
return '草拟🐴的抓包???';
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: 'GET',
|
||||
path: '/api/health',
|
||||
options: {
|
||||
auth: false
|
||||
},
|
||||
handler: (request, h) => {
|
||||
return { status: 'healthy', timestamp: new Date().toISOString() };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: '*',
|
||||
path: '/{any*}',
|
||||
handler: function (request, h) {
|
||||
return '404 Error! Page Not Found!';
|
||||
}
|
||||
});
|
||||
|
||||
// 初始化业务路由和消息队列
|
||||
await initRouter(server);
|
||||
AmqpBus.getInstance();
|
||||
|
||||
|
||||
|
||||
|
||||
logger.getLogger().info('Server running on %s', server.info.uri);
|
||||
|
||||
|
||||
// let rrrr=await server.mongo.db.collection("c_group_members").find({_id:MongodbUtil.getObjectIdById("61ab056be7f7b93c30c040b0")}).toArray();
|
||||
// console.log(rrrr);
|
||||
|
||||
//导入数据
|
||||
// var RedisUtil=require("@src/util/RedisUtil");
|
||||
// var list=JSON.parse(await RedisUtil.getCache("userList"));
|
||||
// var axios=require("axios");
|
||||
// let accessToken="20700f26a5764c1a98205e4f8a562186";
|
||||
// let secret="33620cbef178118716418480d19a6615";
|
||||
// var querystring = require('querystring');
|
||||
// for(var i=22062;i<list.length;i++) {
|
||||
// console.log("个数:" + i);
|
||||
// let item=list[i];
|
||||
// if(item.name == 1000 || item.name == 10000 || item.name == 1100){
|
||||
// continue;
|
||||
// }
|
||||
// if(!item.balance){
|
||||
// console.log("钱为空跳过:" + i);
|
||||
// continue;
|
||||
// }
|
||||
// let balance=item.balance;
|
||||
// if(balance<=0){
|
||||
// console.log("钱为0跳过:" + i);
|
||||
// continue;
|
||||
// }
|
||||
// let user=await server.mongo.db.collection("user").findOne({phone:item.phone});
|
||||
// if(!user)continue;
|
||||
// if(user.balance == item.balance){
|
||||
// console.log("钱都一样跳过:" + i);
|
||||
// continue;
|
||||
// }
|
||||
// let data={
|
||||
// money:item.balance,
|
||||
// userId:user._id.toString(),
|
||||
// access_token:accessToken
|
||||
// }
|
||||
//
|
||||
// let res = await axios({
|
||||
// method:"POST",
|
||||
// url:"http://admin.juliao-pc.com/console/Recharge?secret="+secret+"&time=1641363387&access_token="+accessToken,
|
||||
// data:querystring.stringify(data)
|
||||
// });
|
||||
// if (res.status != 200) {
|
||||
// console.log("获取出错:" + i);
|
||||
// --i;
|
||||
// continue;
|
||||
// }
|
||||
// if(res.data.resultCode!=1){
|
||||
// console.log("返回出错:" + user._id);
|
||||
// console.log(data)
|
||||
// console.log(res.data);
|
||||
// --i;
|
||||
// continue;
|
||||
// }
|
||||
// console.log(res.data);
|
||||
// }
|
||||
// console.log("完毕")
|
||||
|
||||
};
|
||||
|
||||
const initRouter = async (server) => {
|
||||
const registerRouters = (dirPath, basePath) => {
|
||||
fs.readdirSync(dirPath).filter((file) => {
|
||||
return (file.indexOf(".") !== 0) && (file !== "BaseRouter.js");
|
||||
}).forEach((file) => {
|
||||
try {
|
||||
const RouterFile = require(path.join(basePath, file));
|
||||
new RouterFile(server);
|
||||
} catch (error) {
|
||||
console.error(`加载路由文件 ${file} 失败:`, error);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
registerRouters(path.join(process.cwd(), "src/routers"), "@src/routers");
|
||||
registerRouters(path.join(process.cwd(), "src/wapRouters"), "@src/wapRouters");
|
||||
};
|
||||
|
||||
|
||||
//promise没try退出
|
||||
process.on('unhandledRejection', (err) => {
|
||||
console.log("未处理的Promise错误:");
|
||||
console.log(err);
|
||||
// 不退出,只记录错误
|
||||
});
|
||||
|
||||
init().then(r => {
|
||||
console.log("初始化完成");
|
||||
}).catch(e => {
|
||||
console.log("初始化失败");
|
||||
console.log(e);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
//测试
|
||||
// setTimeout(()=>{
|
||||
// console.log("登录中");
|
||||
// const Test=require('@src/test/Test');
|
||||
// new Test();
|
||||
// },8000);
|
||||
307
backend/src/adapters/BaseProxyAdapter.js
Normal file
@@ -0,0 +1,307 @@
|
||||
const axios = require('axios');
|
||||
|
||||
/**
|
||||
* 代理平台适配器基类
|
||||
* 定义统一的代理平台接口规范
|
||||
*/
|
||||
class BaseProxyAdapter {
|
||||
constructor(config = {}) {
|
||||
this.config = config;
|
||||
|
||||
// 初始化日志器 - 支持独立运行
|
||||
try {
|
||||
const Log4jUtil = require('../util/Log4jUtil');
|
||||
this.logger = Log4jUtil.logger(this.constructor.name);
|
||||
} catch (error) {
|
||||
// 独立运行时使用简单的控制台日志
|
||||
this.logger = {
|
||||
info: (...args) => console.log(`[${this.constructor.name}]`, ...args),
|
||||
error: (...args) => console.error(`[${this.constructor.name}]`, ...args),
|
||||
debug: (...args) => console.log(`[${this.constructor.name}]`, ...args),
|
||||
warn: (...args) => console.warn(`[${this.constructor.name}]`, ...args)
|
||||
};
|
||||
}
|
||||
|
||||
this.authenticated = false;
|
||||
this.lastAuthTime = null;
|
||||
this.authValidDuration = 30 * 60 * 1000; // 30分钟认证有效期
|
||||
}
|
||||
|
||||
/**
|
||||
* 认证接口
|
||||
* @returns {Promise<boolean>} 认证是否成功
|
||||
*/
|
||||
async authenticate() {
|
||||
throw new Error('authenticate() method must be implemented');
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查认证是否有效
|
||||
* @returns {boolean}
|
||||
*/
|
||||
isAuthValid() {
|
||||
if (!this.authenticated || !this.lastAuthTime) {
|
||||
return false;
|
||||
}
|
||||
return (Date.now() - this.lastAuthTime) < this.authValidDuration;
|
||||
}
|
||||
|
||||
/**
|
||||
* 确保认证有效
|
||||
* @returns {Promise<boolean>}
|
||||
*/
|
||||
async ensureAuthenticated() {
|
||||
if (!this.isAuthValid()) {
|
||||
this.logger.info('认证已过期,重新认证...');
|
||||
return await this.authenticate();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取代理列表
|
||||
* @param {Object} options 查询选项 {type, country, limit, offset}
|
||||
* @returns {Promise<Array>} 代理列表
|
||||
*/
|
||||
async getProxyList(options = {}) {
|
||||
throw new Error('getProxyList() method must be implemented');
|
||||
}
|
||||
|
||||
/**
|
||||
* 检测单个代理
|
||||
* @param {Object} proxy 代理信息
|
||||
* @returns {Promise<Object>} 检测结果
|
||||
*/
|
||||
async checkProxy(proxy) {
|
||||
throw new Error('checkProxy() method must be implemented');
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取账户余额
|
||||
* @returns {Promise<Object>} 余额信息
|
||||
*/
|
||||
async getBalance() {
|
||||
throw new Error('getBalance() method must be implemented');
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取使用统计
|
||||
* @param {Object} options 查询选项
|
||||
* @returns {Promise<Object>} 统计信息
|
||||
*/
|
||||
async getStatistics(options = {}) {
|
||||
throw new Error('getStatistics() method must be implemented');
|
||||
}
|
||||
|
||||
/**
|
||||
* 通用HTTP请求方法
|
||||
* @param {string} method HTTP方法
|
||||
* @param {string} url 请求URL
|
||||
* @param {Object} data 请求数据
|
||||
* @param {Object} headers 请求头
|
||||
* @returns {Promise<Object>} 响应数据
|
||||
*/
|
||||
async makeRequest(method, url, data = null, headers = {}) {
|
||||
try {
|
||||
const config = {
|
||||
method,
|
||||
url,
|
||||
headers: {
|
||||
'User-Agent': 'TelegramManagementSystem/1.0',
|
||||
'Content-Type': 'application/json',
|
||||
...headers
|
||||
},
|
||||
timeout: 30000, // 30秒超时
|
||||
validateStatus: (status) => status < 500 // 不抛出4xx错误
|
||||
};
|
||||
|
||||
if (data) {
|
||||
if (method.toUpperCase() === 'GET') {
|
||||
config.params = data;
|
||||
} else {
|
||||
config.data = data;
|
||||
}
|
||||
}
|
||||
|
||||
const response = await axios(config);
|
||||
|
||||
if (response.status >= 400) {
|
||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||
}
|
||||
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
this.logger.error(`请求失败 [${method}] ${url}:`, error.message);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 通用代理检测方法
|
||||
* @param {Object} proxy 代理配置
|
||||
* @param {string} testUrl 测试URL
|
||||
* @param {number} timeout 超时时间
|
||||
* @returns {Promise<Object>} 检测结果
|
||||
*/
|
||||
async performProxyCheck(proxy, testUrl = 'http://httpbin.org/ip', timeout = 15000) {
|
||||
const startTime = Date.now();
|
||||
|
||||
try {
|
||||
// 构建代理配置
|
||||
const proxyConfig = {
|
||||
host: proxy.ip_address || proxy.ip,
|
||||
port: parseInt(proxy.port),
|
||||
auth: proxy.username && proxy.password ? {
|
||||
username: proxy.username,
|
||||
password: proxy.password
|
||||
} : null
|
||||
};
|
||||
|
||||
// 发送测试请求
|
||||
const response = await axios.get(testUrl, {
|
||||
proxy: proxyConfig,
|
||||
timeout,
|
||||
headers: {
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
|
||||
}
|
||||
});
|
||||
|
||||
const responseTime = Date.now() - startTime;
|
||||
const realIP = response.data.origin || response.data.ip;
|
||||
|
||||
return {
|
||||
success: true,
|
||||
responseTime,
|
||||
realIP,
|
||||
anonymity: this.detectAnonymity(response.data, proxy.ip_address || proxy.ip),
|
||||
httpStatus: response.status,
|
||||
headers: response.headers,
|
||||
testUrl,
|
||||
timestamp: new Date()
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error.message,
|
||||
errorCode: error.code,
|
||||
responseTime: Date.now() - startTime,
|
||||
testUrl,
|
||||
timestamp: new Date()
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检测代理匿名性
|
||||
* @param {Object} responseData 响应数据
|
||||
* @param {string} proxyIP 代理IP
|
||||
* @returns {string} 匿名级别: transparent, anonymous, elite
|
||||
*/
|
||||
detectAnonymity(responseData, proxyIP) {
|
||||
const realIP = responseData.origin || responseData.ip;
|
||||
|
||||
// 如果返回的IP就是代理IP,说明是透明代理
|
||||
if (realIP === proxyIP) {
|
||||
return 'transparent';
|
||||
}
|
||||
|
||||
// 检查是否暴露了真实IP信息
|
||||
const headers = responseData.headers || {};
|
||||
const suspiciousHeaders = [
|
||||
'X-Forwarded-For',
|
||||
'X-Real-IP',
|
||||
'Via',
|
||||
'Forwarded',
|
||||
'X-Originating-IP',
|
||||
'X-Remote-IP'
|
||||
];
|
||||
|
||||
for (const header of suspiciousHeaders) {
|
||||
if (headers[header.toLowerCase()]) {
|
||||
return 'anonymous';
|
||||
}
|
||||
}
|
||||
|
||||
return 'elite';
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化代理信息
|
||||
* @param {Object} rawProxy 原始代理数据
|
||||
* @returns {Object} 格式化后的代理信息
|
||||
*/
|
||||
formatProxy(rawProxy) {
|
||||
return {
|
||||
ip_address: rawProxy.ip || rawProxy.host || rawProxy.server,
|
||||
port: parseInt(rawProxy.port),
|
||||
username: rawProxy.username || rawProxy.user,
|
||||
password: rawProxy.password || rawProxy.pass,
|
||||
proxy_type: rawProxy.type || 'residential',
|
||||
country_code: rawProxy.country || rawProxy.country_code,
|
||||
city: rawProxy.city,
|
||||
protocol: rawProxy.protocol || 'http',
|
||||
expires_at: rawProxy.expires_at,
|
||||
created_at: rawProxy.created_at || new Date()
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量检测代理
|
||||
* @param {Array} proxies 代理列表
|
||||
* @param {number} concurrency 并发数
|
||||
* @returns {Promise<Array>} 检测结果列表
|
||||
*/
|
||||
async batchCheckProxies(proxies, concurrency = 10) {
|
||||
const results = [];
|
||||
|
||||
// 分批处理以控制并发
|
||||
for (let i = 0; i < proxies.length; i += concurrency) {
|
||||
const batch = proxies.slice(i, i + concurrency);
|
||||
const batchPromises = batch.map(proxy =>
|
||||
this.performProxyCheck(proxy).catch(error => ({
|
||||
success: false,
|
||||
error: error.message,
|
||||
proxy: proxy
|
||||
}))
|
||||
);
|
||||
|
||||
const batchResults = await Promise.allSettled(batchPromises);
|
||||
results.push(...batchResults.map(result => result.value));
|
||||
|
||||
// 短暂延迟避免过于频繁的请求
|
||||
if (i + concurrency < proxies.length) {
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析错误信息
|
||||
* @param {Error} error 错误对象
|
||||
* @returns {Object} 格式化的错误信息
|
||||
*/
|
||||
parseError(error) {
|
||||
return {
|
||||
message: error.message,
|
||||
code: error.code,
|
||||
status: error.response?.status,
|
||||
statusText: error.response?.statusText,
|
||||
timestamp: new Date()
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成请求签名(如需要)
|
||||
* @param {Object} params 请求参数
|
||||
* @returns {string} 签名字符串
|
||||
*/
|
||||
generateSignature(params) {
|
||||
// 子类可以重写此方法实现特定的签名算法
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = BaseProxyAdapter;
|
||||
461
backend/src/adapters/CultureAdapter.js
Normal file
@@ -0,0 +1,461 @@
|
||||
const Logger = require("@src/util/Log4jUtil");
|
||||
|
||||
/**
|
||||
* 文化适配器 - 处理不同文化背景的姓名规则和习惯
|
||||
*/
|
||||
class CultureAdapter {
|
||||
static getInstance() {
|
||||
if (!CultureAdapter.instance) {
|
||||
CultureAdapter.instance = new CultureAdapter();
|
||||
}
|
||||
return CultureAdapter.instance;
|
||||
}
|
||||
|
||||
constructor() {
|
||||
this.logger = Logger.getLogger("CultureAdapter");
|
||||
this.cultureRules = this.initializeCultureRules();
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化各文化背景的规则
|
||||
*/
|
||||
initializeCultureRules() {
|
||||
return {
|
||||
cn: { // 中国
|
||||
nameOrder: 'lastName_firstName',
|
||||
commonLastNames: ['王', '李', '张', '刘', '陈', '杨', '赵', '黄', '周', '吴', '徐', '孙', '朱', '马', '胡', '郭', '林', '何', '高', '梁'],
|
||||
commonFirstNames: {
|
||||
male: ['伟', '强', '明', '华', '建', '军', '峰', '磊', '超', '鹏', '杰', '勇', '涛', '刚', '斌'],
|
||||
female: ['芳', '娜', '敏', '静', '丽', '红', '艳', '玲', '梅', '燕', '霞', '萍', '秀', '兰', '莉'],
|
||||
neutral: ['文', '斌', '宇', '晨', '阳', '雨', '心', '安', '和', '平']
|
||||
},
|
||||
nameStructure: {
|
||||
pattern: '{lastName}{firstName}',
|
||||
lastNameLength: [1, 2],
|
||||
firstNameLength: [1, 2],
|
||||
totalMaxLength: 4,
|
||||
allowDoubleLastName: true
|
||||
},
|
||||
phonetics: {
|
||||
consonants: ['zh', 'ch', 'sh', 'r', 'z', 'c', 's', 'b', 'p', 'm', 'f', 'd', 't', 'n', 'l', 'g', 'k', 'h', 'j', 'q', 'x'],
|
||||
vowels: ['a', 'o', 'e', 'i', 'u', 'v', 'ai', 'ei', 'ao', 'ou', 'an', 'en', 'ang', 'eng', 'ong']
|
||||
},
|
||||
culturalNotes: '中文名通常由姓氏+名字组成,姓氏在前。常见姓氏有百家姓记录,名字通常有美好寓意。'
|
||||
},
|
||||
us: { // 美国
|
||||
nameOrder: 'firstName_lastName',
|
||||
commonLastNames: ['Smith', 'Johnson', 'Williams', 'Brown', 'Jones', 'Garcia', 'Miller', 'Davis', 'Rodriguez', 'Martinez', 'Hernandez', 'Lopez', 'Gonzalez', 'Wilson', 'Anderson', 'Thomas', 'Taylor', 'Moore', 'Jackson', 'Martin'],
|
||||
commonFirstNames: {
|
||||
male: ['James', 'Robert', 'John', 'Michael', 'William', 'David', 'Richard', 'Joseph', 'Thomas', 'Christopher', 'Charles', 'Daniel', 'Matthew', 'Anthony', 'Mark'],
|
||||
female: ['Mary', 'Patricia', 'Jennifer', 'Linda', 'Elizabeth', 'Barbara', 'Susan', 'Jessica', 'Sarah', 'Karen', 'Lisa', 'Nancy', 'Betty', 'Helen', 'Sandra'],
|
||||
neutral: ['Alex', 'Jordan', 'Taylor', 'Casey', 'Riley', 'Avery', 'Parker', 'Quinn', 'Blake', 'Sage']
|
||||
},
|
||||
nameStructure: {
|
||||
pattern: '{firstName} {lastName}',
|
||||
lastNameLength: [4, 12],
|
||||
firstNameLength: [3, 10],
|
||||
totalMaxLength: 25,
|
||||
allowMiddleName: true
|
||||
},
|
||||
phonetics: {
|
||||
consonants: ['b', 'c', 'd', 'f', 'g', 'h', 'j', 'k', 'l', 'm', 'n', 'p', 'q', 'r', 's', 't', 'v', 'w', 'x', 'y', 'z'],
|
||||
vowels: ['a', 'e', 'i', 'o', 'u'],
|
||||
commonEndings: ['son', 'sen', 'ton', 'man', 'er', 'ing']
|
||||
},
|
||||
culturalNotes: '美式英文名通常由名+姓组成,部分人有中间名。姓氏多源于职业、地名或父名。'
|
||||
},
|
||||
jp: { // 日本
|
||||
nameOrder: 'lastName_firstName',
|
||||
commonLastNames: ['佐藤', '鈴木', '高橋', '田中', '渡辺', '伊藤', '山本', '中村', '小林', '加藤', '吉田', '山田', '佐々木', '山口', '松本', '井上', '木村', '林', '斎藤', '清水'],
|
||||
commonFirstNames: {
|
||||
male: ['太郎', '次郎', '三郎', '健', '誠', '学', '博', '明', '清', '正', '弘', '茂', '実', '豊', '武'],
|
||||
female: ['花子', '美子', '恵子', '裕子', '直子', '由美', '真由美', '美智子', '久美子', '洋子', '智子', '美香', '麻衣', '愛', '美穂'],
|
||||
neutral: ['翼', '薫', '光', '歩', '渉', '遥', '司', '恵', '純', '望']
|
||||
},
|
||||
nameStructure: {
|
||||
pattern: '{lastName} {firstName}',
|
||||
lastNameLength: [1, 4],
|
||||
firstNameLength: [1, 3],
|
||||
totalMaxLength: 6,
|
||||
useHiragana: true,
|
||||
useKatakana: true,
|
||||
useKanji: true
|
||||
},
|
||||
phonetics: {
|
||||
hiragana: ['あ', 'い', 'う', 'え', 'お', 'か', 'き', 'く', 'け', 'こ', 'さ', 'し', 'す', 'せ', 'そ', 'た', 'ち', 'つ', 'て', 'と', 'な', 'に', 'ぬ', 'ね', 'の', 'は', 'ひ', 'ふ', 'へ', 'ほ', 'ま', 'み', 'む', 'め', 'も', 'や', 'ゆ', 'よ', 'ら', 'り', 'る', 'れ', 'ろ', 'わ', 'を', 'ん'],
|
||||
commonSyllables: ['ta', 'ka', 'sa', 'na', 'ma', 'ra', 'wa', 'shi', 'chi', 'ki', 'mi', 'ri']
|
||||
},
|
||||
culturalNotes: '日文名通常由姓+名组成,姓氏在前。可使用汉字、平假名、片假名。有特定的读音规则。'
|
||||
},
|
||||
kr: { // 韩国
|
||||
nameOrder: 'lastName_firstName',
|
||||
commonLastNames: ['김', '이', '박', '최', '정', '강', '조', '윤', '장', '임', '한', '오', '서', '신', '권', '황', '안', '송', '류', '전'],
|
||||
commonFirstNames: {
|
||||
male: ['민수', '준호', '성민', '현우', '지훈', '태현', '동현', '민준', '서준', '예준', '도윤', '시우', '주원', '하준', '선우'],
|
||||
female: ['지현', '수빈', '은영', '미영', '수진', '지영', '민정', '서연', '지우', '서현', '예원', '채원', '다은', '소영', '하은'],
|
||||
neutral: ['지은', '승민', '예진', '하늘', '슬기', '지혜', '현서', '준영', '수현', '민경']
|
||||
},
|
||||
nameStructure: {
|
||||
pattern: '{lastName} {firstName}',
|
||||
lastNameLength: [1, 2],
|
||||
firstNameLength: [2, 3],
|
||||
totalMaxLength: 4,
|
||||
useHangul: true
|
||||
},
|
||||
phonetics: {
|
||||
consonants: ['ㄱ', 'ㄴ', 'ㄷ', 'ㄹ', 'ㅁ', 'ㅂ', 'ㅅ', 'ㅇ', 'ㅈ', 'ㅊ', 'ㅋ', 'ㅌ', 'ㅍ', 'ㅎ'],
|
||||
vowels: ['ㅏ', 'ㅑ', 'ㅓ', 'ㅕ', 'ㅗ', 'ㅛ', 'ㅜ', 'ㅠ', 'ㅡ', 'ㅣ', 'ㅐ', 'ㅒ', 'ㅔ', 'ㅖ', 'ㅘ', 'ㅙ', 'ㅚ', 'ㅝ', 'ㅞ', 'ㅟ', 'ㅢ'],
|
||||
commonSyllables: ['김', '이', '박', '민', '준', '현', '수', '지', '영', '우']
|
||||
},
|
||||
culturalNotes: '韩文名通常由姓+名组成,姓氏在前。使用韩文(한글)书写,有固定的音节组合规则。'
|
||||
},
|
||||
es: { // 西班牙
|
||||
nameOrder: 'firstName_lastName_secondLastName',
|
||||
commonLastNames: ['García', 'González', 'Rodríguez', 'Fernández', 'López', 'Martínez', 'Sánchez', 'Pérez', 'Gómez', 'Martín', 'Jiménez', 'Ruiz', 'Hernández', 'Díaz', 'Moreno', 'Álvarez', 'Muñoz', 'Romero', 'Alonso', 'Gutiérrez'],
|
||||
commonFirstNames: {
|
||||
male: ['Antonio', 'José', 'Manuel', 'Francisco', 'Juan', 'David', 'José Antonio', 'José Luis', 'Jesús', 'Javier', 'Carlos', 'Miguel', 'Alejandro', 'Rafael', 'Fernando'],
|
||||
female: ['María Carmen', 'María', 'Carmen', 'Josefa', 'Isabel', 'Ana María', 'María Dolores', 'María Pilar', 'María Teresa', 'Ana', 'Francisca', 'Laura', 'Antonia', 'Dolores', 'María Ángeles'],
|
||||
neutral: ['Alejandro', 'Andrea', 'Ángel', 'Carmen', 'Gabriel', 'Patricia', 'Raquel', 'Sergio', 'Valentina', 'Xavier']
|
||||
},
|
||||
nameStructure: {
|
||||
pattern: '{firstName} {lastName} {secondLastName}',
|
||||
lastNameLength: [4, 12],
|
||||
firstNameLength: [3, 15],
|
||||
totalMaxLength: 40,
|
||||
hasSecondLastName: true,
|
||||
compoundFirstNames: true
|
||||
},
|
||||
phonetics: {
|
||||
consonants: ['b', 'c', 'd', 'f', 'g', 'h', 'j', 'k', 'l', 'm', 'n', 'ñ', 'p', 'q', 'r', 'rr', 's', 't', 'v', 'w', 'x', 'y', 'z'],
|
||||
vowels: ['a', 'e', 'i', 'o', 'u'],
|
||||
commonEndings: ['ez', 'es', 'os', 'as', 'án', 'ón']
|
||||
},
|
||||
culturalNotes: '西班牙名通常包含名字+父姓+母姓。很多名字有宗教背景,常见复合名字。'
|
||||
},
|
||||
pt: { // 葡萄牙/巴西
|
||||
nameOrder: 'firstName_middleName_lastName',
|
||||
commonLastNames: ['Silva', 'Santos', 'Ferreira', 'Pereira', 'Oliveira', 'Costa', 'Rodrigues', 'Martins', 'Jesus', 'Sousa', 'Fernandes', 'Gonçalves', 'Gomes', 'Lopes pereira', 'Marques', 'Alves', 'Almeida', 'Ribeiro', 'Pinto', 'Carvalho'],
|
||||
commonFirstNames: {
|
||||
male: ['José', 'António', 'Francisco', 'Manuel', 'Carlos', 'João', 'Pedro', 'Luis', 'Paulo', 'Rui', 'Miguel', 'Nuno', 'Ricardo', 'Bruno', 'André'],
|
||||
female: ['Maria', 'Ana', 'Manuela', 'Isabel', 'Helena', 'Teresa', 'Cristina', 'Fernanda', 'Carla', 'Paula', 'Sandra', 'Sofia', 'Catarina', 'Joana', 'Patrícia'],
|
||||
neutral: ['Alex', 'Gabriel', 'Isa', 'Pat', 'Sam', 'Val', 'Chris', 'Lou', 'Dani', 'Rafa']
|
||||
},
|
||||
nameStructure: {
|
||||
pattern: '{firstName} {middleName} {lastName}',
|
||||
lastNameLength: [4, 12],
|
||||
firstNameLength: [3, 12],
|
||||
totalMaxLength: 35,
|
||||
hasMiddleName: true,
|
||||
multipleLastNames: true
|
||||
},
|
||||
phonetics: {
|
||||
consonants: ['b', 'c', 'd', 'f', 'g', 'h', 'j', 'k', 'l', 'm', 'n', 'p', 'q', 'r', 's', 't', 'v', 'w', 'x', 'y', 'z', 'ç'],
|
||||
vowels: ['a', 'e', 'i', 'o', 'u', 'ã', 'õ', 'á', 'é', 'í', 'ó', 'ú', 'â', 'ê', 'ô'],
|
||||
commonEndings: ['es', 'as', 'os', 'ão', 'ões']
|
||||
},
|
||||
culturalNotes: '葡语名字常包含多个名字和姓氏,有天主教传统。巴西和葡萄牙略有差异。'
|
||||
},
|
||||
ru: { // 俄罗斯
|
||||
nameOrder: 'firstName_middleName_lastName',
|
||||
commonLastNames: ['Иванов', 'Смирнов', 'Кузнецов', 'Попов', 'Васильев', 'Петров', 'Соколов', 'Михайлов', 'Новиков', 'Фёдоров', 'Морозов', 'Волков', 'Алексеев', 'Лебедев', 'Семёнов', 'Егоров', 'Павлов', 'Козлов', 'Степанов', 'Николаев'],
|
||||
commonFirstNames: {
|
||||
male: ['Александр', 'Михаил', 'Максим', 'Артём', 'Даниил', 'Дмитрий', 'Кирилл', 'Андрей', 'Егор', 'Никита', 'Алексей', 'Матвей', 'Тимофей', 'Роман', 'Владимир'],
|
||||
female: ['София', 'Анастасия', 'Мария', 'Дарья', 'Анна', 'Елизавета', 'Виктория', 'Варвара', 'Полина', 'Алиса', 'Ксения', 'Елена', 'Арина', 'Екатерина', 'Александра'],
|
||||
neutral: ['Валя', 'Женя', 'Саша', 'Шура', 'Валера', 'Слава', 'Ника', 'Рина', 'Костя', 'Витя']
|
||||
},
|
||||
nameStructure: {
|
||||
pattern: '{firstName} {middleName} {lastName}',
|
||||
hasPatronymic: true,
|
||||
lastNameLength: [5, 15],
|
||||
firstNameLength: [4, 12],
|
||||
totalMaxLength: 40,
|
||||
masculineFeminineEndings: true
|
||||
},
|
||||
phonetics: {
|
||||
consonants: ['б', 'в', 'г', 'д', 'ж', 'з', 'к', 'л', 'м', 'н', 'п', 'р', 'с', 'т', 'ф', 'х', 'ц', 'ч', 'ш', 'щ'],
|
||||
vowels: ['а', 'е', 'ё', 'и', 'о', 'у', 'ы', 'э', 'ю', 'я'],
|
||||
commonEndings: {
|
||||
maleLastName: ['ов', 'ев', 'ин', 'ын', 'ский', 'цкий'],
|
||||
femaleLastName: ['ова', 'ева', 'ина', 'ына', 'ская', 'цкая']
|
||||
}
|
||||
},
|
||||
culturalNotes: '俄语名字包含名+父称+姓。父称基于父亲名字生成。姓氏有男女性变化。'
|
||||
},
|
||||
in: { // 印度
|
||||
nameOrder: 'firstName_lastName',
|
||||
commonLastNames: ['Sharma', 'Verma', 'Singh', 'Kumar', 'Gupta', 'Agarwal', 'Yadav', 'Jain', 'Mishra', 'Patel', 'Shah', 'Khan', 'Reddy', 'Nair', 'Iyer', 'Rao', 'Joshi', 'Sinha', 'Bhat', 'Menon'],
|
||||
commonFirstNames: {
|
||||
male: ['Raj', 'Amit', 'Rohit', 'Vikram', 'Arun', 'Suresh', 'Mahesh', 'Rakesh', 'Ramesh', 'Ajay', 'Sanjay', 'Pradeep', 'Deepak', 'Ashok', 'Ravi'],
|
||||
female: ['Priya', 'Kavya', 'Anita', 'Sunita', 'Meera', 'Pooja', 'Neha', 'Shreya', 'Ritu', 'Geeta', 'Seema', 'Rekha', 'Shanti', 'Lakshmi', 'Radha'],
|
||||
neutral: ['Arjun', 'Kiran', 'Shubham', 'Rohan', 'Ananya', 'Aarav', 'Devi', 'Om', 'Sai', 'Dev']
|
||||
},
|
||||
nameStructure: {
|
||||
pattern: '{firstName} {lastName}',
|
||||
lastNameLength: [3, 10],
|
||||
firstNameLength: [3, 8],
|
||||
totalMaxLength: 20,
|
||||
hasCaste: true,
|
||||
hasRegionalVariations: true
|
||||
},
|
||||
phonetics: {
|
||||
consonants: ['k', 'g', 'c', 'j', 't', 'd', 'n', 'p', 'b', 'm', 'y', 'r', 'l', 'v', 's', 'h'],
|
||||
vowels: ['a', 'i', 'u', 'e', 'o', 'aa', 'ii', 'uu', 'ai', 'au'],
|
||||
commonSyllables: ['ra', 'vi', 'ka', 'ma', 'na', 'ta', 'sa', 'ya', 'la', 'pa']
|
||||
},
|
||||
culturalNotes: '印度名字多样化,反映宗教、地区、种姓背景。很多名字有宗教或自然意义。'
|
||||
},
|
||||
th: { // 泰国
|
||||
nameOrder: 'firstName_lastName',
|
||||
commonLastNames: ['จันทร์', 'แสง', 'สุข', 'ดี', 'ใจ', 'ชัย', 'วงษ์', 'สิงห์', 'ทอง', 'เพชร', 'บุญ', 'ศรี', 'วิชัย', 'สมบัติ', 'เจริญ', 'มั่น', 'เกียรติ', 'นาค', 'มาลัย', 'เรือง'],
|
||||
commonFirstNames: {
|
||||
male: ['สมชาย', 'สมศักดิ์', 'สมพงษ์', 'วิชาย', 'วิรัช', 'ประยุทธ', 'สุรชัย', 'ธีรพงษ์', 'วิทยา', 'อรรถ', 'กิตติ', 'ชาติ', 'วันชัย', 'สมิต', 'นิรันดร์'],
|
||||
female: ['สมหญิง', 'วันเพ็ญ', 'สุวรรณ', 'มาลี', 'วิมล', 'สุนีย์', 'ปรีดา', 'สุภาพ', 'อรุณ', 'สาวิตรี', 'ลักษมี', 'วาสนา', 'กมลา', 'จันทนา', 'ศิริ'],
|
||||
neutral: ['นิรันดร์', 'สุนทร', 'อรุณ', 'เพชร', 'ทอง', 'ดาว', 'สาย', 'ขาว', 'แดง', 'ใส']
|
||||
},
|
||||
nameStructure: {
|
||||
pattern: '{firstName} {lastName}',
|
||||
lastNameLength: [3, 12],
|
||||
firstNameLength: [3, 10],
|
||||
totalMaxLength: 25,
|
||||
usesNicknames: true,
|
||||
meaningBased: true
|
||||
},
|
||||
phonetics: {
|
||||
consonants: ['ก', 'ข', 'ค', 'ง', 'จ', 'ฉ', 'ช', 'ซ', 'ญ', 'ด', 'ต', 'ถ', 'ท', 'ธ', 'น', 'บ', 'ป', 'ผ', 'ฝ', 'พ', 'ฟ', 'ภ', 'ม', 'ย', 'ร', 'ฤ', 'ล', 'ฦ', 'ว', 'ศ', 'ษ', 'ส', 'ห', 'ฬ', 'อ', 'ฮ'],
|
||||
vowels: ['ะ', 'า', 'ำ', 'ิ', 'ี', 'ึ', 'ื', 'ุ', 'ู', 'เ', 'แ', 'โ', 'ใ', 'ไ', 'ๅ', 'ๆ', '็', '่', '้', '๊', '๋'],
|
||||
tones: [5] // 泰语有5个声调
|
||||
},
|
||||
culturalNotes: '泰语名字通常有好的寓意,多使用梵语或巴利语词根。常用昵称称呼。'
|
||||
},
|
||||
vn: { // 越南
|
||||
nameOrder: 'lastName_middleName_firstName',
|
||||
commonLastNames: ['Nguyễn', 'Trần', 'Lê', 'Phạm', 'Hoàng', 'Huỳnh', 'Phan', 'Vũ', 'Võ', 'Đặng', 'Bùi', 'Đỗ', 'Hồ', 'Ngô', 'Dương', 'Lý', 'Đinh', 'Đào', 'Cao', 'Lương'],
|
||||
commonFirstNames: {
|
||||
male: ['Minh', 'Tuấn', 'Hùng', 'Dũng', 'Thành', 'Long', 'Nam', 'Hoàng', 'Quang', 'Khoa', 'Thái', 'Việt', 'Đức', 'Bảo', 'Phúc'],
|
||||
female: ['Linh', 'Hương', 'Mai', 'Lan', 'Nga', 'Hoa', 'Trang', 'Thảo', 'Hạnh', 'Thu', 'Phương', 'Ánh', 'Tuyết', 'Yến', 'Xuân'],
|
||||
neutral: ['An', 'Bình', 'Thanh', 'Kim', 'Hạnh', 'Phúc', 'Tâm', 'Thành', 'Việt', 'Quý']
|
||||
},
|
||||
nameStructure: {
|
||||
pattern: '{lastName} {middleName} {firstName}',
|
||||
lastNameLength: [2, 8],
|
||||
firstNameLength: [2, 6],
|
||||
totalMaxLength: 20,
|
||||
hasMiddleName: true,
|
||||
meaningBased: true
|
||||
},
|
||||
phonetics: {
|
||||
consonants: ['b', 'c', 'd', 'đ', 'g', 'h', 'k', 'l', 'm', 'n', 'p', 'q', 'r', 's', 't', 'v', 'x', 'y'],
|
||||
vowels: ['a', 'ă', 'â', 'e', 'ê', 'i', 'o', 'ô', 'ơ', 'u', 'ư', 'y'],
|
||||
tones: ['không dấu', 'huyền', 'sắc', 'hỏi', 'ngã', 'nặng']
|
||||
},
|
||||
culturalNotes: '越南名字通常是姓+中间名+名。70%的人姓Nguyen。名字多有美好寓意。'
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取指定文化的规则
|
||||
* @param {string} culture 文化代码
|
||||
* @returns {Object} 文化规则
|
||||
*/
|
||||
getCultureRules(culture) {
|
||||
const rules = this.cultureRules[culture.toLowerCase()];
|
||||
if (!rules) {
|
||||
this.logger.warn(`未知文化: ${culture},使用默认美国规则`);
|
||||
return this.cultureRules.us;
|
||||
}
|
||||
return rules;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据文化背景生成符合习惯的姓名
|
||||
* @param {string} culture 文化代码
|
||||
* @param {string} gender 性别
|
||||
* @param {Object} options 选项
|
||||
* @returns {Object} 生成的姓名
|
||||
*/
|
||||
generateCulturalName(culture, gender = 'neutral', options = {}) {
|
||||
const rules = this.getCultureRules(culture);
|
||||
const genderNames = rules.commonFirstNames[gender] || rules.commonFirstNames.neutral;
|
||||
|
||||
const firstName = this.getRandomItem(genderNames);
|
||||
const lastName = this.getRandomItem(rules.commonLastNames);
|
||||
|
||||
const name = {
|
||||
firstName,
|
||||
lastName,
|
||||
culture,
|
||||
gender
|
||||
};
|
||||
|
||||
// 根据文化特点添加额外字段
|
||||
if (rules.nameStructure.hasMiddleName) {
|
||||
name.middleName = this.generateMiddleName(rules, gender);
|
||||
}
|
||||
|
||||
if (rules.nameStructure.hasSecondLastName) {
|
||||
name.secondLastName = this.getRandomItem(rules.commonLastNames);
|
||||
}
|
||||
|
||||
if (rules.nameStructure.hasPatronymic) {
|
||||
name.patronymic = this.generatePatronymic(rules, gender, options.fatherName);
|
||||
}
|
||||
|
||||
// 生成显示名称
|
||||
name.displayName = this.formatDisplayName(name, rules);
|
||||
|
||||
return name;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成中间名
|
||||
* @param {Object} rules 文化规则
|
||||
* @param {string} gender 性别
|
||||
* @returns {string} 中间名
|
||||
*/
|
||||
generateMiddleName(rules, gender) {
|
||||
if (rules.commonFirstNames[gender]) {
|
||||
return this.getRandomItem(rules.commonFirstNames[gender]);
|
||||
}
|
||||
return this.getRandomItem(rules.commonFirstNames.neutral || []);
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成父称(俄语等)
|
||||
* @param {Object} rules 文化规则
|
||||
* @param {string} gender 性别
|
||||
* @param {string} fatherName 父亲名字
|
||||
* @returns {string} 父称
|
||||
*/
|
||||
generatePatronymic(rules, gender, fatherName) {
|
||||
if (!fatherName) {
|
||||
fatherName = this.getRandomItem(rules.commonFirstNames.male || rules.commonFirstNames.neutral);
|
||||
}
|
||||
|
||||
// 俄语父称规则
|
||||
if (rules === this.cultureRules.ru) {
|
||||
const base = fatherName.replace(/[ая]$/, '');
|
||||
return gender === 'female' ? `${base}овна` : `${base}ович`;
|
||||
}
|
||||
|
||||
return fatherName;
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化显示名称
|
||||
* @param {Object} name 姓名对象
|
||||
* @param {Object} rules 文化规则
|
||||
* @returns {string} 格式化的显示名称
|
||||
*/
|
||||
formatDisplayName(name, rules) {
|
||||
let pattern = rules.nameStructure.pattern;
|
||||
|
||||
pattern = pattern.replace('{firstName}', name.firstName || '');
|
||||
pattern = pattern.replace('{lastName}', name.lastName || '');
|
||||
pattern = pattern.replace('{middleName}', name.middleName || '');
|
||||
pattern = pattern.replace('{secondLastName}', name.secondLastName || '');
|
||||
pattern = pattern.replace('{patronymic}', name.patronymic || '');
|
||||
|
||||
return pattern.replace(/\s+/g, ' ').trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证姓名是否符合文化规则
|
||||
* @param {Object} name 姓名对象
|
||||
* @param {string} culture 文化代码
|
||||
* @returns {boolean} 是否符合规则
|
||||
*/
|
||||
validateCulturalName(name, culture) {
|
||||
const rules = this.getCultureRules(culture);
|
||||
|
||||
// 检查必要字段
|
||||
if (!name.firstName && !name.displayName) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 检查长度限制
|
||||
if (rules.nameStructure.totalMaxLength) {
|
||||
const totalLength = (name.firstName || '').length + (name.lastName || '').length + (name.middleName || '').length;
|
||||
if (totalLength > rules.nameStructure.totalMaxLength) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// 检查名字长度
|
||||
if (name.firstName && rules.nameStructure.firstNameLength) {
|
||||
const [min, max] = rules.nameStructure.firstNameLength;
|
||||
if (name.firstName.length < min || name.firstName.length > max) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// 检查姓氏长度
|
||||
if (name.lastName && rules.nameStructure.lastNameLength) {
|
||||
const [min, max] = rules.nameStructure.lastNameLength;
|
||||
if (name.lastName.length < min || name.lastName.length > max) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取文化的特征描述
|
||||
* @param {string} culture 文化代码
|
||||
* @returns {Object} 文化特征
|
||||
*/
|
||||
getCultureFeatures(culture) {
|
||||
const rules = this.getCultureRules(culture);
|
||||
return {
|
||||
culture,
|
||||
nameOrder: rules.nameOrder,
|
||||
nameStructure: rules.nameStructure,
|
||||
culturalNotes: rules.culturalNotes,
|
||||
commonPatterns: {
|
||||
lastNameCount: rules.commonLastNames.length,
|
||||
firstNameCount: Object.values(rules.commonFirstNames).flat().length
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有支持的文化列表
|
||||
* @returns {Array<string>} 支持的文化列表
|
||||
*/
|
||||
getSupportedCultures() {
|
||||
return Object.keys(this.cultureRules);
|
||||
}
|
||||
|
||||
/**
|
||||
* 从数组中随机获取一个元素
|
||||
* @param {Array} array 数组
|
||||
* @returns {*} 随机元素
|
||||
*/
|
||||
getRandomItem(array) {
|
||||
if (!array || array.length === 0) return '';
|
||||
return array[Math.floor(Math.random() * array.length)];
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据语音规则生成姓名
|
||||
* @param {string} culture 文化代码
|
||||
* @param {number} syllableCount 音节数
|
||||
* @returns {string} 生成的姓名
|
||||
*/
|
||||
generatePhoneticName(culture, syllableCount = 2) {
|
||||
const rules = this.getCultureRules(culture);
|
||||
if (!rules.phonetics) return '';
|
||||
|
||||
let name = '';
|
||||
for (let i = 0; i < syllableCount; i++) {
|
||||
const consonant = this.getRandomItem(rules.phonetics.consonants);
|
||||
const vowel = this.getRandomItem(rules.phonetics.vowels);
|
||||
name += consonant + vowel;
|
||||
}
|
||||
|
||||
return name;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = CultureAdapter;
|
||||
433
backend/src/adapters/PlatformAdapter.js
Normal file
@@ -0,0 +1,433 @@
|
||||
const Logger = require("@src/util/Log4jUtil");
|
||||
|
||||
/**
|
||||
* 平台适配器 - 处理不同信使平台的姓名规则和格式
|
||||
*/
|
||||
class PlatformAdapter {
|
||||
static getInstance() {
|
||||
if (!PlatformAdapter.instance) {
|
||||
PlatformAdapter.instance = new PlatformAdapter();
|
||||
}
|
||||
return PlatformAdapter.instance;
|
||||
}
|
||||
|
||||
constructor() {
|
||||
this.logger = Logger.getLogger("PlatformAdapter");
|
||||
this.platformRules = this.initializePlatformRules();
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化各平台规则
|
||||
*/
|
||||
initializePlatformRules() {
|
||||
return {
|
||||
telegram: {
|
||||
nameStructure: 'firstName_lastName',
|
||||
maxFirstNameLength: 64,
|
||||
maxLastNameLength: 64,
|
||||
allowEmoji: true,
|
||||
allowSpecialChars: true,
|
||||
supportedCharsets: ['unicode'],
|
||||
requiredFields: ['firstName'],
|
||||
optionalFields: ['lastName'],
|
||||
displayFormat: '{firstName} {lastName}',
|
||||
validation: {
|
||||
minLength: 1,
|
||||
maxLength: 64,
|
||||
allowNumbers: true,
|
||||
allowSpaces: true,
|
||||
forbiddenChars: []
|
||||
}
|
||||
},
|
||||
whatsapp: {
|
||||
nameStructure: 'displayName',
|
||||
maxDisplayNameLength: 25,
|
||||
allowEmoji: true,
|
||||
allowSpecialChars: true,
|
||||
supportedCharsets: ['unicode'],
|
||||
requiredFields: ['displayName'],
|
||||
optionalFields: [],
|
||||
displayFormat: '{displayName}',
|
||||
validation: {
|
||||
minLength: 1,
|
||||
maxLength: 25,
|
||||
allowNumbers: true,
|
||||
allowSpaces: true,
|
||||
forbiddenChars: []
|
||||
}
|
||||
},
|
||||
signal: {
|
||||
nameStructure: 'profileName',
|
||||
maxProfileNameLength: 26,
|
||||
allowEmoji: true,
|
||||
allowSpecialChars: true,
|
||||
supportedCharsets: ['unicode'],
|
||||
requiredFields: [],
|
||||
optionalFields: ['profileName'],
|
||||
displayFormat: '{profileName}',
|
||||
validation: {
|
||||
minLength: 0,
|
||||
maxLength: 26,
|
||||
allowNumbers: true,
|
||||
allowSpaces: true,
|
||||
forbiddenChars: []
|
||||
}
|
||||
},
|
||||
wechat: {
|
||||
nameStructure: 'nickname',
|
||||
maxNicknameLength: 20,
|
||||
allowEmoji: true,
|
||||
allowSpecialChars: true,
|
||||
supportedCharsets: ['unicode', 'chinese'],
|
||||
requiredFields: ['nickname'],
|
||||
optionalFields: [],
|
||||
displayFormat: '{nickname}',
|
||||
validation: {
|
||||
minLength: 1,
|
||||
maxLength: 20,
|
||||
allowNumbers: true,
|
||||
allowSpaces: true,
|
||||
forbiddenChars: ['@', '#']
|
||||
}
|
||||
},
|
||||
line: {
|
||||
nameStructure: 'displayName',
|
||||
maxDisplayNameLength: 20,
|
||||
allowEmoji: true,
|
||||
allowSpecialChars: true,
|
||||
supportedCharsets: ['unicode', 'japanese', 'korean'],
|
||||
requiredFields: ['displayName'],
|
||||
optionalFields: [],
|
||||
displayFormat: '{displayName}',
|
||||
validation: {
|
||||
minLength: 1,
|
||||
maxLength: 20,
|
||||
allowNumbers: true,
|
||||
allowSpaces: true,
|
||||
forbiddenChars: []
|
||||
}
|
||||
},
|
||||
discord: {
|
||||
nameStructure: 'username_discriminator',
|
||||
maxUsernameLength: 32,
|
||||
allowEmoji: false,
|
||||
allowSpecialChars: false,
|
||||
supportedCharsets: ['ascii'],
|
||||
requiredFields: ['username'],
|
||||
optionalFields: [],
|
||||
displayFormat: '{username}#{discriminator}',
|
||||
validation: {
|
||||
minLength: 2,
|
||||
maxLength: 32,
|
||||
allowNumbers: true,
|
||||
allowSpaces: false,
|
||||
forbiddenChars: ['@', '#', ':', '```']
|
||||
}
|
||||
},
|
||||
facebook_messenger: {
|
||||
nameStructure: 'firstName_lastName',
|
||||
maxFirstNameLength: 50,
|
||||
maxLastNameLength: 50,
|
||||
allowEmoji: true,
|
||||
allowSpecialChars: true,
|
||||
supportedCharsets: ['unicode'],
|
||||
requiredFields: ['firstName'],
|
||||
optionalFields: ['lastName'],
|
||||
displayFormat: '{firstName} {lastName}',
|
||||
validation: {
|
||||
minLength: 1,
|
||||
maxLength: 50,
|
||||
allowNumbers: true,
|
||||
allowSpaces: true,
|
||||
forbiddenChars: []
|
||||
}
|
||||
},
|
||||
instagram: {
|
||||
nameStructure: 'fullName',
|
||||
maxFullNameLength: 50,
|
||||
allowEmoji: true,
|
||||
allowSpecialChars: true,
|
||||
supportedCharsets: ['unicode'],
|
||||
requiredFields: [],
|
||||
optionalFields: ['fullName'],
|
||||
displayFormat: '{fullName}',
|
||||
validation: {
|
||||
minLength: 0,
|
||||
maxLength: 50,
|
||||
allowNumbers: true,
|
||||
allowSpaces: true,
|
||||
forbiddenChars: []
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取指定平台的规则
|
||||
* @param {string} platform 平台名称
|
||||
* @returns {Object} 平台规则
|
||||
*/
|
||||
getPlatformRules(platform) {
|
||||
const rules = this.platformRules[platform.toLowerCase()];
|
||||
if (!rules) {
|
||||
this.logger.warn(`未知平台: ${platform},使用默认Telegram规则`);
|
||||
return this.platformRules.telegram;
|
||||
}
|
||||
return rules;
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化姓名以适应指定平台
|
||||
* @param {Object} name 原始姓名对象
|
||||
* @param {string} platform 目标平台
|
||||
* @returns {Object} 格式化后的姓名
|
||||
*/
|
||||
formatName(name, platform) {
|
||||
const rules = this.getPlatformRules(platform);
|
||||
const formatted = { ...name };
|
||||
|
||||
switch (rules.nameStructure) {
|
||||
case 'firstName_lastName':
|
||||
formatted.firstName = this.truncateAndValidate(name.firstName || '', rules.maxFirstNameLength || 64, rules);
|
||||
formatted.lastName = this.truncateAndValidate(name.lastName || '', rules.maxLastNameLength || 64, rules);
|
||||
formatted.displayName = rules.displayFormat
|
||||
.replace('{firstName}', formatted.firstName)
|
||||
.replace('{lastName}', formatted.lastName)
|
||||
.trim();
|
||||
break;
|
||||
|
||||
case 'displayName':
|
||||
formatted.displayName = this.truncateAndValidate(
|
||||
name.displayName || `${name.firstName || ''} ${name.lastName || ''}`.trim(),
|
||||
rules.maxDisplayNameLength || 25,
|
||||
rules
|
||||
);
|
||||
break;
|
||||
|
||||
case 'profileName':
|
||||
formatted.profileName = this.truncateAndValidate(
|
||||
name.profileName || name.displayName || `${name.firstName || ''} ${name.lastName || ''}`.trim(),
|
||||
rules.maxProfileNameLength || 26,
|
||||
rules
|
||||
);
|
||||
break;
|
||||
|
||||
case 'nickname':
|
||||
formatted.nickname = this.truncateAndValidate(
|
||||
name.nickname || name.displayName || `${name.firstName || ''} ${name.lastName || ''}`.trim(),
|
||||
rules.maxNicknameLength || 20,
|
||||
rules
|
||||
);
|
||||
break;
|
||||
|
||||
case 'username_discriminator':
|
||||
formatted.username = this.truncateAndValidate(
|
||||
this.toValidUsername(name.firstName || name.displayName || 'user'),
|
||||
rules.maxUsernameLength || 32,
|
||||
rules
|
||||
);
|
||||
formatted.discriminator = this.generateDiscriminator();
|
||||
formatted.displayName = `${formatted.username}#${formatted.discriminator}`;
|
||||
break;
|
||||
|
||||
case 'fullName':
|
||||
formatted.fullName = this.truncateAndValidate(
|
||||
name.fullName || `${name.firstName || ''} ${name.lastName || ''}`.trim(),
|
||||
rules.maxFullNameLength || 50,
|
||||
rules
|
||||
);
|
||||
break;
|
||||
|
||||
default:
|
||||
this.logger.warn(`未知的姓名结构: ${rules.nameStructure}`);
|
||||
break;
|
||||
}
|
||||
|
||||
// 确保必填字段存在
|
||||
this.ensureRequiredFields(formatted, rules);
|
||||
|
||||
return formatted;
|
||||
}
|
||||
|
||||
/**
|
||||
* 截断并验证字符串
|
||||
* @param {string} str 原始字符串
|
||||
* @param {number} maxLength 最大长度
|
||||
* @param {Object} rules 验证规则
|
||||
* @returns {string} 处理后的字符串
|
||||
*/
|
||||
truncateAndValidate(str, maxLength, rules) {
|
||||
if (!str) return '';
|
||||
|
||||
// 移除禁用字符
|
||||
let cleaned = str;
|
||||
if (rules.validation && rules.validation.forbiddenChars) {
|
||||
for (const char of rules.validation.forbiddenChars) {
|
||||
cleaned = cleaned.replace(new RegExp(`\\${char}`, 'g'), '');
|
||||
}
|
||||
}
|
||||
|
||||
// 处理特殊字符和emoji
|
||||
if (!rules.allowEmoji) {
|
||||
cleaned = this.removeEmojis(cleaned);
|
||||
}
|
||||
if (!rules.allowSpecialChars) {
|
||||
cleaned = this.removeSpecialChars(cleaned);
|
||||
}
|
||||
|
||||
// 处理空格
|
||||
if (rules.validation && !rules.validation.allowSpaces) {
|
||||
cleaned = cleaned.replace(/\s+/g, '');
|
||||
}
|
||||
|
||||
// 截断到最大长度
|
||||
if (cleaned.length > maxLength) {
|
||||
cleaned = cleaned.substring(0, maxLength).trim();
|
||||
}
|
||||
|
||||
// 确保最小长度
|
||||
if (rules.validation && rules.validation.minLength && cleaned.length < rules.validation.minLength) {
|
||||
cleaned = cleaned.padEnd(rules.validation.minLength, '0');
|
||||
}
|
||||
|
||||
return cleaned;
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换为有效的用户名格式
|
||||
* @param {string} name 原始名称
|
||||
* @returns {string} 用户名格式
|
||||
*/
|
||||
toValidUsername(name) {
|
||||
return name
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9_]/g, '')
|
||||
.replace(/_{2,}/g, '_')
|
||||
.replace(/^_+|_+$/g, '');
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成随机判别符(如Discord的#1234)
|
||||
* @returns {string} 4位数字判别符
|
||||
*/
|
||||
generateDiscriminator() {
|
||||
return Math.floor(Math.random() * 9999).toString().padStart(4, '0');
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除emoji
|
||||
* @param {string} str 原始字符串
|
||||
* @returns {string} 移除emoji后的字符串
|
||||
*/
|
||||
removeEmojis(str) {
|
||||
return str.replace(/[\u{1f300}-\u{1f5ff}\u{1f900}-\u{1f9ff}\u{1f600}-\u{1f64f}\u{1f680}-\u{1f6ff}\u{2600}-\u{26ff}\u{2700}-\u{27bf}\u{1f1e6}-\u{1f1ff}\u{1f191}-\u{1f251}\u{1f004}\u{1f0cf}\u{1f170}-\u{1f171}\u{1f17e}-\u{1f17f}\u{1f18e}\u{3030}\u{2b50}\u{2b55}\u{2934}-\u{2935}\u{2b05}-\u{2b07}\u{2b1b}-\u{2b1c}\u{3297}\u{3299}\u{303d}\u{00a9}\u{00ae}\u{2122}\u{23f3}\u{24c2}\u{23e9}-\u{23ef}\u{25b6}\u{23f8}-\u{23fa}]/gu, '');
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除特殊字符
|
||||
* @param {string} str 原始字符串
|
||||
* @returns {string} 移除特殊字符后的字符串
|
||||
*/
|
||||
removeSpecialChars(str) {
|
||||
return str.replace(/[^\w\s]/g, '');
|
||||
}
|
||||
|
||||
/**
|
||||
* 确保必填字段存在
|
||||
* @param {Object} name 姓名对象
|
||||
* @param {Object} rules 平台规则
|
||||
*/
|
||||
ensureRequiredFields(name, rules) {
|
||||
for (const field of rules.requiredFields) {
|
||||
if (!name[field] || name[field].trim().length === 0) {
|
||||
switch (field) {
|
||||
case 'firstName':
|
||||
name[field] = 'User';
|
||||
break;
|
||||
case 'lastName':
|
||||
name[field] = 'Unknown';
|
||||
break;
|
||||
case 'displayName':
|
||||
case 'profileName':
|
||||
case 'nickname':
|
||||
case 'fullName':
|
||||
name[field] = 'Anonymous User';
|
||||
break;
|
||||
case 'username':
|
||||
name[field] = 'anonymous';
|
||||
break;
|
||||
default:
|
||||
name[field] = 'Unknown';
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证姓名是否符合平台规则
|
||||
* @param {Object} name 姓名对象
|
||||
* @param {string} platform 平台名称
|
||||
* @returns {boolean} 是否符合规则
|
||||
*/
|
||||
validateName(name, platform) {
|
||||
const rules = this.getPlatformRules(platform);
|
||||
|
||||
// 检查必填字段
|
||||
for (const field of rules.requiredFields) {
|
||||
if (!name[field] || name[field].trim().length === 0) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// 检查字段长度和格式
|
||||
for (const [field, value] of Object.entries(name)) {
|
||||
if (value && typeof value === 'string') {
|
||||
const maxLength = rules[`max${field.charAt(0).toUpperCase() + field.slice(1)}Length`];
|
||||
if (maxLength && value.length > maxLength) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (rules.validation) {
|
||||
if (value.length < rules.validation.minLength) {
|
||||
return false;
|
||||
}
|
||||
if (rules.validation.forbiddenChars.some(char => value.includes(char))) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有支持的平台列表
|
||||
* @returns {Array<string>} 支持的平台列表
|
||||
*/
|
||||
getSupportedPlatforms() {
|
||||
return Object.keys(this.platformRules);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取平台的特性描述
|
||||
* @param {string} platform 平台名称
|
||||
* @returns {Object} 平台特性
|
||||
*/
|
||||
getPlatformFeatures(platform) {
|
||||
const rules = this.getPlatformRules(platform);
|
||||
return {
|
||||
platform,
|
||||
nameStructure: rules.nameStructure,
|
||||
allowEmoji: rules.allowEmoji,
|
||||
allowSpecialChars: rules.allowSpecialChars,
|
||||
supportedCharsets: rules.supportedCharsets,
|
||||
requiredFields: rules.requiredFields,
|
||||
optionalFields: rules.optionalFields,
|
||||
validation: rules.validation
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = PlatformAdapter;
|
||||
562
backend/src/adapters/RolaIPAdapter.js
Normal file
@@ -0,0 +1,562 @@
|
||||
const BaseProxyAdapter = require('./BaseProxyAdapter');
|
||||
const crypto = require('crypto');
|
||||
|
||||
/**
|
||||
* Rola-IP代理平台适配器
|
||||
* 官方网站: https://admin.rola-ip.co
|
||||
*/
|
||||
class RolaIPAdapter extends BaseProxyAdapter {
|
||||
constructor(config) {
|
||||
super(config);
|
||||
|
||||
this.baseUrl = config.apiUrl || 'http://api.rola-ip.co';
|
||||
this.username = config.username;
|
||||
this.password = config.password;
|
||||
this.sessionToken = null;
|
||||
this.language = config.language || 'zh'; // zh/en
|
||||
|
||||
// Rola-IP 特有配置
|
||||
this.endpoints = {
|
||||
// 可能的登录端点
|
||||
login: '/login',
|
||||
loginAlt: '/api/login',
|
||||
loginUser: '/user/login',
|
||||
logout: '/api/user/logout',
|
||||
balance: '/api/user/balance',
|
||||
proxies: '/api/proxy/list',
|
||||
extract: '/api/proxy/extract',
|
||||
check: '/api/proxy/check',
|
||||
whitelist: '/api/user/whitelist',
|
||||
statistics: '/api/user/statistics'
|
||||
};
|
||||
|
||||
this.proxyTypeMap = {
|
||||
'residential': 'residential',
|
||||
'datacenter': 'datacenter',
|
||||
'mobile': 'mobile',
|
||||
'static_residential': 'static',
|
||||
'ipv6': 'ipv6'
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户认证
|
||||
* @returns {Promise<boolean>}
|
||||
*/
|
||||
async authenticate() {
|
||||
try {
|
||||
if (!this.username || !this.password) {
|
||||
throw new Error('用户名和密码不能为空');
|
||||
}
|
||||
|
||||
this.logger.info('开始Rola-IP平台认证...');
|
||||
|
||||
// 尝试不同的字段名组合
|
||||
const loginDataVariations = [
|
||||
{
|
||||
username: this.username,
|
||||
password: this.password,
|
||||
lang: this.language
|
||||
},
|
||||
{
|
||||
user: this.username,
|
||||
pass: this.password,
|
||||
language: this.language
|
||||
},
|
||||
{
|
||||
email: this.username,
|
||||
password: this.password
|
||||
},
|
||||
{
|
||||
account: this.username,
|
||||
pwd: this.password
|
||||
}
|
||||
];
|
||||
|
||||
// 尝试多个可能的登录端点
|
||||
const loginEndpoints = [
|
||||
this.endpoints.login, // /login
|
||||
this.endpoints.loginAlt, // /api/login
|
||||
this.endpoints.loginUser // /user/login
|
||||
];
|
||||
|
||||
let lastError = null;
|
||||
|
||||
for (const endpoint of loginEndpoints) {
|
||||
for (const loginData of loginDataVariations) {
|
||||
try {
|
||||
this.logger.info(`尝试登录端点: ${endpoint}, 数据:`, JSON.stringify(loginData, null, 2));
|
||||
|
||||
const response = await this.makeRequest(
|
||||
'POST',
|
||||
`${this.baseUrl}${endpoint}`,
|
||||
loginData
|
||||
);
|
||||
|
||||
this.logger.info(`端点 ${endpoint} 响应:`, JSON.stringify(response, null, 2));
|
||||
|
||||
if (response.code === 200 || response.success || response.status === 'ok') {
|
||||
this.sessionToken = response.data?.token || response.token || response.access_token;
|
||||
this.authenticated = true;
|
||||
this.lastAuthTime = Date.now();
|
||||
|
||||
this.logger.info(`Rola-IP认证成功,使用端点: ${endpoint}`);
|
||||
return true;
|
||||
} else {
|
||||
this.logger.warn(`端点 ${endpoint} 返回错误:`, response.msg || response.message);
|
||||
lastError = new Error(response.msg || response.message || '认证失败');
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.warn(`端点 ${endpoint} 请求失败:`, error.message);
|
||||
lastError = error;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 所有端点都失败了
|
||||
throw lastError || new Error('所有登录端点都无法连接');
|
||||
|
||||
} catch (error) {
|
||||
this.logger.error('Rola-IP认证失败:', error.message);
|
||||
this.authenticated = false;
|
||||
this.sessionToken = null;
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取代理列表
|
||||
* @param {Object} options 查询选项
|
||||
* @returns {Promise<Array>}
|
||||
*/
|
||||
async getProxyList(options = {}) {
|
||||
await this.ensureAuthenticated();
|
||||
|
||||
try {
|
||||
const params = {
|
||||
type: this.proxyTypeMap[options.type] || 'residential',
|
||||
country: options.country || '',
|
||||
count: options.limit || 100,
|
||||
format: 'json',
|
||||
protocol: options.protocol || 'http'
|
||||
};
|
||||
|
||||
// 如果指定了城市
|
||||
if (options.city) {
|
||||
params.city = options.city;
|
||||
}
|
||||
|
||||
// 如果指定了运营商
|
||||
if (options.isp) {
|
||||
params.isp = options.isp;
|
||||
}
|
||||
|
||||
const response = await this.makeRequest(
|
||||
'GET',
|
||||
`${this.baseUrl}${this.endpoints.proxies}`,
|
||||
params,
|
||||
{
|
||||
'Authorization': `Bearer ${this.sessionToken}`,
|
||||
'Accept-Language': this.language
|
||||
}
|
||||
);
|
||||
|
||||
if (response.code === 200 || response.success) {
|
||||
return this.parseProxyList(response.data || response.proxies || []);
|
||||
} else {
|
||||
throw new Error(response.message || '获取代理列表失败');
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
this.logger.error('获取Rola-IP代理列表失败:', error.message);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 提取代理IP
|
||||
* @param {Object} options 提取选项
|
||||
* @returns {Promise<Array>}
|
||||
*/
|
||||
async extractProxies(options = {}) {
|
||||
await this.ensureAuthenticated();
|
||||
|
||||
try {
|
||||
const extractData = {
|
||||
type: this.proxyTypeMap[options.type] || 'residential',
|
||||
count: options.count || 10,
|
||||
country: options.country || '',
|
||||
format: options.format || 'json',
|
||||
protocol: options.protocol || 'http',
|
||||
duration: options.duration || 60 // 代理有效期(分钟)
|
||||
};
|
||||
|
||||
const response = await this.makeRequest(
|
||||
'POST',
|
||||
`${this.baseUrl}${this.endpoints.extract}`,
|
||||
extractData,
|
||||
{
|
||||
'Authorization': `Bearer ${this.sessionToken}`,
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
);
|
||||
|
||||
if (response.code === 200 || response.success) {
|
||||
return this.parseProxyList(response.data || response.proxies || []);
|
||||
} else {
|
||||
throw new Error(response.message || '提取代理失败');
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
this.logger.error('提取Rola-IP代理失败:', error.message);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检测代理可用性
|
||||
* @param {Object} proxy 代理信息
|
||||
* @returns {Promise<Object>}
|
||||
*/
|
||||
async checkProxy(proxy) {
|
||||
const proxyFormatted = {
|
||||
ip_address: proxy.ip_address || proxy.ip,
|
||||
port: proxy.port,
|
||||
username: proxy.username,
|
||||
password: proxy.password,
|
||||
protocol: proxy.protocol || 'http'
|
||||
};
|
||||
|
||||
// 使用基类的通用检测方法
|
||||
const baseResult = await this.performProxyCheck(proxyFormatted);
|
||||
|
||||
// Rola-IP特有的检测逻辑
|
||||
try {
|
||||
await this.ensureAuthenticated();
|
||||
|
||||
// 调用平台自己的检测接口
|
||||
const response = await this.makeRequest(
|
||||
'POST',
|
||||
`${this.baseUrl}${this.endpoints.check}`,
|
||||
{
|
||||
ip: proxyFormatted.ip_address,
|
||||
port: proxyFormatted.port,
|
||||
type: proxy.proxy_type || 'residential'
|
||||
},
|
||||
{
|
||||
'Authorization': `Bearer ${this.sessionToken}`
|
||||
}
|
||||
);
|
||||
|
||||
if (response.code === 200 || response.success) {
|
||||
return {
|
||||
...baseResult,
|
||||
platformCheck: {
|
||||
status: response.data?.status || 'unknown',
|
||||
location: response.data?.location,
|
||||
isp: response.data?.isp,
|
||||
asn: response.data?.asn,
|
||||
platformResult: true
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
this.logger.warn('平台检测接口调用失败:', error.message);
|
||||
}
|
||||
|
||||
return baseResult;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取账户余额
|
||||
* @returns {Promise<Object>}
|
||||
*/
|
||||
async getBalance() {
|
||||
await this.ensureAuthenticated();
|
||||
|
||||
try {
|
||||
const response = await this.makeRequest(
|
||||
'GET',
|
||||
`${this.baseUrl}${this.endpoints.balance}`,
|
||||
null,
|
||||
{
|
||||
'Authorization': `Bearer ${this.sessionToken}`
|
||||
}
|
||||
);
|
||||
|
||||
if (response.code === 200 || response.success) {
|
||||
const balanceData = response.data || response;
|
||||
return {
|
||||
balance: parseFloat(balanceData.balance || 0),
|
||||
currency: balanceData.currency || 'USD',
|
||||
credit: parseFloat(balanceData.credit || 0),
|
||||
expires_at: balanceData.expires_at,
|
||||
package_info: balanceData.package_info,
|
||||
traffic_used: balanceData.traffic_used,
|
||||
traffic_total: balanceData.traffic_total
|
||||
};
|
||||
} else {
|
||||
throw new Error(response.message || '获取余额失败');
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
this.logger.error('获取Rola-IP余额失败:', error.message);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取使用统计
|
||||
* @param {Object} options 查询选项
|
||||
* @returns {Promise<Object>}
|
||||
*/
|
||||
async getStatistics(options = {}) {
|
||||
await this.ensureAuthenticated();
|
||||
|
||||
try {
|
||||
const params = {
|
||||
start_date: options.start_date || this.getDateString(-7), // 默认7天前
|
||||
end_date: options.end_date || this.getDateString(0), // 今天
|
||||
group_by: options.group_by || 'day' // day, hour
|
||||
};
|
||||
|
||||
const response = await this.makeRequest(
|
||||
'GET',
|
||||
`${this.baseUrl}${this.endpoints.statistics}`,
|
||||
params,
|
||||
{
|
||||
'Authorization': `Bearer ${this.sessionToken}`
|
||||
}
|
||||
);
|
||||
|
||||
if (response.code === 200 || response.success) {
|
||||
return this.parseStatistics(response.data || response);
|
||||
} else {
|
||||
throw new Error(response.message || '获取统计信息失败');
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
this.logger.error('获取Rola-IP统计信息失败:', error.message);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 管理IP白名单
|
||||
* @param {string} action 操作类型: add, remove, list
|
||||
* @param {string} ip IP地址
|
||||
* @returns {Promise<Object>}
|
||||
*/
|
||||
async manageWhitelist(action, ip = null) {
|
||||
await this.ensureAuthenticated();
|
||||
|
||||
try {
|
||||
let endpoint = `${this.baseUrl}${this.endpoints.whitelist}`;
|
||||
let method = 'GET';
|
||||
let data = null;
|
||||
|
||||
switch (action) {
|
||||
case 'add':
|
||||
method = 'POST';
|
||||
data = { ip, action: 'add' };
|
||||
break;
|
||||
case 'remove':
|
||||
method = 'POST';
|
||||
data = { ip, action: 'remove' };
|
||||
break;
|
||||
case 'list':
|
||||
default:
|
||||
method = 'GET';
|
||||
break;
|
||||
}
|
||||
|
||||
const response = await this.makeRequest(
|
||||
method,
|
||||
endpoint,
|
||||
data,
|
||||
{
|
||||
'Authorization': `Bearer ${this.sessionToken}`
|
||||
}
|
||||
);
|
||||
|
||||
if (response.code === 200 || response.success) {
|
||||
return response.data || response;
|
||||
} else {
|
||||
throw new Error(response.message || '白名单操作失败');
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
this.logger.error('Rola-IP白名单操作失败:', error.message);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析代理列表
|
||||
* @param {Array} rawProxies 原始代理数据
|
||||
* @returns {Array}
|
||||
*/
|
||||
parseProxyList(rawProxies) {
|
||||
if (!Array.isArray(rawProxies)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return rawProxies.map(proxy => {
|
||||
// Rola-IP返回格式可能是 ip:port 或者对象格式
|
||||
if (typeof proxy === 'string') {
|
||||
const [ip, port] = proxy.split(':');
|
||||
return this.formatProxy({
|
||||
ip,
|
||||
port: parseInt(port),
|
||||
type: 'residential',
|
||||
protocol: 'http'
|
||||
});
|
||||
}
|
||||
|
||||
return this.formatProxy({
|
||||
ip: proxy.ip || proxy.host,
|
||||
port: proxy.port,
|
||||
username: proxy.username || proxy.user,
|
||||
password: proxy.password || proxy.pass,
|
||||
type: proxy.type || 'residential',
|
||||
country: proxy.country,
|
||||
city: proxy.city,
|
||||
protocol: proxy.protocol || 'http',
|
||||
expires_at: proxy.expires_at || proxy.expire_time
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析统计信息
|
||||
* @param {Object} rawStats 原始统计数据
|
||||
* @returns {Object}
|
||||
*/
|
||||
parseStatistics(rawStats) {
|
||||
return {
|
||||
total_requests: rawStats.total_requests || 0,
|
||||
successful_requests: rawStats.successful_requests || 0,
|
||||
failed_requests: rawStats.failed_requests || 0,
|
||||
success_rate: rawStats.success_rate || 0,
|
||||
traffic_used: rawStats.traffic_used || 0,
|
||||
traffic_remaining: rawStats.traffic_remaining || 0,
|
||||
countries_used: rawStats.countries_used || [],
|
||||
daily_stats: rawStats.daily_stats || [],
|
||||
hourly_stats: rawStats.hourly_stats || []
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取日期字符串
|
||||
* @param {number} daysOffset 天数偏移
|
||||
* @returns {string}
|
||||
*/
|
||||
getDateString(daysOffset = 0) {
|
||||
const date = new Date();
|
||||
date.setDate(date.getDate() + daysOffset);
|
||||
return date.toISOString().split('T')[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* 退出登录
|
||||
* @returns {Promise<boolean>}
|
||||
*/
|
||||
async logout() {
|
||||
if (!this.sessionToken) {
|
||||
return true;
|
||||
}
|
||||
|
||||
try {
|
||||
await this.makeRequest(
|
||||
'POST',
|
||||
`${this.baseUrl}${this.endpoints.logout}`,
|
||||
null,
|
||||
{
|
||||
'Authorization': `Bearer ${this.sessionToken}`
|
||||
}
|
||||
);
|
||||
|
||||
this.sessionToken = null;
|
||||
this.authenticated = false;
|
||||
this.lastAuthTime = null;
|
||||
|
||||
this.logger.info('Rola-IP退出登录成功');
|
||||
return true;
|
||||
|
||||
} catch (error) {
|
||||
this.logger.warn('Rola-IP退出登录失败:', error.message);
|
||||
// 即使退出失败,也清除本地状态
|
||||
this.sessionToken = null;
|
||||
this.authenticated = false;
|
||||
this.lastAuthTime = null;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取支持的国家列表
|
||||
* @returns {Array}
|
||||
*/
|
||||
getSupportedCountries() {
|
||||
return [
|
||||
{ code: 'US', name: '美国', name_en: 'United States' },
|
||||
{ code: 'UK', name: '英国', name_en: 'United Kingdom' },
|
||||
{ code: 'DE', name: '德国', name_en: 'Germany' },
|
||||
{ code: 'FR', name: '法国', name_en: 'France' },
|
||||
{ code: 'JP', name: '日本', name_en: 'Japan' },
|
||||
{ code: 'KR', name: '韩国', name_en: 'South Korea' },
|
||||
{ code: 'AU', name: '澳大利亚', name_en: 'Australia' },
|
||||
{ code: 'CA', name: '加拿大', name_en: 'Canada' },
|
||||
{ code: 'BR', name: '巴西', name_en: 'Brazil' },
|
||||
{ code: 'IN', name: '印度', name_en: 'India' },
|
||||
{ code: 'SG', name: '新加坡', name_en: 'Singapore' },
|
||||
{ code: 'HK', name: '香港', name_en: 'Hong Kong' },
|
||||
{ code: 'TW', name: '台湾', name_en: 'Taiwan' },
|
||||
{ code: 'RU', name: '俄罗斯', name_en: 'Russia' },
|
||||
{ code: 'NL', name: '荷兰', name_en: 'Netherlands' }
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取支持的代理类型
|
||||
* @returns {Array}
|
||||
*/
|
||||
getSupportedTypes() {
|
||||
return [
|
||||
{
|
||||
type: 'residential',
|
||||
name: '住宅代理',
|
||||
name_en: 'Residential Proxy',
|
||||
description: '真实家庭IP,匿名性最高'
|
||||
},
|
||||
{
|
||||
type: 'datacenter',
|
||||
name: '数据中心代理',
|
||||
name_en: 'Datacenter Proxy',
|
||||
description: '高速稳定,适合大量请求'
|
||||
},
|
||||
{
|
||||
type: 'mobile',
|
||||
name: '移动代理',
|
||||
name_en: 'Mobile Proxy',
|
||||
description: '移动网络IP,适合移动应用'
|
||||
},
|
||||
{
|
||||
type: 'static_residential',
|
||||
name: '静态住宅代理',
|
||||
name_en: 'Static Residential Proxy',
|
||||
description: '固定住宅IP,长期稳定'
|
||||
},
|
||||
{
|
||||
type: 'ipv6',
|
||||
name: 'IPv6代理',
|
||||
name_en: 'IPv6 Proxy',
|
||||
description: 'IPv6协议代理'
|
||||
}
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = RolaIPAdapter;
|
||||
118
backend/src/amqp/AmqpBus.js
Normal file
@@ -0,0 +1,118 @@
|
||||
const amqp = require('amqplib/callback_api');
|
||||
const ImportTgMqHandler=require("@src/amqp/handles/ImportTgMqHandler");
|
||||
const GroupLanguageMqHandler=require("@src/amqp/handles/GroupLanguageMqHandler");
|
||||
const logger=require("@src/util/Log4jUtil")
|
||||
const config=require("@src/config/Config");
|
||||
|
||||
class AmqpBus{
|
||||
|
||||
static getInstance(){
|
||||
if(!AmqpBus.instance){
|
||||
AmqpBus.instance=new AmqpBus();
|
||||
}
|
||||
return AmqpBus.instance;
|
||||
}
|
||||
|
||||
|
||||
constructor() {
|
||||
this.logger=logger.getLogger("AmqpBus");
|
||||
//重新连接次数
|
||||
this.reconnectCount=0;
|
||||
//通道,发送使用
|
||||
this.channel="";
|
||||
//连接
|
||||
this.connect();
|
||||
|
||||
|
||||
//初始化导入Tg的处理器
|
||||
ImportTgMqHandler.getInstance();
|
||||
|
||||
//初始化群语言处理器
|
||||
GroupLanguageMqHandler.getInstance();
|
||||
|
||||
//监听连接状态
|
||||
|
||||
|
||||
//例子
|
||||
//发送者
|
||||
//amqp.connect('amqp://localhost', (err, conn) =>{
|
||||
// conn.createChannel((err, ch)=>{
|
||||
// this.channel=ch;
|
||||
// let q = 'hello';
|
||||
//
|
||||
// //断言队列是否存在
|
||||
// ch.assertQueue(q, {durable: true});
|
||||
// ch.sendToQueue(q, Buffer.from('Hello World!'));
|
||||
// console.log("Sent 'Hello World!'");
|
||||
// });
|
||||
//});
|
||||
|
||||
//接受者
|
||||
//amqp.connect('amqp://localhost', (err, conn) =>{
|
||||
// conn.createChannel(function(err, ch) {
|
||||
// let q = 'hello';
|
||||
// ch.assertQueue(q, {durable: true});
|
||||
// console.log(" [*] Waiting for messages in %s. To exit press CTRL+C", q);
|
||||
// ch.consume(q, function(msg) {
|
||||
// console.log(" [x] Received %s", msg.content.toString());
|
||||
// //如果noAck默认是false,如果要确认消息,执行ch.ack(msg);
|
||||
// }, {noAck: true});
|
||||
// //noAck就是发消息就是出栈,不需要回复确认消息。
|
||||
// });
|
||||
//});
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
//连接amqp
|
||||
connect(){
|
||||
amqp.connect(config.isDev?config.devRabbitMq:config.proRabbitMq, (err, conn) =>{
|
||||
if(err) {
|
||||
this.logger.error('amqp连接失败', err);
|
||||
console.log("MQ重新连接");
|
||||
setTimeout(() => this.reconnect(), 5000);
|
||||
return;
|
||||
}
|
||||
//创建通道
|
||||
conn.createChannel((err, ch)=>{
|
||||
this.channel=ch;
|
||||
});
|
||||
//监听连接错误
|
||||
conn.on("error",(err)=>{
|
||||
this.logger.error('[bus mq] connection error ',err);
|
||||
this.reconnect();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
//重连amqp
|
||||
reconnect(){
|
||||
setTimeout( ()=>{
|
||||
this.connect();
|
||||
this.reconnectCount++;
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
//发送队列消息
|
||||
//queueName 队列名称
|
||||
//msg 消息
|
||||
//channelName 通道名称
|
||||
//buffer
|
||||
send(channelName,buffer){
|
||||
//断言队列是否存在,durable:是否持久化存储
|
||||
try {
|
||||
this.channel.assertQueue(channelName,{durable: true});
|
||||
this.channel.sendToQueue(channelName,buffer);
|
||||
}catch (e){
|
||||
console.log("发送队列消息失败",e);
|
||||
}
|
||||
console.log("发送队列消息");
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
module.exports=AmqpBus;
|
||||
|
||||
|
||||
22
backend/src/amqp/AmqpQueueName.js
Normal file
@@ -0,0 +1,22 @@
|
||||
|
||||
|
||||
|
||||
class AmqpQueueName{
|
||||
|
||||
static getInstance(){
|
||||
if(!AmqpQueueName.instance){
|
||||
AmqpQueueName.instance=new AmqpQueueName();
|
||||
}
|
||||
return AmqpQueueName.instance;
|
||||
}
|
||||
|
||||
//导入Tg队列名
|
||||
importTg="importTg";
|
||||
//设置群组语言队列名
|
||||
setGroupLanguage="setGroupLanguage";
|
||||
//群组群发消息
|
||||
sendGroupMessage="sendGroupMessage";
|
||||
|
||||
}
|
||||
|
||||
module.exports=AmqpQueueName;
|
||||
156
backend/src/amqp/handles/BaseMqHandler.js
Normal file
@@ -0,0 +1,156 @@
|
||||
const amqp = require('amqplib/callback_api');
|
||||
const logger=require("@src/util/Log4jUtil")
|
||||
const config=require("@src/config/Config");
|
||||
|
||||
|
||||
class BaseMqHandler{
|
||||
|
||||
|
||||
constructor() {
|
||||
this.logger=logger.getLogger();
|
||||
//存储当前连接
|
||||
this.conn="";
|
||||
//重新连接次数
|
||||
this.reconnectCount=0;
|
||||
//通道子类需要覆盖此channel
|
||||
this.channel="";
|
||||
|
||||
//连接
|
||||
this.connect();
|
||||
//检查的定时器
|
||||
this.checkTimer=null;
|
||||
//是否正在重连
|
||||
this.reconning=false;
|
||||
|
||||
this.checkTimer="";
|
||||
}
|
||||
|
||||
initial(){
|
||||
//存储当前连接
|
||||
this.conn="";
|
||||
//重新连接次数
|
||||
this.reconnectCount=0;
|
||||
//通道子类需要覆盖此channel
|
||||
this.channel="";
|
||||
|
||||
//连接
|
||||
this.connect();
|
||||
//检查的定时器
|
||||
this.checkTimer=null;
|
||||
//是否正在重连
|
||||
this.reconning=false;
|
||||
|
||||
this.checkTimer="";
|
||||
}
|
||||
|
||||
//连接amqp
|
||||
connect(){
|
||||
let param={
|
||||
heartbeat:10,
|
||||
};
|
||||
if(!config.isDev){
|
||||
param.username=config.mqProUsername;
|
||||
param.password=config.mqProPassword;
|
||||
}
|
||||
amqp.connect(config.isDev?config.devRabbitMq:config.proRabbitMq,param, (err, conn) =>{
|
||||
if(err){
|
||||
console.log("amqp连接失败",err);
|
||||
this.reconning=false;
|
||||
this.reconnect();
|
||||
return;
|
||||
}
|
||||
if(!conn){
|
||||
console.log("conn无,重新连接");
|
||||
this.reconning=false;
|
||||
this.reconnect();
|
||||
return;
|
||||
}
|
||||
if(!conn.createChannel){
|
||||
console.log("conn.createChannel为空,重新连接");
|
||||
this.reconning=false;
|
||||
console.log(conn);
|
||||
this.reconnect();
|
||||
return;
|
||||
}
|
||||
this.conn=conn;
|
||||
//创建通道
|
||||
this.createChannel();
|
||||
//监听连接错误
|
||||
conn.on("error",(err)=>{
|
||||
this.logger.error('[mq] connection error ',err);
|
||||
this.reconnect();
|
||||
});
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
createChannel(){
|
||||
if(this.checkTimer)return;
|
||||
this.conn.createChannel((err, ch)=>{
|
||||
if(err){
|
||||
console.log("error",err);
|
||||
this.reconning=false;
|
||||
this.reconnect();
|
||||
return;
|
||||
}
|
||||
this.channel=ch;
|
||||
this.init().then().catch();
|
||||
this.reconning=false;
|
||||
if(this.checkTimer){
|
||||
clearInterval(this.checkTimer);
|
||||
this.checkTimer=null;
|
||||
return;
|
||||
}
|
||||
this.checkTimer=setInterval(()=>{
|
||||
if(!this.channel || !this.channel.get){
|
||||
clearInterval(this.checkTimer);
|
||||
this.checkTimer=null;
|
||||
this.createChannel();
|
||||
}
|
||||
},5000);
|
||||
});
|
||||
}
|
||||
|
||||
//重连amqp
|
||||
reconnect(){
|
||||
if(this.reconning)return;
|
||||
this.reconning=true;
|
||||
console.log("MQ重新连接");
|
||||
this.connect();
|
||||
this.reconnectCount++;
|
||||
}
|
||||
|
||||
|
||||
//子类需要重写此方法
|
||||
async init(){
|
||||
console.log("初始化通道");
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
//动态引入,否则报错
|
||||
getClientBus(){
|
||||
return require("@src/client/ClientBus");
|
||||
}
|
||||
|
||||
//发送队列消息
|
||||
send(channelName,buffer){
|
||||
if(!this.channel){
|
||||
this.logger.error("未初始化通道");
|
||||
return;
|
||||
}
|
||||
//断言队列是否存在,durable:是否持久化存储
|
||||
try {
|
||||
this.channel.assertQueue(channelName,{durable: true});
|
||||
this.channel.sendToQueue(channelName,buffer);
|
||||
}catch (e){
|
||||
console.log("发送队列消息失败",e);
|
||||
}
|
||||
console.log("发送队列消息");
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
module.exports=BaseMqHandler;
|
||||
91
backend/src/amqp/handles/GroupLanguageMqHandler.js
Normal file
@@ -0,0 +1,91 @@
|
||||
const AmqpQueueName = require("@src/amqp/AmqpQueueName");
|
||||
const BaseMqHandler = require("@src/amqp/handles/BaseMqHandler");
|
||||
const {sleep}=require("@src/util/Util");
|
||||
const GroupService = require("@src/service/MGroupService");
|
||||
const AliCloudUtil = require("@src/util/AliCloudUtil");
|
||||
|
||||
|
||||
//群语言队列处理
|
||||
class GroupLanguageMqHandler extends BaseMqHandler{
|
||||
|
||||
static getInstance(){
|
||||
if(!GroupLanguageMqHandler.instance){
|
||||
GroupLanguageMqHandler.instance=new GroupLanguageMqHandler();
|
||||
}
|
||||
return GroupLanguageMqHandler.instance;
|
||||
}
|
||||
|
||||
|
||||
//初始化事件
|
||||
async init(){
|
||||
for(let i=0;i<5;i++){
|
||||
this.handler().then()
|
||||
.catch((e)=>{
|
||||
console.log("通道错误",e);
|
||||
});
|
||||
await sleep(parseInt(Math.random()*10));
|
||||
}
|
||||
}
|
||||
|
||||
//处理事件
|
||||
async handler(){
|
||||
if(!this.channel){
|
||||
this.logger.error("未初始化通道");
|
||||
return;
|
||||
}
|
||||
const queueName=AmqpQueueName.getInstance().setGroupLanguage;
|
||||
this.channel.assertQueue(queueName, {durable: true});
|
||||
this.channel.prefetch(10);
|
||||
let lockIndex=false;
|
||||
let timer;
|
||||
let finished=()=>{
|
||||
lockIndex=false;
|
||||
}
|
||||
timer=setInterval(async ()=>{
|
||||
if(lockIndex)return;
|
||||
if(!this.channel || !this.channel.get){
|
||||
console.log("设置群组语言的Channel为空")
|
||||
clearInterval(timer);
|
||||
timer=null;
|
||||
return;
|
||||
}
|
||||
lockIndex=true;
|
||||
this.channel.get(queueName,{},async (err,msg)=>{
|
||||
//获取不到消息
|
||||
if(!msg){
|
||||
finished();
|
||||
return;
|
||||
}
|
||||
//群id
|
||||
const id = msg.content.toString();
|
||||
try {
|
||||
//查询群组
|
||||
const GroupService=require("@src/service/MGroupService");
|
||||
const group=await GroupService.getInstance().findById(id);
|
||||
if(!group){
|
||||
//群组不存在,确认消息
|
||||
this.channel.ack(msg);
|
||||
return;
|
||||
}
|
||||
//获取语言
|
||||
const AliCloudUtil=require("@src/util/AliCloudUtil");
|
||||
const language=await AliCloudUtil.getInstance().languageDetect(group.title);
|
||||
this.logger.info("群组id:",id,"获取语言:",language);
|
||||
await GroupService.getInstance().updateById(id,{
|
||||
language:language
|
||||
});
|
||||
//确认消息
|
||||
this.channel.ack(msg);
|
||||
}catch (e){
|
||||
console.log("设置语言报错:","拒绝消息:",e);
|
||||
this.channel.nack(msg);
|
||||
}finally {
|
||||
finished();
|
||||
}
|
||||
});
|
||||
},2000);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
module.exports=GroupLanguageMqHandler;
|
||||
394
backend/src/amqp/handles/GroupTaskHandler.js
Normal file
@@ -0,0 +1,394 @@
|
||||
const BaseMqHandler = require("@src/amqp/handles/BaseMqHandler");
|
||||
const MGroupService = require("@src/service/MGroupService");
|
||||
const MMessageService = require("@src/service/MMessageService");
|
||||
const MApiDataService = require("@src/service/MApiDataService");
|
||||
const MTgAccountService = require("@src/service/MTgAccountService");
|
||||
const AccountUsage = require("@src/util/AccountUsage");
|
||||
const MJoinGroupService = require("@src/service/MJoinGroupService");
|
||||
const {sleep} = require("@src/util/Util");
|
||||
const {NewMessage} = require("telegram/events");
|
||||
const {Api} = require("telegram");
|
||||
const MGroupSendLogService= require("@src/service/MGroupSendLogService");
|
||||
const MParamService = require("@src/service/MParamService");
|
||||
const ParamKey = require("@src/static/ParamKey");
|
||||
const StringUtil = require("@src/util/StringUtil");
|
||||
|
||||
const Logger = require("@src/util/Log4jUtil");
|
||||
|
||||
//一个任务一个GroupTaskHandler
|
||||
class GroupTaskHandler extends BaseMqHandler {
|
||||
|
||||
constructor(groupTask) {
|
||||
console.log("初始化群发任务处理器");
|
||||
super();
|
||||
this.groupTask = groupTask;
|
||||
this.queueName = groupTask.name;
|
||||
//账号
|
||||
this.accountCache = [];
|
||||
//客户端长连接
|
||||
this.clientCache = [];
|
||||
|
||||
//定时get任务
|
||||
this.timerArr=[];
|
||||
|
||||
this.logger= Logger.getLogger("GroupTaskHandler");
|
||||
}
|
||||
|
||||
//取消任务
|
||||
cancel() {
|
||||
if(this.timer){
|
||||
clearInterval(this.timer);
|
||||
this.timer=null;
|
||||
}
|
||||
for(let i=0;i<this.timerArr.length;i++){
|
||||
if(this.timerArr[i]){
|
||||
clearInterval(this.timerArr[i]);
|
||||
this.timerArr[i]=null;
|
||||
}
|
||||
}
|
||||
this.timerArr=[];
|
||||
for(let i=0;i<this.clientCache.length;i++){
|
||||
let item=this.clientCache[i];
|
||||
this.getClientBus().getInstance().deleteCache(item);
|
||||
item=null;
|
||||
}
|
||||
this.clientCache=[];
|
||||
}
|
||||
|
||||
//初始化
|
||||
async init() {
|
||||
for (let i = 0; i < 5; i++) {
|
||||
this.handler(i).then()
|
||||
.catch((e) => {
|
||||
console.log("通道错误", e);
|
||||
});
|
||||
await sleep(parseInt(Math.random()*10));
|
||||
}
|
||||
}
|
||||
|
||||
//获取账号
|
||||
async getAccount(groupId) {
|
||||
let isJoinGroup = true;
|
||||
let joinModel;
|
||||
//本地缓存先查询
|
||||
let account;
|
||||
for (let i = 0; i < this.accountCache.length; i++) {
|
||||
let item = this.accountCache[i];
|
||||
//判断是否有存已加入群的账号
|
||||
if (item.addGroupIds && item.addGroupIds.split(",").indexOf(groupId) !== -1) {
|
||||
//判断账号是否在可用期内
|
||||
if (!item.nextTime || (Number(item.nextTime) <= new Date().getTime())) {
|
||||
console.log("本地缓存中取account")
|
||||
account = item;
|
||||
//更新下次时间
|
||||
let t=await MTgAccountService.getInstance().updateNextTime(item.id);
|
||||
item.nextTime=t;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
//本地无缓存,查询中取
|
||||
if (!account) {
|
||||
//随机已加入群的随机账号
|
||||
account = await MTgAccountService.getInstance().getRandomAccountByUsageIdAndAddGroupId(AccountUsage.群组推广, groupId);
|
||||
if(account)this.accountCache.push(account);
|
||||
console.log("查询数据库加入群组的号");
|
||||
}
|
||||
//本地取一个号复用
|
||||
if(!account){
|
||||
for (let i = 0; i < this.accountCache.length; i++) {
|
||||
let item = this.accountCache[i];
|
||||
//判断账号是否在可用期内
|
||||
if (!item.nextTime || Number(item.nextTime) <= new Date().getTime()) {
|
||||
if(await MTgAccountService.getInstance().getTodayCanJoinGroupByTgAccount(item)){
|
||||
console.log("本地缓存中随意取account")
|
||||
account = item;
|
||||
//更新下次时间
|
||||
let t=await MTgAccountService.getInstance().updateNextTime(item.id);
|
||||
item.nextTime=t;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
//上面都没有,取一个新的
|
||||
if(!account){
|
||||
isJoinGroup = false;
|
||||
account = await MTgAccountService.getInstance().getRandomAccountByUsageId(AccountUsage.群组推广);
|
||||
if(account)this.accountCache.push(account);
|
||||
console.log("群发--查询数据库号");
|
||||
}
|
||||
|
||||
|
||||
if(isJoinGroup){
|
||||
joinModel = await MJoinGroupService.getInstance().findByGroupIdAndAccountId(groupId, account.id);
|
||||
}
|
||||
|
||||
return {
|
||||
isJoinGroup: isJoinGroup,
|
||||
joinModel: joinModel,
|
||||
account: account
|
||||
}
|
||||
}
|
||||
|
||||
async getClient(account, item) {
|
||||
let client;
|
||||
//查询本地缓存
|
||||
for (let i = 0; i < this.clientCache.length; i++) {
|
||||
if (this.clientCache[i] && this.clientCache[i].accountId === account.id) {
|
||||
console.log("从缓存中取client");
|
||||
return this.clientCache[i];
|
||||
}
|
||||
}
|
||||
let clientBus = this.getClientBus().getInstance();
|
||||
//获取客户端
|
||||
client = await clientBus.getClient(account, item.apiId, item.apiHash, true);
|
||||
console.log("获取新的client")
|
||||
if(!client){
|
||||
//数据库实在没有,从缓存中取一个
|
||||
let cacheItem;
|
||||
//尝试获取的次数
|
||||
let getCount=0;
|
||||
while (getCount<5){
|
||||
const n=StringUtil.getRandomNum(0,this.clientCache.length);
|
||||
cacheItem=this.clientCache[n];
|
||||
//表示今日还可以加群
|
||||
if(await MTgAccountService.getInstance().getTodayCanJoinGroupByTgAccount(cacheItem.tgAccount)){
|
||||
client=cacheItem;
|
||||
break;
|
||||
}
|
||||
++getCount;
|
||||
}
|
||||
if(!client){
|
||||
//实在没有client
|
||||
this.logger.error("实在没有client");
|
||||
return;
|
||||
}
|
||||
}
|
||||
//添加消息监听
|
||||
this.addClientListener(client).then();
|
||||
this.clientCache.push(client);
|
||||
await client.connect();
|
||||
return client;
|
||||
}
|
||||
|
||||
//监听消息事件
|
||||
async addClientListener(client){
|
||||
if(!client)return;
|
||||
let message=await MMessageService.getInstance().findById(this.groupTask.replyMessageId);
|
||||
if(!message)return;
|
||||
client.tgClient.addEventHandler(async (event) => {
|
||||
if(event.originalUpdate.className === "UpdateShortMessage"){
|
||||
if(message.isText){
|
||||
try {
|
||||
event.message.respond({
|
||||
message:message.text
|
||||
});
|
||||
}catch (e){
|
||||
|
||||
}
|
||||
}else{
|
||||
try {
|
||||
event.message.respond({
|
||||
file:message.filePath
|
||||
});
|
||||
}catch (e){
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
},new NewMessage({}));
|
||||
}
|
||||
|
||||
deleteClient(phone,client){
|
||||
let clientName=client.name;
|
||||
this.getClientBus().getInstance().deleteCache(client);
|
||||
for (let i = 0; i < this.clientCache.length; i++) {
|
||||
if (this.clientCache[i].name === clientName) {
|
||||
this.clientCache.splice(i, 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
for(let i=0;i<this.accountCache.length;i++){
|
||||
if(this.accountCache[i].phone === phone){
|
||||
this.accountCache.splice(i, 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
async checkBand(client){
|
||||
//被封号了
|
||||
if(client.canContinue()){
|
||||
let phone=client.phone;
|
||||
await this.deleteClient(phone,client);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
//处理事件
|
||||
async handler(i) {
|
||||
if (!this.channel) {
|
||||
this.logger.error("未初始化通道");
|
||||
return;
|
||||
}
|
||||
this.channel.assertQueue(this.queueName, {durable: true});
|
||||
this.channel.prefetch(1);
|
||||
let lockIndex = false;
|
||||
let finished = () => {
|
||||
lockIndex = false;
|
||||
}
|
||||
|
||||
this.timerArr[i] = setInterval(async () => {
|
||||
if (lockIndex) return;
|
||||
if (!this.channel || !this.channel.get) {
|
||||
console.log("发送消息的Channel为空")
|
||||
clearInterval(this.timerArr[i]);
|
||||
this.timerArr[i] = null;
|
||||
this.reconnect();
|
||||
return;
|
||||
}
|
||||
lockIndex = true;
|
||||
this.channel.get(this.queueName, {noAck:true}, async (err, msg) => {
|
||||
//获取不到消息
|
||||
if (!msg) {
|
||||
finished();
|
||||
return;
|
||||
}
|
||||
let isTimerHandler=false;
|
||||
let safeTimer=setTimeout(() => {
|
||||
isTimerHandler=true;
|
||||
finished();
|
||||
},20000);
|
||||
let client = "";
|
||||
try {
|
||||
const str = msg.content.toString();
|
||||
const obj = JSON.parse(str);
|
||||
const groupId = obj.groupId;
|
||||
const messageId = obj.messageId;
|
||||
const group = await MGroupService.getInstance().findById(groupId);
|
||||
if (!group) {
|
||||
this.logger.error("查询不到群组,无法发送");
|
||||
return;
|
||||
}
|
||||
this.logger.info("执行群组任务,群组id="+groupId+",群用户名="+group.username);
|
||||
const message = await MMessageService.getInstance().findById(messageId);
|
||||
if (!message) {
|
||||
this.logger.error("查询不到消息,无法发送");
|
||||
return;
|
||||
}
|
||||
//随机apiId
|
||||
let item = await MApiDataService.getInstance().getCanWorkRandomItem();
|
||||
if (!item) {
|
||||
this.logger.error("无apiId");
|
||||
return;
|
||||
}
|
||||
let accObj = await this.getAccount(groupId);
|
||||
let isJoinGroup = accObj.isJoinGroup;
|
||||
let joinModel = accObj.joinModel;
|
||||
let account = accObj.account;
|
||||
|
||||
if (!account) {
|
||||
this.logger.error("群组发送无可用账号");
|
||||
return;
|
||||
}
|
||||
//查询是否在间隔时间内
|
||||
let lastSend=MGroupSendLogService.getInstance().findLastByAccountId(account.id);
|
||||
if(lastSend){
|
||||
let groupSendIntervalParam=await MParamService.getInstance().findByKey(ParamKey.groupSendInterval);
|
||||
if(( new Date().getTime() - new Date(lastSend.createdAt).getTime() ) < (groupSendIntervalParam.value*1000)){
|
||||
this.logger.info(account.phone+"在间隔时间内,拦截发送")
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
client = await this.getClient(account, item);
|
||||
|
||||
if (!client) {
|
||||
console.log("发送消息,client为空跳过");
|
||||
return;
|
||||
}
|
||||
|
||||
let channelId = "";
|
||||
let accessHash = "";
|
||||
|
||||
const groupInfo = await client.getGroupInfoByLink(group.link);
|
||||
if(await this.checkBand(client)){
|
||||
return;
|
||||
}
|
||||
if (!groupInfo || groupInfo.err) {
|
||||
console.log("获取不到群链接跳过");
|
||||
return;
|
||||
}
|
||||
channelId = groupInfo.id;
|
||||
accessHash = groupInfo.accessHash;
|
||||
|
||||
//判断数据库有没有加入过群组
|
||||
if (!isJoinGroup) {
|
||||
//查询是否可以发送
|
||||
if (!client.groupCanSendMsg(groupInfo)) {
|
||||
console.log("该群无法发送,跳过");
|
||||
return;
|
||||
}
|
||||
//加入群组
|
||||
let joinRes = await client.joinGroup(group,group.link);
|
||||
if(await this.checkBand(client)){
|
||||
return;
|
||||
}
|
||||
if(!joinRes || joinRes.err){
|
||||
//加群报错
|
||||
return;
|
||||
}
|
||||
await sleep(800);
|
||||
}
|
||||
|
||||
let sendRes;
|
||||
let peer=new Api.InputPeerChannel({
|
||||
channelId:channelId,
|
||||
accessHash:accessHash
|
||||
});
|
||||
//文本消息
|
||||
if (message.isText) {
|
||||
sendRes=await client.sendGroupMsg(peer,group,{
|
||||
message: message.text,
|
||||
tgAccount:account,
|
||||
groupTask:this.groupTask
|
||||
});
|
||||
} else {
|
||||
//发送群文件
|
||||
console.log("发送群文件");
|
||||
sendRes=await client.sendFile(peer,message.filePath,{
|
||||
isGroup:true,
|
||||
group:group,
|
||||
tgAccount:account,
|
||||
groupTask:this.groupTask
|
||||
});
|
||||
console.log("发送结束")
|
||||
|
||||
}
|
||||
if(await this.checkBand(client)){
|
||||
return;
|
||||
}
|
||||
} catch (e) {
|
||||
if(client){
|
||||
client.handlerErr(false,()=>{},e.toString()).then();
|
||||
}
|
||||
console.log("错误", e);
|
||||
} finally {
|
||||
if(isTimerHandler)clearInterval(safeTimer);
|
||||
finished();
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
|
||||
}, 2000);
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
module.exports = GroupTaskHandler;
|
||||
237
backend/src/amqp/handles/ImportTgMqHandler.js
Normal file
@@ -0,0 +1,237 @@
|
||||
const AmqpQueueName = require("@src/amqp/AmqpQueueName");
|
||||
const MGroupService = require("@src/service/MGroupService");
|
||||
const AccountUsage = require("@src/util/AccountUsage");
|
||||
const {sleep}=require("@src/util/Util");
|
||||
const BaseMqHandler = require("@src/amqp/handles/BaseMqHandler");
|
||||
const Logger = require("@src/util/Log4jUtil");
|
||||
|
||||
class ImportTgMqHandler extends BaseMqHandler{
|
||||
|
||||
static getInstance(){
|
||||
if(!ImportTgMqHandler.instance){
|
||||
ImportTgMqHandler.instance=new ImportTgMqHandler();
|
||||
}
|
||||
return ImportTgMqHandler.instance;
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
//导入的客户端
|
||||
this.client=null;
|
||||
this.logger= Logger.getLogger("ImportTgMqHandler");
|
||||
}
|
||||
|
||||
//初始化事件
|
||||
async init(){
|
||||
for (let i = 0; i < 1; i++) {
|
||||
this.handler().then()
|
||||
.catch((e) => {
|
||||
this.logger.error("通道错误", e);
|
||||
});
|
||||
await sleep(Math.random()*10);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 这导入这个群组链接这个类里面也写一个getClient方式进行客户端得获取
|
||||
* 类似于java里面override方法
|
||||
* @returns {Promise<null>}
|
||||
*/
|
||||
async getClient(){
|
||||
//如果已经初始化过,但是账号不可以继续进行,删除缓存
|
||||
if(this.client
|
||||
&& !this.client.canContinue()
|
||||
){
|
||||
await this.getClientBus().getInstance().deleteCache(this.client);
|
||||
this.client=null;
|
||||
}
|
||||
if(this.client){
|
||||
return this.client;
|
||||
}
|
||||
//如果没有初始化过,则初始化
|
||||
this.client=await this.getClientBus().getInstance().getCanWorkClient(AccountUsage.采集,false);
|
||||
return this.client;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* //处理事件 处理器
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async handler() {
|
||||
if(!this.channel){
|
||||
this.logger.error("未初始化通道");
|
||||
return;
|
||||
}
|
||||
const queueName=AmqpQueueName.getInstance().importTg;
|
||||
this.channel.assertQueue(queueName, {durable: true});
|
||||
this.channel.prefetch(10);
|
||||
let lockIndex=false;
|
||||
let timer;
|
||||
let finished=()=>{
|
||||
lockIndex=false;
|
||||
}
|
||||
let cancelClient=()=>{
|
||||
this.getClientBus().getInstance().deleteCache(this.client);
|
||||
this.client=null;
|
||||
}
|
||||
//消费者
|
||||
timer=setInterval(async (message, ...args)=>{
|
||||
if(lockIndex)return;
|
||||
if(!this.channel || !this.channel.get){
|
||||
console.log("importTg的Channel为空")
|
||||
clearInterval(timer);
|
||||
timer=null;
|
||||
this.initial();
|
||||
return;
|
||||
}
|
||||
lockIndex=true;
|
||||
this.channel.get(queueName,{},async (err,msg)=>{
|
||||
//获取不到消息
|
||||
if(!msg){
|
||||
finished();
|
||||
return;
|
||||
}
|
||||
await sleep(2000);
|
||||
let isTimerHandler=false;
|
||||
let safeTimer=setTimeout(() => {
|
||||
if(isHandled)return;
|
||||
isTimerHandler=true;
|
||||
try{
|
||||
this.channel.nack(msg);
|
||||
}catch(e){}
|
||||
finished();
|
||||
|
||||
},20000);
|
||||
|
||||
let link = msg.content.toString();
|
||||
this.logger.info("队列处理需要导入的链接=", link);
|
||||
let client;
|
||||
let isHandled=false;
|
||||
|
||||
let ack=()=>{
|
||||
try {
|
||||
isHandled=true;
|
||||
if(this.channel){
|
||||
this.channel.ack(msg);
|
||||
}
|
||||
finished();
|
||||
}catch (e){
|
||||
this.logger.error("导入群组ack报错",e);
|
||||
this.reconnect();
|
||||
}
|
||||
}
|
||||
let noAck=()=>{
|
||||
try {
|
||||
isHandled=true;
|
||||
if(this.channel){
|
||||
this.channel.nack(msg);
|
||||
}
|
||||
finished();
|
||||
}catch (e){
|
||||
this.logger.error("导入群组nack报错",e);
|
||||
this.reconnect();
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
//不是tg群,跳过
|
||||
if (link.indexOf("t.me") === -1) {
|
||||
//确认消息
|
||||
ack();
|
||||
return;
|
||||
}
|
||||
|
||||
//非公开群组-先跳过
|
||||
if (link.indexOf("joinchat") !== -1) {
|
||||
//确认消息
|
||||
ack();
|
||||
return;
|
||||
}
|
||||
|
||||
//链接包含加号的
|
||||
if (link.indexOf("+") !== -1) {
|
||||
//确认消息
|
||||
ack();
|
||||
return;
|
||||
}
|
||||
|
||||
const mGroupService = MGroupService.getInstance();
|
||||
//公开群组
|
||||
//用户名
|
||||
link=link.trim();
|
||||
let username = link.slice(link.lastIndexOf("/") + 1);
|
||||
username=username.replace("@","");
|
||||
|
||||
const old = await mGroupService.findOneByUsername(username);
|
||||
if (old) {
|
||||
this.logger.info("群组已存在", link);
|
||||
//确认消息
|
||||
ack();
|
||||
return;
|
||||
}
|
||||
|
||||
//动态引入,否则报错。
|
||||
client = await this.getClient();
|
||||
if(!client){
|
||||
this.logger.error("无采集类型的账号");
|
||||
//拒绝消息
|
||||
noAck();
|
||||
return;
|
||||
}
|
||||
//搜索
|
||||
let groupInfo = await client.getGroupInfoByLink(link);
|
||||
//统计缓存一个账号的调用次数和相关记录,防止一个账号过度调用而导致封号。
|
||||
|
||||
//空数据 //比如无效的username啥的要缓存24小时,不然会反复读取
|
||||
if(!groupInfo || groupInfo.err){
|
||||
//确认消息
|
||||
ack();
|
||||
return;
|
||||
}
|
||||
//
|
||||
if(!groupInfo || !groupInfo.username){
|
||||
ack();
|
||||
return;
|
||||
}
|
||||
//读取群组的完整信息
|
||||
let fullChannelInfo=await client.getFullChannel(groupInfo.id,groupInfo.accessHash);
|
||||
//{"fullChat":{"flags":8233,"canViewParticipants":true,"canSetUsername":false,"canSetStickers":false,"hiddenPrehistory":false,"canSetLocation":false,"hasScheduled":false,"canViewStats":false,"blocked":false,"flags2":0,"canDeleteChannel":false,"id":"1518270434","about":"","participantsCount":1851,"adminsCount":null,"kickedCount":null,"bannedCount":null,"onlineCount":40,"readInboxMaxId":0,"readOutboxMaxId":5841,"unreadCount":0,"chatPhoto":{"flags":0,"hasStickers":false,"id":"5134263460111165848","accessHash":"4035783204218428322","fileReference":{"type":"Buffer","data":[0,98,123,209,108,31,191,144,135,152,26,241,175,27,235,119,161,242,151,48,129]},"date":1651000475,"sizes":[{"type":"a","w":160,"h":160,"size":5945,"className":"PhotoSize"},{"type":"b","w":320,"h":320,"size":12617,"className":"PhotoSize"},{"type":"c","w":640,"h":640,"size":17489,"className":"PhotoSize"},{"type":"i","bytes":{"type":"Buffer","data":[1,40,40,202,162,138,124,74,172,199,115,99,3,32,99,57,62,148,61,4,33,70,8,28,169,218,120,6,155,90,13,44,140,172,133,1,227,6,45,191,112,122,213,25,21,86,66,17,247,168,232,216,235,83,25,55,184,218,176,218,40,162,172,66,133,98,165,130,146,7,83,138,54,176,254,18,63,10,146,57,202,66,209,237,7,118,121,207,181,61,174,153,149,148,168,195,103,191,173,77,223,97,232,43,221,76,208,249,100,28,227,150,199,36,84,27,31,4,236,110,14,15,21,49,187,98,219,130,40,56,219,248,122,82,53,206,227,146,156,134,220,48,222,249,169,73,173,144,222,189,72,89,89,78,24,16,125,8,162,157,44,158,108,133,177,140,246,205,21,107,204,145,148,81,69,48,10,40,162,128,10,40,162,128,63]},"className":"PhotoStrippedSize"}],"videoSizes":null,"dcId":1,"className":"Photo"},"notifySettings":{"flags":0,"showPreviews":null,"silent":null,"muteUntil":null,"iosSound":null,"androidSound":null,"otherSound":null,"className":"PeerNotifySettings"},"exportedInvite":null,"botInfo":[{"userId":"162726413","description":"This is the most complete Bot to help you manage your groups easily and safely!\n\nJust add me to a group as Admin and then use /settings to set up functions.","commands":[],"menuButton":{"className":"BotMenuButtonCommands"},"className":"BotInfo"}],"migratedFromChatId":null,"migratedFromMaxId":null,"pinnedMsgId":3116,"stickerset":null,"availableMinId":null,"folderId":null,"linkedChatId":null,"location":null,"slowmodeSeconds":null,"slowmodeNextSendDate":null,"statsDc":null,"pts":7070,"call":null,"ttlPeriod":null,"pendingSuggestions":null,"groupcallDefaultJoinAs":null,"themeEmoticon":null,"requestsPending":null,"recentRequesters":null,"defaultSendAs":null,"availableReactions":null,"className":"ChannelFull"},"chats":[{"flags":270660,"creator":false,"left":true,"broadcast":false,"verified":false,"megagroup":true,"restricted":false,"signatures":false,"min":false,"scam":false,"hasLink":false,"hasGeo":false,"slowmodeEnabled":false,"callActive":false,"callNotEmpty":false,"fake":false,"gigagroup":false,"noforwards":false,"id":"1518270434","accessHash":"1542687578125949193","title":"Binance Smart Contract4990","username":"BinanceSmartContract90","photo":{"flags":2,"hasVideo":false,"photoId":"5134263460111165848","strippedThumb":{"type":"Buffer","data":[1,8,8,205,249,118,99,35,57,206,113,69,20,80,7]},"dcId":1,"className":"ChatPhoto"},"date":1645438492,"restrictionReason":null,"adminRights":null,"bannedRights":null,"defaultBannedRights":{"flags":132096,"viewMessages":false,"sendMessages":false,"sendMedia":false,"sendStickers":false,"sendGifs":false,"sendGames":false,"sendInline":false,"embedLinks":false,"sendPolls":false,"changeInfo":true,"inviteUsers":false,"pinMessages":true,"untilDate":2147483647,"className":"ChatBannedRights"},"participantsCount":null,"className":"Channel"}],"users":[{"flags":33570859,"self":false,"contact":false,"mutualContact":false,"deleted":false,"bot":true,"botChatHistory":false,"botNochats":false,"verified":false,"restricted":false,"min":false,"botInlineGeo":false,"support":false,"scam":false,"applyMinPhoto":true,"fake":false,"botAttachMenu":false,"id":"162726413","accessHash":"8618602004816265344","firstName":"Group Help","lastName":null,"username":"GroupHelpBot","phone":null,"photo":{"flags":2,"hasVideo":false,"photoId":"698904622486628333","strippedThumb":{"type":"Buffer","data":[1,8,8,96,54,198,195,4,0,195,249,81,69,21,171,118,50,81,185]},"dcId":4,"className":"UserProfilePhoto"},"status":null,"botInfoVersion":42,"restrictionReason":null,"botInlinePlaceholder":null,"langCode":null,"className":"User"}],"className":"messages.ChatFull"}
|
||||
if(fullChannelInfo.fullChat != null) {
|
||||
this.logger.info("群组描述信息" + fullChannelInfo.fullChat.about);
|
||||
this.logger.info("群组人数"+ fullChannelInfo.fullChat.participantsCount);
|
||||
}
|
||||
//this.logger.info("群组完整信息",fullChannelInfo);
|
||||
//循环channel类型数组(频道或群组)
|
||||
const group=await mGroupService.create({
|
||||
title: groupInfo.title,
|
||||
username:username,
|
||||
link: link,
|
||||
count: fullChannelInfo.fullChat.participantsCount?fullChannelInfo.fullChat.participantsCount:0, //groupInfo.participantsCount读取不到人数了
|
||||
type: groupInfo.broadcast ? "频道" : "群组",
|
||||
isPublic: !groupInfo.has_link ? 1 : 0,
|
||||
description: fullChannelInfo.fullChat.about?fullChannelInfo.fullChat.about:"未设置描述",
|
||||
});
|
||||
//push到设置群语言队列
|
||||
this.send(AmqpQueueName.getInstance().setGroupLanguage,Buffer.from(String(group.id)));
|
||||
ack();
|
||||
}catch (e){
|
||||
|
||||
this.logger.error("importTgMq报错",e);
|
||||
}finally {
|
||||
if(this.client && !this.client.canContinue()){
|
||||
await cancelClient();
|
||||
}
|
||||
if(isTimerHandler)
|
||||
return;
|
||||
clearTimeout(safeTimer);
|
||||
if(!isHandled){
|
||||
noAck();
|
||||
}
|
||||
}
|
||||
});
|
||||
},2000)
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = ImportTgMqHandler;
|
||||
3
backend/src/api/Account.js
Normal file
@@ -0,0 +1,3 @@
|
||||
class Account{
|
||||
|
||||
}
|
||||
3
backend/src/api/Auth.js
Normal file
@@ -0,0 +1,3 @@
|
||||
class Auth{
|
||||
|
||||
}
|
||||
11
backend/src/api/Bots.js
Normal file
@@ -0,0 +1,11 @@
|
||||
class Bots{
|
||||
//是否设计成单例模式
|
||||
|
||||
|
||||
constructor() {
|
||||
}
|
||||
|
||||
async sendMessage(){
|
||||
|
||||
}
|
||||
}
|
||||
3
backend/src/api/Channels.js
Normal file
@@ -0,0 +1,3 @@
|
||||
class Channels{
|
||||
|
||||
}
|
||||
6
backend/src/api/Contacts.js
Normal file
@@ -0,0 +1,6 @@
|
||||
|
||||
|
||||
class Contacts{
|
||||
|
||||
|
||||
}
|
||||
5
backend/src/api/Folders.js
Normal file
@@ -0,0 +1,5 @@
|
||||
|
||||
|
||||
class Folders {
|
||||
|
||||
}
|
||||
5
backend/src/api/Help.js
Normal file
@@ -0,0 +1,5 @@
|
||||
|
||||
|
||||
class Help{
|
||||
|
||||
}
|
||||
5
backend/src/api/Langpack.js
Normal file
@@ -0,0 +1,5 @@
|
||||
|
||||
class Langpack{
|
||||
|
||||
|
||||
}
|
||||
4
backend/src/api/Messages.js
Normal file
@@ -0,0 +1,4 @@
|
||||
|
||||
class Message{
|
||||
|
||||
}
|
||||
5
backend/src/api/Payments.js
Normal file
@@ -0,0 +1,5 @@
|
||||
|
||||
|
||||
class Payments{
|
||||
|
||||
}
|
||||
5
backend/src/api/Phone.js
Normal file
@@ -0,0 +1,5 @@
|
||||
|
||||
|
||||
class Phone{
|
||||
|
||||
}
|
||||
0
backend/src/api/Photos.js
Normal file
0
backend/src/api/Stats.js
Normal file
0
backend/src/api/Updates.js
Normal file
0
backend/src/api/Upload.js
Normal file
3
backend/src/api/Users.js
Normal file
@@ -0,0 +1,3 @@
|
||||
class Users{
|
||||
|
||||
}
|
||||
BIN
backend/src/assets/defaultAvatar/1.jpeg
Normal file
|
After Width: | Height: | Size: 29 KiB |
BIN
backend/src/assets/defaultAvatar/2.jpeg
Normal file
|
After Width: | Height: | Size: 33 KiB |
BIN
backend/src/assets/defaultAvatar/3.jpeg
Normal file
|
After Width: | Height: | Size: 39 KiB |
BIN
backend/src/assets/defaultAvatar/4.jpeg
Normal file
|
After Width: | Height: | Size: 8.9 KiB |
BIN
backend/src/assets/defaultAvatar/5.jpeg
Normal file
|
After Width: | Height: | Size: 18 KiB |
BIN
backend/src/assets/defaultAvatar/6.jpeg
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
backend/src/assets/defaultAvatar/7.jpeg
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
backend/src/assets/defaultAvatar/8.jpeg
Normal file
|
After Width: | Height: | Size: 32 KiB |
BIN
backend/src/assets/defaultAvatar/9.jpeg
Normal file
|
After Width: | Height: | Size: 17 KiB |
169
backend/src/bus/AutoRegisterBus.js
Normal file
@@ -0,0 +1,169 @@
|
||||
const RedisUtil = require("@src/util/RedisUtil");
|
||||
const MParamService = require("@src/service/MParamService");
|
||||
const ParamKey = require("@src/static/ParamKey");
|
||||
const ClientBus = require("@src/client/ClientBus");
|
||||
const MApiDataService = require("@src/service/MApiDataService");
|
||||
const Logger = require("@src/util/Log4jUtil");
|
||||
const SimActiveUtil=require("@src/util/SimActiveUtil");
|
||||
const MTGRegisterLogService=require("@src/service/MTGRegisterLogService");
|
||||
const MAccountUsageService=require("@src/service/MAccountUsageService");
|
||||
const {sleep} = require("@src/util/Util");
|
||||
|
||||
class AutoRegisterBus{
|
||||
static getInstance() {
|
||||
if (!AutoRegisterBus.instance) {
|
||||
AutoRegisterBus.instance = new AutoRegisterBus();
|
||||
}
|
||||
return AutoRegisterBus.instance;
|
||||
}
|
||||
|
||||
constructor() {
|
||||
this.logger=Logger.getLogger("AutoRegisterBus");
|
||||
//是否在自动注册
|
||||
this.running=false;
|
||||
//正在注册数
|
||||
this.registerIngCount=0;
|
||||
}
|
||||
|
||||
stop(){
|
||||
this.running=false;
|
||||
}
|
||||
|
||||
//注册速度的控制
|
||||
|
||||
//测试的话再也不能使用全自动注册了。
|
||||
|
||||
//有一次报错一次把所有余额一下子获取空了。
|
||||
|
||||
//弱网优化
|
||||
|
||||
//扣钱和实际注册的账号是否相符合的问题
|
||||
|
||||
/**
|
||||
*
|
||||
* @param country 国家
|
||||
* @param usageId 用途ID
|
||||
* @param namePrompt AI生成名字的提示词
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async start(country,usageId,namePrompt){
|
||||
if(this.running)return;
|
||||
this.running=true;
|
||||
//判断是否超过每日注册数
|
||||
let dayCount=await RedisUtil.getCache(RedisUtil.dayMaxRegisterCount); //多个项目的redis冲突问题
|
||||
//每日最大次数
|
||||
let dayMaxCount=await MParamService.getInstance().findByKey(ParamKey.dayMaxRegisterCount);
|
||||
if(dayMaxCount){
|
||||
this.logger.info("每日最大注册数:"+dayMaxCount.value);
|
||||
}else{
|
||||
this.logger.info("每日最大注册数:0");
|
||||
//这种情况,自动注册相当于没有限制,容易超过预算,要退出才对。
|
||||
this.stop();
|
||||
//要end ?
|
||||
return;
|
||||
}
|
||||
//
|
||||
dayMaxCount=dayMaxCount.value;
|
||||
//每次最大 比如每次注册10个
|
||||
let qps=await MParamService.getInstance().findByKey(ParamKey.registerQPS);
|
||||
qps=qps.value;
|
||||
if(qps){
|
||||
this.logger.info("每次注册数:"+qps);
|
||||
}
|
||||
//
|
||||
if(dayCount && dayCount>=dayMaxCount){
|
||||
this.running=false;
|
||||
return;
|
||||
}
|
||||
//
|
||||
for(let i=0;i<qps;i++){
|
||||
if(!this.running)return;
|
||||
++this.registerIngCount;
|
||||
this.startRegister(country,usageId,dayMaxCount,qps,namePrompt).then();
|
||||
await sleep(2000);
|
||||
}
|
||||
}
|
||||
|
||||
//结束注册
|
||||
async end(country,usageId,dayMaxCount,qps,namePrompt){
|
||||
--this.registerIngCount;
|
||||
//判断是否超过每日注册数
|
||||
let dayCount=await RedisUtil.getCache(RedisUtil.dayMaxRegisterCount);
|
||||
if(dayCount && dayCount>=dayMaxCount){
|
||||
this.running=false;
|
||||
return;
|
||||
}
|
||||
//如果注册正在进行中, 并且注册数小于每日注册数,则继续注册
|
||||
if(this.running && this.registerIngCount<qps){
|
||||
this.startRegister(country,usageId,dayMaxCount,qps,namePrompt).then();
|
||||
}else {
|
||||
this.stop();
|
||||
}
|
||||
}
|
||||
|
||||
/***
|
||||
*
|
||||
* @param country 国家
|
||||
* @param usageId 用途ID
|
||||
* @param dayMaxCount 每日最大注册数
|
||||
* @param qps
|
||||
* @param namePrompt AI生成名字的提示词
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async startRegister(country,usageId,dayMaxCount,qps,namePrompt){
|
||||
try{
|
||||
this.logger.info("startRegister");
|
||||
//this.logger.info("getAllCountry: "+ JSON.stringify(await SimActiveUtil.getAllCountry()));
|
||||
if(!usageId){
|
||||
usageId=await MAccountUsageService.getInstance().getIdByRandom();
|
||||
}
|
||||
//注册
|
||||
let apiDataList=await MApiDataService.getInstance().findListByMinRegCount(1);
|
||||
if(!apiDataList || !apiDataList.length>0){
|
||||
this.logger.error("注册函数:获取不到appId");
|
||||
await this.end(country,usageId,dayMaxCount,qps);
|
||||
return;
|
||||
}
|
||||
let item=apiDataList[0];
|
||||
const phoneObj=await SimActiveUtil.getPhoneByCountry(country);
|
||||
this.logger.info("SimActiveUtil.getPhoneByCountry phoneObj: " + JSON.stringify(phoneObj)); //{"id":"880450942","phone":"84921330724","country":"10"}
|
||||
if(!phoneObj){
|
||||
this.logger.error("注册函数:获取不到手机号");
|
||||
await this.end(country,usageId,dayMaxCount,qps);
|
||||
return;
|
||||
}
|
||||
//获取代理ip
|
||||
///获取client //tgAccount为空也可以获取到client???
|
||||
let client=await ClientBus.getInstance().getClient("", item.apiId, item.apiHash,true); //其实注册肯定是使用代理ip最好的。
|
||||
if(!client){
|
||||
this.logger.error("注册函数:获取不到client");
|
||||
await this.end(country,usageId,dayMaxCount,qps);
|
||||
return;
|
||||
}else{
|
||||
this.logger.info("注册函数:获取到client");
|
||||
}
|
||||
//记录获取号码到数据库
|
||||
await MTGRegisterLogService.getInstance().create({
|
||||
phoneId:phoneObj.id,
|
||||
phone:phoneObj.phone,
|
||||
country:country?country:("随机--"+phoneObj.country),
|
||||
type:"SmsActivate",
|
||||
state:0
|
||||
});
|
||||
//
|
||||
await client.doRegisterByPhone({
|
||||
...phoneObj,
|
||||
usageId: usageId,
|
||||
namePrompt: namePrompt,
|
||||
country: country
|
||||
});
|
||||
await this.end(country,usageId,dayMaxCount,qps,namePrompt);
|
||||
}
|
||||
catch(e){
|
||||
this.logger.error("startRegister报错:" + e.toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
module.exports=AutoRegisterBus;
|
||||
429
backend/src/bus/GroupCollectionMemberBus.js
Normal file
@@ -0,0 +1,429 @@
|
||||
const MGroupService=require("@src/service/MGroupService");
|
||||
const MGroupUserService=require("@src/service/MGroupUserService");
|
||||
const MTgAccountService = require("@src/service/MTgAccountService");
|
||||
const AccountUsage = require("@src/util/AccountUsage");
|
||||
const logger = require("@src/util/Log4jUtil");
|
||||
const MApiDataService = require("@src/service/MApiDataService");
|
||||
const ClientBus = require("@src/client/ClientBus");
|
||||
const {Api} = require("telegram");
|
||||
const GroupMembersService=require("@src/dbService/GroupMembersService");
|
||||
const MGroupMembers=require("@src/dbModes/MGroupMembers");
|
||||
const axios=require("axios");
|
||||
const cheerio = require('cheerio');
|
||||
const MUserService = require("@src/service/MUserService");
|
||||
const { Op } = require("sequelize");
|
||||
const RedisService = require("@src/service/RedisService");
|
||||
|
||||
//采集群成员
|
||||
class GroupCollectionMemberBus{
|
||||
static getInstance() {
|
||||
//加锁
|
||||
if (!GroupCollectionMemberBus.instance) {
|
||||
GroupCollectionMemberBus.instance = new GroupCollectionMemberBus();
|
||||
}
|
||||
return GroupCollectionMemberBus.instance;
|
||||
}
|
||||
|
||||
constructor() {
|
||||
this.logger=logger.getLogger("GroupCollectionMemberBus");
|
||||
//正在采集的群组ids
|
||||
this.collectionGroupIdsObj={};
|
||||
}
|
||||
|
||||
//正在采集的群组的下标
|
||||
getAllCollectionGroupIds(){
|
||||
return Object.keys(this.collectionGroupIdsObj);
|
||||
}
|
||||
|
||||
//获取账号,根据群组id获取Telegram账号
|
||||
async getAccount(groupId){
|
||||
//查询已加入过群的账号,不一定更好,因为群组可能会被解散或者账号被踢出群组
|
||||
//之前先查询是否有已经加入群组的账号,如果没有就查询一个可用的账号。
|
||||
//let account = await MTgAccountService.getInstance().getRandomAccountByUsageIdAndAddGroupId(AccountUsage.采集,groupId);
|
||||
let account = await MTgAccountService.getInstance().getOneCollectAccountByApiName("getParticipants");
|
||||
if(!account){
|
||||
this.logger.info("没有可用的账号");
|
||||
}
|
||||
this.logger.info("采集群成员,获取账号:"+JSON.stringify(account));
|
||||
return account;
|
||||
}
|
||||
|
||||
//获取client
|
||||
async getClient(account){
|
||||
//随机apiId
|
||||
let item = await MApiDataService.getInstance().getCanWorkRandomItem();
|
||||
if (!item) {
|
||||
this.logger.error("群成员采集无apiId");
|
||||
return null;
|
||||
}
|
||||
let client = await ClientBus.getInstance().getClient(account, item.apiId, item.apiHash, true);
|
||||
if(!client){
|
||||
this.logger.error("群成员采集无Client");
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
await client.connect();
|
||||
}catch (e) {
|
||||
this.logger.error("群成员采集Client连接失败");
|
||||
}
|
||||
return client;
|
||||
}
|
||||
|
||||
//通过t.me的方式获取描述
|
||||
async getDescriptionFromWebByUsername(username){
|
||||
let desc=null;
|
||||
//通过访问网页的方式获取简介
|
||||
let tgPage = await axios({
|
||||
method:"POST",
|
||||
url:"https://t.me/"+ username,
|
||||
});
|
||||
if (tgPage.status === 200) {
|
||||
let $=cheerio.load(tgPage.data);
|
||||
desc=$(".tgme_page_description").text();
|
||||
return desc;
|
||||
}
|
||||
}
|
||||
|
||||
//通过api的方式获取描述
|
||||
async getDescriptionByApi(client,username){
|
||||
let desc=null;
|
||||
//通过api的方式获取简介
|
||||
let tgPage = await client.getChat(username);
|
||||
if (tgPage.status === 200) {
|
||||
desc=tgPage.data.description;
|
||||
return desc;
|
||||
}
|
||||
}
|
||||
|
||||
//执行结束后的回调,采集指定群组的所有成员。可以发起多个群的采集?
|
||||
//返回值:之前只做单个群的采集,所以不需要返回值,但是做了被群集合采集调用之后,就需要返回值方便进行处理了
|
||||
//记得注意所有的分支都要处理好返回值
|
||||
async start(groupId){
|
||||
//
|
||||
if(this.collectionGroupIdsObj[groupId])return;
|
||||
let group=await MGroupService.getInstance().findById(groupId);
|
||||
if(!group)return {
|
||||
err:"群组不存在"
|
||||
};
|
||||
let account=await this.getAccount(groupId);
|
||||
if(!account){
|
||||
this.logger.error("群成员采集找不到账号");
|
||||
return {
|
||||
err:"群成员采集找不到账号"
|
||||
};
|
||||
}
|
||||
|
||||
let client=await this.getClient(account);
|
||||
if(!client){
|
||||
this.logger.error("群成员采集找不到client");
|
||||
return {
|
||||
err:"群成员采集找不到client"
|
||||
};
|
||||
}
|
||||
|
||||
this.collectionGroupIdsObj[groupId]=1;
|
||||
let groupInfo;
|
||||
let channelId;
|
||||
let accessHash;
|
||||
|
||||
let fullChannelInfo=null;
|
||||
|
||||
let arr=[];
|
||||
let offset=0;
|
||||
|
||||
let deleteClient=()=>{
|
||||
ClientBus.getInstance().deleteCache(client);
|
||||
account=null;
|
||||
client=null;
|
||||
}
|
||||
|
||||
//目前是采集完毕才拉人。这个算是定义一个函数还是执行?????? 命名函数
|
||||
let finished=()=>{
|
||||
delete this.collectionGroupIdsObj[groupId];
|
||||
}
|
||||
|
||||
//把死循环搞成递归执行????
|
||||
// waitUntil().interval(500)
|
||||
// .times(100000000)
|
||||
while (true){
|
||||
if(!client){
|
||||
account=await this.getAccount(groupId);
|
||||
if(!account){
|
||||
this.logger.error("群成员采集找不到账号");
|
||||
return {
|
||||
err:"群成员采集找不到账号"
|
||||
};
|
||||
}
|
||||
client=await this.getClient(account);
|
||||
if(!client){
|
||||
this.logger.error("群成员采集找不到client");
|
||||
finished();
|
||||
return{
|
||||
err:"群成员采集找不到client"
|
||||
};
|
||||
}
|
||||
}
|
||||
if(!groupInfo || groupInfo.err){//群组信息为空或者群组错误不为空
|
||||
groupInfo=await client.getGroupInfoByLink(group.link);
|
||||
if (!groupInfo || groupInfo.err) {
|
||||
this.logger.error("群成员采集获取群信息失败,跳过");
|
||||
if(groupInfo&& groupInfo.err){
|
||||
this.logger.error("GroupInfo.err : "+ groupInfo.err);
|
||||
}
|
||||
|
||||
if(groupInfo && groupInfo.err && (groupInfo.err.indexOf("USERNAME_NOT_OCCUPIED")!==-1
|
||||
|| groupInfo.err.indexOf("CHANNEL_INVALID")!==-1)
|
||||
|| groupInfo.err.indexOf("USERNAME_INVALID")!==-1) //漏掉了这种,导致一直死循环
|
||||
{
|
||||
deleteClient();
|
||||
finished();
|
||||
return {
|
||||
err:groupInfo.err
|
||||
}
|
||||
}
|
||||
|
||||
if(groupInfo && groupInfo.err && groupInfo.err.indexOf("CHAT_ADMIN_REQUIRED")){
|
||||
//是频道,而且没有管理员权限。这个时候应该退出循环。
|
||||
this.logger.info("这个群实际上是一个频道,而且当前账号不是管理员,group username: "+ group.username);
|
||||
//是不是应该return还是
|
||||
deleteClient();
|
||||
finished();
|
||||
return {
|
||||
err:groupInfo.err
|
||||
}
|
||||
}
|
||||
|
||||
if(groupInfo && groupInfo.err && groupInfo.err.indexOf("群组已经被禁用")){ //这种也从群集合先移除掉吧。
|
||||
//不能用这个账号了,一般是这个账号被这个群组封禁,或者这个账号异常。
|
||||
//https://t.me/BinanceRussian 有的这种官方的有认证的大群组,就会出现这个错误。
|
||||
//或者即使获取群成员也只获取到一百多个。可是这个是十几万人的群。
|
||||
deleteClient()
|
||||
finished();
|
||||
return {
|
||||
err:groupInfo.err
|
||||
}
|
||||
}
|
||||
|
||||
//其他错误
|
||||
|
||||
//检查是不是client的问题
|
||||
if(!client.canContinue()){
|
||||
deleteClient();
|
||||
}
|
||||
continue; //这样子会回到循环开始的地方重新去获取一个账号开始。
|
||||
}
|
||||
//获取群组信息没错,会执行到这里
|
||||
channelId=groupInfo.id;
|
||||
accessHash=groupInfo.accessHash;
|
||||
}//群组信息为空或者群组错误不为空
|
||||
|
||||
//fullChannelInfo =await client.getFullChannel(channelId, accessHash);
|
||||
//这个调用的频率限制是每秒一次,所以这里要等待一秒。???
|
||||
//如果忘记加await,还会出现错误。变成了异步调用,然后群成员获取完之后提示一个东西出来说什么在断开后进行了网络连接。
|
||||
//this.logger.info("fullChannelInfo: "+ JSON.stringify(fullChannelInfo));
|
||||
|
||||
// {"fullChat":{"flags":139273,"canViewParticipants":true,"canSetUsername":false,"canSetStickers":false,"hiddenPrehistory":false,"canSetLocation":false,"hasScheduled":false,"canViewStats":false,"blocked":false,"flags2":0,"canDeleteChannel":false,"id":"1334373934",
|
||||
// "about":"","participantsCount":4934,"adminsCount":null,"kickedCount":null,"bannedCount":null,
|
||||
// "onlineCount":40,"readInboxMaxId":0,"readOutboxMaxId":157777,"unreadCount":0,
|
||||
// "chatPhoto":{"flags":0,"hasStickers":false,"id":"6174610775218432400","accessHash":"-5621437017086602737",
|
||||
// "fileReference":{"type":"Buffer","data":[0,98,117,193,32,48,133,74,149,77,3,245,64,95,169,64,65,78,244,188,89]},"date":1588980498,
|
||||
// "sizes":[{"type":"a","w":160,"h":160,"size":7958,"className":"PhotoSize"},{"type":"b","w":320,"h":320,"size":19165,"className":"PhotoSize"},
|
||||
// {"type":"c","w":640,"h":640,"size":46021,"className":"PhotoSize"},{"type":"i","bytes":{"type":"Buffer","data":[1,40,40,217,162,170,73,169,218,199,35,35,59,110,83,131,242,154,111,246,181,167,247,219,254,249,52,236,192,187,69,65,29,204,115,198,94,22,220,7,243,166,130,219,149,183,6,36,224,224,214,124,234,246,29,139,52,81,69,88,140,107,155,121,162,185,153,150,57,25,100,96,193,163,235,244,53,19,125,167,114,178,193,58,225,10,227,105,31,195,138,216,154,19,35,171,6,0,128,71,35,56,207,113,239,80,253,141,242,167,206,57,24,235,147,158,190,254,245,92,192,81,177,180,186,137,12,156,169,44,62,83,220,115,254,53,100,69,115,31,57,206,0,24,81,219,142,156,125,106,83,109,50,148,217,49,32,17,144,73,20,246,183,149,155,62,121,28,231,140,255,0,141,101,40,243,59,177,220,146,19,39,148,158,103,222,218,55,125,104,164,130,22,136,156,200,206,15,169,206,40,166,163,110,162,38,162,138,42,128,40,162,138,0,40,162,138,0]},"className":"PhotoStrippedSize"}],"videoSizes":null,"dcId":5,"className":"Photo"},"notifySettings":{"flags":0,"showPreviews":null,"silent":null,"muteUntil":null,"iosSound":null,"androidSound":null,"otherSound":null,"className":"PeerNotifySettings"},"exportedInvite":null,"botInfo":[],"migratedFromChatId":null,"migratedFromMaxId":null,"pinnedMsgId":null,"stickerset":null,"availableMinId":null,"folderId":null,"linkedChatId":null,"location":null,"slowmodeSeconds":30,"slowmodeNextSendDate":null,"statsDc":null,"pts":212205,"call":null,"ttlPeriod":null,"pendingSuggestions":null,"groupcallDefaultJoinAs":null,"themeEmoticon":null,"requestsPending":null,"recentRequesters":null,"defaultSendAs":null,"availableReactions":null,"className":"ChannelFull"},"chats":[{"flags":4464964,"creator":false,"left":true,"broadcast":false,"verified":false,"megagroup":true,"restricted":false,"signatures":false,"min":false,"scam":false,"hasLink":false,"hasGeo":false,"slowmodeEnabled":true,"callActive":false,"callNotEmpty":false,"fake":false,"gigagroup":false,"noforwards":false,"id":"1334373934","accessHash":"-1124390018036649201","title":"CET 2021 (Ck)","username":"CkCET2021","photo":{"flags":2,"hasVideo":false,"photoId":"6174610775218432400","strippedThumb":{"type":"Buffer","data":[1,8,8,209,79,180,239,253,230,210,185,227,2,138,40,165,107,129]},"dcId":5,"className":"ChatPhoto"},"date":1586679026,"restrictionReason":null,"adminRights":null,"bannedRights":null,"defaultBannedRights":{"flags":165368,"viewMessages":false,"sendMessages":false,"sendMedia":false,"sendStickers":true,"sendGifs":true,"sendGames":true,"sendInline":true,"embedLinks":true,"sendPolls":true,"changeInfo":true,"inviteUsers":true,"pinMessages":true,"untilDate":2147483647,"className":"ChatBannedRights"},"participantsCount":null,"className":"Channel"}],"users":[],"className":"messages.ChatFull"}
|
||||
|
||||
|
||||
this.logger.info("马上开始执行获取群成员");
|
||||
let res=null;
|
||||
try{
|
||||
res=await client.getParticipants({
|
||||
channel: new Api.InputChannel({
|
||||
channelId,
|
||||
accessHash
|
||||
}),
|
||||
//可以获取群成员的过滤器,可以是管理员,可以是踢出的人等类型
|
||||
filter: new Api.ChannelParticipantsRecent({}),
|
||||
offset: offset,
|
||||
limit: 10000,
|
||||
hash: 2176980 //暂时不知道这个值是什么
|
||||
});
|
||||
}catch (e) {
|
||||
this.logger.error(e.toString());
|
||||
}
|
||||
|
||||
this.logger.info("保存获取群成员的操作记录");
|
||||
await RedisService.getInstance().saveGetParticipantsRecord(client.phone,groupId,new Date().getTime());
|
||||
|
||||
//获取群成员失败
|
||||
if(!res || res.err || !res.users.length>0){
|
||||
if(!client.canContinue()){ //每个请求前,每个请求后。都检查下客户端?
|
||||
deleteClient();
|
||||
}
|
||||
if(res && res.err && res.err.indexOf("CHAT_ADMIN_REQUIRED")){ //说明这是个频道。有的时候从数据库获取了是群组,但是群组也是可能改成频道啥的。所以还是要具体判断。
|
||||
//需要返回情况,方便处理整理的集合里面有频道的情况,方便进行群集合的剔除。或者在源头上进行群集合的过滤。
|
||||
this.logger.info("这个群实际上是一个频道,而且当前账号不是管理员,group username: "+ group.username);
|
||||
deleteClient();
|
||||
finished();
|
||||
return {
|
||||
err:res.err
|
||||
}
|
||||
// return {
|
||||
// err:"频道读取成员需要是管理员权限:CHAT_ADMIN_REQUIRED"
|
||||
// }
|
||||
}
|
||||
if( res&& res.users.length<=0){
|
||||
this.logger.info("群成员采集获取群成员失败或者是最后一页,跳出循环"); //获取到为零,也可能是最后一页吧?
|
||||
break;
|
||||
}
|
||||
//其他的错误情况,比如群组不存在,群组不是群组等等。
|
||||
continue;
|
||||
}
|
||||
this.logger.info("获取到分页成员数量:"+res.users.length);
|
||||
for(let i=0;i<res.users.length;i++){
|
||||
let item=res.users[i];
|
||||
//没有用户名的不存// 肯定都要存呀。
|
||||
//if(!item.username)continue;
|
||||
arr.push(item);
|
||||
}
|
||||
//最后一页了
|
||||
if(res.users.length<200){ //如果刚刚好是200,而且是第五十页也是最后一页吧?
|
||||
this.logger.info("GroupCollectionMemberBus 采集到最后一页");
|
||||
//写入缓存,方便账号不会超过限制,每天一定的读取次数 //开始调用的时候写入一次缓存就可以了吧???
|
||||
await RedisService.getInstance().saveGetParticipantsRecord(client.phone,groupId,new Date().getTime());
|
||||
|
||||
break;
|
||||
}
|
||||
offset+=200;
|
||||
}//end while
|
||||
|
||||
// 过滤掉无效数据
|
||||
arr = arr.filter(item=>{
|
||||
//检查是不是机器人,发现机器人很多是没有 firstname lastname的
|
||||
let isOk = true
|
||||
if(item.bot || (!item.firstName&&!item.lastName) || item.deleted || !item.username) { //暂时没有username的先不要,后期能拉uid的时候再处理。
|
||||
// this.logger.info("机器人,不插入");
|
||||
isOk = false
|
||||
}
|
||||
return isOk;
|
||||
});
|
||||
|
||||
await this.saveGroupUserTran(arr, groupId)
|
||||
deleteClient();
|
||||
finished();
|
||||
//返回采集到的符合条件的群成员
|
||||
return arr;
|
||||
}
|
||||
|
||||
async saveGroupUserTran(tgUserArr, groupId) {
|
||||
await this.saveUser(tgUserArr, groupId)
|
||||
await this.saveUserGroup(tgUserArr, groupId)
|
||||
await MGroupService.getInstance().updateUserCountAndLastUpdateTime(groupId);
|
||||
}
|
||||
|
||||
async saveUser(userArray, groupId, opts) {
|
||||
// 去除数据库中已有的数据
|
||||
this.logger.info("用户开始入库", userArray.length)
|
||||
let memberIdArr = userArray.map(item=>{
|
||||
return item.id
|
||||
})
|
||||
|
||||
let dbUserArr = await MUserService.getInstance().findAllByParam({
|
||||
attributes:["user_id"],
|
||||
where:{
|
||||
user_id:{
|
||||
[Op.in]: memberIdArr
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
let dbUserIdArr = dbUserArr.map(e=>e['user_id'])
|
||||
// 过滤掉 已经在数据库的数据
|
||||
userArray = userArray.filter(e=>
|
||||
!dbUserIdArr.includes(e.id)
|
||||
)
|
||||
// 转换为数据库对象
|
||||
let userDbArray = userArray.map(item=>{
|
||||
return {
|
||||
group_id:groupId,
|
||||
username:item.username, //用户名 会有为空的情况
|
||||
//fist_name:item.firstName,
|
||||
// 就因为first_name 错误写成fist_name 导致有的字段变成空数据,但是照样可以插入。只是没有firstname lastname。也没有报错,可笑了。
|
||||
//看来不能直接写字段名字,要用对象的方式。面向对象编程才不容易报错。调试了好久,老师的
|
||||
first_name:item.firstName,
|
||||
last_name:item.lastName, //不能为空
|
||||
user_id:item.id,
|
||||
access_hash: item.accessHash+"", //SequelizeValidationError: string violation: access_hash cannot be an array or an object
|
||||
phone:item.phone, //会有为空的情况
|
||||
// description: ,
|
||||
//user_status:item.status.className, //会有为空的时候报错,处理一下
|
||||
update_status_time:new Date().getTime(),
|
||||
update_time:new Date().getTime(),
|
||||
//last_online_time:item.status.wasOnline, ////会有为空的时候报错,处理一下
|
||||
}
|
||||
}
|
||||
)
|
||||
// 批量插入
|
||||
let result = await MUserService.getInstance().bulkCreate(userDbArray, opts)
|
||||
this.logger.debug("用户入库结束", userDbArray.length)
|
||||
return result;
|
||||
}
|
||||
// 保存用户和群组的关系
|
||||
async saveUserGroup(tgUserArray, groupId, opts = {}) {
|
||||
if (!tgUserArray || tgUserArray.length === 0 || !groupId) {
|
||||
return []
|
||||
}
|
||||
this.logger.debug("用户群组关系入库开始", tgUserArray.length)
|
||||
const tgUserIdArr = tgUserArray.map(e=>e.id);
|
||||
// 获取数据库的userId
|
||||
let userDbArr = await MUserService.getInstance().getModel().findAll({
|
||||
attributes: ['id'],
|
||||
where:{
|
||||
user_id: {[Op.in]: tgUserIdArr}
|
||||
}
|
||||
})
|
||||
let dbUserIdArr = userDbArr.map(e=>e.id)
|
||||
|
||||
// 查询关系表中的记录
|
||||
const guDbArr = await MGroupUserService.getInstance().findAllByParam({
|
||||
attributes: ['user_id'],
|
||||
where: {
|
||||
group_id: groupId,
|
||||
user_id: {
|
||||
[Op.in]: dbUserIdArr
|
||||
}
|
||||
}
|
||||
})
|
||||
// 过滤掉已存在的记录
|
||||
const guDbUserIdArr = guDbArr.map(e=>e.user_id)
|
||||
dbUserIdArr = dbUserIdArr.filter(e=>!guDbUserIdArr.includes(e))
|
||||
// 插入群组与用户的关系
|
||||
const groupUserDbArr = dbUserIdArr.map(e=>{return {
|
||||
user_id: e,
|
||||
group_id: groupId
|
||||
}})
|
||||
const result = await MGroupUserService.getInstance().getModel().bulkCreate(groupUserDbArr, opts)
|
||||
this.logger.debug("用户群组关系入库结束", groupUserDbArr.length)
|
||||
return result
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
module.exports=GroupCollectionMemberBus;
|
||||
|
||||
|
||||
//插入mongo数据库
|
||||
// let member=new MGroupMembers();
|
||||
// member.groupId=groupId;
|
||||
// member.username=item.username;
|
||||
// member.firstname=item.firstName;
|
||||
// member.lastname=item.lastName;
|
||||
// member.desc=desc;
|
||||
// let status=item.status;
|
||||
// if(status){
|
||||
// member.userStatus=status.className;
|
||||
// member.updateStatusTime=new Date().getTime();
|
||||
// if(status.className === "userStatusOffline"){
|
||||
// member.lastOnlineTime=status.wasOnline;
|
||||
// }
|
||||
// }
|
||||
// await GroupMembersService.getInstance().create(member.getObject());
|
||||
|
||||
239
backend/src/bus/MusterCollectionMemberBus.js
Normal file
@@ -0,0 +1,239 @@
|
||||
const MGroupService=require("@src/service/MGroupService");
|
||||
const MTgAccountService = require("@src/service/MTgAccountService");
|
||||
const AccountUsage = require("@src/util/AccountUsage");
|
||||
const logger = require("@src/util/Log4jUtil");
|
||||
const MApiDataService = require("@src/service/MApiDataService");
|
||||
const ClientBus = require("@src/client/ClientBus");
|
||||
const {Api} = require("telegram");
|
||||
const GroupMembersService=require("@src/dbService/GroupMembersService");
|
||||
const MGroupMembers=require("@src/dbModes/MGroupMembers");
|
||||
const MGroupMusterService=require("@src/service/MGroupMusterService");
|
||||
const axios=require("axios");
|
||||
const cheerio = require('cheerio');
|
||||
|
||||
|
||||
waitUntil = require('wait-until');
|
||||
|
||||
|
||||
const {sleep} = require("@src/util/Util");
|
||||
const MUserService = require("@src/service/MUserService");
|
||||
const PullMembersBus = require("@src/bus/PullMembersBus");
|
||||
const GroupCollectionMemberBus = require("@src/bus/GroupCollectionMemberBus");
|
||||
|
||||
|
||||
//群集合方式采集群成员,其实可以继承群采集成员的类
|
||||
class MusterCollectionMemberBus{
|
||||
static getInstance() {
|
||||
if (!MusterCollectionMemberBus.instance) {
|
||||
MusterCollectionMemberBus.instance = new MusterCollectionMemberBus();
|
||||
}
|
||||
return MusterCollectionMemberBus.instance;
|
||||
}
|
||||
|
||||
constructor() {
|
||||
this.logger=logger.getLogger("MusterCollectionMemberBus");
|
||||
|
||||
//key是集合id,value是0或1
|
||||
//0表示,1表示
|
||||
//这个是为了记录集合采集的状态
|
||||
//0表示未采集,1表示已采集
|
||||
//这个是临时变量,那么每次启动都会重新采集
|
||||
//应该是要写入到数据库记录状态。这个是已经在采集,防止当下重复采集同一个群集合。
|
||||
this.collectionMemberObj={};
|
||||
}
|
||||
|
||||
/***
|
||||
* 获取群成员
|
||||
* @returns {string[]}
|
||||
*/
|
||||
getCollingMusterArr(){
|
||||
//获取正在采集的群集合
|
||||
return Object.keys(this.collectionMemberObj);
|
||||
}
|
||||
|
||||
//采集公开群
|
||||
|
||||
//采集私有群
|
||||
async collectPrivateGroupMember(){
|
||||
|
||||
}
|
||||
|
||||
//采集和拉人同时进行???
|
||||
|
||||
//全自动采集所有群的群成员
|
||||
//每次采集一个群的群成员
|
||||
//每个账号每天只采集五个群
|
||||
async collectAllGroup(){
|
||||
//获取所有的群
|
||||
|
||||
//逐个进行群采集
|
||||
}
|
||||
|
||||
//采集指定群的群成员
|
||||
//每个账号每天只采集五个群,或者看设定了多少个群
|
||||
//返回群组成员的数组
|
||||
//一直换号执行到获取成功到最后一页?
|
||||
//其实居然有offset,其实是可以多个账号共同采集完成的。只是一个账号采集一个群是没有问题的。所以没什么好纠结的。看实际测试了。
|
||||
async collectPublicGroupMember(groupUsername){
|
||||
//获取账号
|
||||
let account=await this.getAccount(groupUsername);
|
||||
if(!account){
|
||||
this.logger.error("获取账号失败");
|
||||
return;
|
||||
}
|
||||
//获取client
|
||||
let client=await this.getClient(account);
|
||||
if(!client){
|
||||
this.logger.error("获取client失败");
|
||||
return;
|
||||
}
|
||||
|
||||
//检查群是否采集过
|
||||
checkGroupIsCollectedByUsername(username)
|
||||
|
||||
//获取群成员
|
||||
//如果一直获取到最后一页
|
||||
//如果获取到中间网络出错了
|
||||
//如果获取到中间账号泛洪了
|
||||
|
||||
let groupMembers=await client.getGroupMembers(username);
|
||||
if(!groupMembers){
|
||||
this.logger.error("获取群成员失败");
|
||||
return ;
|
||||
}
|
||||
//
|
||||
}
|
||||
|
||||
//通过t.me的方式获取描述
|
||||
async getDescriptionByWeb(username){
|
||||
let desc=null;
|
||||
//通过访问网页的方式获取简介
|
||||
let tgPage = await axios({
|
||||
method:"POST",
|
||||
url:"https://t.me/"+ username,
|
||||
});
|
||||
if (tgPage.status === 200) {
|
||||
let $=cheerio.load(tgPage.data);
|
||||
desc=$(".tgme_page_description").text();
|
||||
return desc;
|
||||
}
|
||||
}
|
||||
|
||||
/***
|
||||
*
|
||||
* @param musterId 要采集的群集合
|
||||
* @param taskId 拉人任务id ,其实可以隔离开这个参数
|
||||
* @param allCollect
|
||||
* 返回值 如果采集成功则返回采集到的成员,如果采集失败则返回err错误。
|
||||
*/
|
||||
async start(musterId,taskId=false,allCollect=false){
|
||||
//启动全自动采集
|
||||
//采集前检查(检查是否有账号,检查是否有client,检查群组是否采集过,检查。。。)
|
||||
//开始采集
|
||||
//采集结束,更新群组采集状态
|
||||
//写入采集数据
|
||||
|
||||
//启动全自动拉人
|
||||
|
||||
//如果已经采集过了,则不再采集
|
||||
//怎么判断已经采集过了?
|
||||
//什么情况下,这个群集合会被标记为已经采集过了? 或者是正在采集???
|
||||
if(this.collectionMemberObj[musterId]){
|
||||
this.logger.info("这个群集合已经采集过了");
|
||||
return {
|
||||
err:"这个群集合已经采集过了"
|
||||
};
|
||||
}else{
|
||||
this.logger.info("标记这个群集合正在采集状态,防止重复");
|
||||
this.collectionMemberObj[musterId]=1;
|
||||
}
|
||||
|
||||
//获取群集合
|
||||
let groupMuster=await MGroupMusterService.getInstance().findById(musterId);
|
||||
//如果没有这个集合,则不采集
|
||||
if(!groupMuster || !groupMuster.groupIds){
|
||||
this.logger.info("群集合不存在");
|
||||
return;
|
||||
}
|
||||
let groupIdsArr=groupMuster.groupIds.split(",");
|
||||
if(!groupIdsArr.length){
|
||||
this.logger.info("群集合为空,退出");
|
||||
return;
|
||||
}else{
|
||||
this.logger.info("群集合的群组数量为:"+groupIdsArr.length);
|
||||
}
|
||||
//
|
||||
for(let i=0;i<groupIdsArr.length;i++) {
|
||||
const groupId = groupIdsArr[i];
|
||||
let group = await MGroupService.getInstance().findById(groupId);
|
||||
if (!group){
|
||||
this.logger.info("群组不存在,跳过,进行下一个群组");
|
||||
continue;
|
||||
}
|
||||
this.logger.info("总共的群数量"+ groupIdsArr.length+" 现在遍历到第"+i+"个"+" group title"+ group.title);
|
||||
|
||||
//获取群组的user列表
|
||||
//let mArray=await GroupMembersService.getInstance().findByGroupId(groupId);
|
||||
//从tg_user表获取指定群的user列表
|
||||
this.logger.info("MusterCollectionMemberBus 开始从tg_user表获取成员")
|
||||
let userList = await MUserService.getInstance().findByGroupId(groupId);
|
||||
|
||||
//如果不是全部采集???或者标记是不是全部采集完毕了。
|
||||
if (!allCollect) {
|
||||
this.logger.info("没有全部采集??? allCollect为false")
|
||||
if (userList && userList.length) {
|
||||
this.logger.info("这个群有群成员,采集过了,跳过(除非强制更新,或者离最后一次采集多久才更新采集)userList.length :" + userList.length);
|
||||
continue;
|
||||
}else{
|
||||
this.logger.info("这个群没有群成员");
|
||||
}
|
||||
//这个地方要有所处理的,不然相当于if和else都是执行后面的东西
|
||||
//
|
||||
} else { //
|
||||
this.logger.info("allCollect为true");
|
||||
//需要做什么?
|
||||
}
|
||||
let collectResult=null;
|
||||
try {
|
||||
collectResult = await GroupCollectionMemberBus.getInstance().start(groupId);
|
||||
}catch (e) {
|
||||
this.logger.error("采集群成员报错了: " + e.toString());
|
||||
}
|
||||
//根据采集的结果进行处理
|
||||
//在缓存标记这个群组正在采集
|
||||
|
||||
//没有这个用户名的群,从集合中删除群,且删除群
|
||||
if(collectResult && collectResult.err && (collectResult.err.indexOf("USERNAME_NOT_OCCUPIED")!==-1
|
||||
|| collectResult&& collectResult.err.indexOf("CHANNEL_INVALID")!==-1)
|
||||
|| collectResult && collectResult.err.indexOf("USERNAME_INVALID")!==-1 //漏掉了这种,导致一直死循环
|
||||
||collectResult && collectResult.err &&collectResult.err.indexOf("CHAT_ADMIN_REQUIRED") //判断是不是本来就是标记为频道,如果是标记群组,修改成频道?
|
||||
) // //如果CHAT_ADMIN_REQUIRED,则不采集,并且剔除这个群
|
||||
{
|
||||
let n=[...groupIdsArr];
|
||||
n.splice(i,1);
|
||||
await MGroupMusterService.getInstance().updateById(musterId,{
|
||||
groupIds:n.join(",")
|
||||
});
|
||||
//删除群,但是群成员不删除。是否要删除呢,还是标记成不存在的状态?
|
||||
//await MGroupService.getInstance().deleteById(groupId);
|
||||
}else if(collectResult && collectResult.err){
|
||||
//其他错误
|
||||
this.logger.error("其他错误"+collectResult.err);
|
||||
|
||||
}
|
||||
|
||||
//如果没有错误,则更新群组的状态
|
||||
if(collectResult && !collectResult.err){
|
||||
//
|
||||
this.logger.info("没有报错, collectResult :" + collectResult);
|
||||
}
|
||||
|
||||
}//end for
|
||||
//群组遍历采集完了。
|
||||
//处理完毕,删除相对应的资源和缓存。或者标记这个群集合的任务完成。
|
||||
this.logger.info("$$$$$$$$$$ 群组遍历采集完了 $$$$$$$$$$");
|
||||
this.collectionMemberObj[musterId]=0;
|
||||
}//end start
|
||||
}
|
||||
|
||||
module.exports=MusterCollectionMemberBus;
|
||||
459
backend/src/bus/PullMemberTaskBus.js
Normal file
@@ -0,0 +1,459 @@
|
||||
const MGroupMusterService = require("@src/service/MGroupMusterService");
|
||||
const MGroupService = require("@src/service/MGroupService");
|
||||
const GroupMembersService = require("@src/dbService/GroupMembersService");
|
||||
const MPullMemberTaskService = require("@src/service/MPullMemberTaskService");
|
||||
const ClientBus = require("@src/client/ClientBus");
|
||||
const MParamService = require("@src/service/MParamService");
|
||||
const ParamKey = require("@src/static/ParamKey");
|
||||
const {sleep} = require("@src/util/Util");
|
||||
const {Api} = require("telegram");
|
||||
const ProjectPullLogService = require("@src/dbService/ProjectPullLogService");
|
||||
const MTgAccountService = require("@src/service/MTgAccountService");
|
||||
const MProjectPullLog = require("@src/dbModes/MProjectPullLog");
|
||||
const logger = require("@src/util/Log4jUtil");
|
||||
const MPullMemberStatisticService = require("@src/service/MPullMemberStatisticService");
|
||||
const MPullMemberProjectStatisticService = require("@src/service/MPullMemberProjectStatisticService");
|
||||
const MPullMemberLogService = require("@src/service/MPullMemberLogService");
|
||||
const AccountUsage = require("@src/util/AccountUsage");
|
||||
const MApiDataService = require("@src/service/MApiDataService");
|
||||
const MusterCollectionMemberBus = require("@src/bus/MusterCollectionMemberBus");
|
||||
const RedisUtil = require("@src/util/RedisUtil");
|
||||
const MongodUtil=require("@src/util/MongodbUtil");
|
||||
const UserStatus = require("@src/static/UserStatus"); //实际中操作居然没有利用起来,暂时先不删除,因为不知道有创建了这个。所以编译器其实可以更聪明提高复用性。
|
||||
const MUserService = require("@src/service/MUserService");
|
||||
const GroupCollectionMemberBus = require("@src/bus/GroupCollectionMemberBus");
|
||||
const UserFilter = require("@src/controller/filter/UserFilter");
|
||||
const PullMembersBus = require("@src/bus/PullMembersBus");
|
||||
const AliCloudUtil = require("@src/util/AliCloudUtil");
|
||||
const InviteResult = require("@src/static/InviteResuilt");
|
||||
const MProjectInviteLogService = require("@src/service/MProjectInviteLogService");
|
||||
|
||||
|
||||
//全自动拉人任务,把一个群集合的群成员拉到一个群去
|
||||
//升级修改成一个群集合的群成员拉到一个群集合去。 之前是把所有的群成员先采集完毕了。然后整理,然后拉人。
|
||||
//这个时候如果群数量比较多,如果都是采集过的,那么速度还好。如果都是没有采集过的,那么速度会很慢。
|
||||
//如果群数量比较少,那么速度会比较快。
|
||||
//现在改成是遍历群集合,如果群集合已经采集过,那么就直接拉人。如果群集合没有采集过,那么就采集完毕后,再拉人。
|
||||
//当然也在群集合后面增加了群集合采集。这样子也可以分模块测试,或者在拉人之前先把那个群集合的成员先采集了。
|
||||
//这样子可以避免拉人的时候,群里面的成员还没有采集完毕。
|
||||
//之前有一个坑爹的问题就是遍历群集合的100个群。如果发现某个群没有采集过,就会执行一次这个群集合的拉人。这样子变成所有群都遍历了100遍。总的相当于调度了10000次。
|
||||
//然后没有复用单群拉人的功能。所以account client都是自己创建的。现在都是交给单群的类去管理了,细节都封装了。非常省心。
|
||||
//之前是要管理taskId里面的多个拉人分组啥的,结果也是。现在只要管理任务就行了。
|
||||
class PullMemberTaskBus{
|
||||
//单例模式 保证只有一个实例 应该上锁 但是没有加锁 因为没有加锁 所以可能会出现多个实例
|
||||
//懒汉模式 饿汉模式
|
||||
// static instance;
|
||||
// static getInstance(){
|
||||
// if(!PullMemberTaskBus.instance){
|
||||
// PullMemberTaskBus.instance = new PullMemberTaskBus();
|
||||
// }
|
||||
// return PullMemberTaskBus.instance;
|
||||
// }
|
||||
static getInstance() {
|
||||
if (!PullMemberTaskBus.instance) {
|
||||
PullMemberTaskBus.instance = new PullMemberTaskBus();
|
||||
}
|
||||
return PullMemberTaskBus.instance;
|
||||
}
|
||||
|
||||
constructor() {
|
||||
this.logger=logger.getLogger("PullMemberTaskBus");
|
||||
this.statisticService=MPullMemberStatisticService.getInstance();
|
||||
|
||||
//正在拉群的群,key是任务id+下标,value是数组
|
||||
// this.pullingGroups={}; //应该这么命名才容易看懂
|
||||
//还是说 this.pullingTasks={} ?????
|
||||
this.pullData={};
|
||||
|
||||
//key是任务id,value是下标数组[key,key]
|
||||
this.taskIdKeyCacheObj={};
|
||||
//这个是为了方便管理多个任务,还有防止任务重复执行的?
|
||||
//什么时候赋值,为空意味着什么?
|
||||
this.taskIdCacheObj={};
|
||||
|
||||
//client缓存,key是任务id+下标,value是client
|
||||
this.clientCache={}
|
||||
}
|
||||
|
||||
//根据任务id停止
|
||||
stopByTaskId(taskId){
|
||||
this.logger.log("执行任务结束=",this.taskIdKeyCacheObj[taskId]);
|
||||
//如果任务id不存在,直接返回
|
||||
if(!this.taskIdKeyCacheObj[taskId] || !this.taskIdKeyCacheObj[taskId].length)return;
|
||||
//不然就遍历所有的下标
|
||||
let arr=this.taskIdKeyCacheObj[taskId];
|
||||
for(let i=0;i<arr.length;i++){
|
||||
console.log("停止任务",+arr[i]);
|
||||
arr[i]=String(arr[i]);
|
||||
this.finished(taskId,arr[i]);
|
||||
}
|
||||
delete this.taskIdKeyCacheObj[taskId];
|
||||
}
|
||||
|
||||
//结束
|
||||
finished(taskId,key){
|
||||
this.logger.info("结束任务",taskId,key);
|
||||
delete this.pullData[key];
|
||||
if(this.clientCache[key]){
|
||||
this.logger.info("执行结束client="+this.clientCache[key].phone);
|
||||
ClientBus.getInstance().deleteCache(this.clientCache[key]).then();
|
||||
delete this.clientCache[key];
|
||||
}
|
||||
//
|
||||
if(this.taskIdKeyCacheObj[taskId] && this.taskIdKeyCacheObj[taskId].length){
|
||||
for(let i=0;i<this.taskIdKeyCacheObj[taskId].length;i++){
|
||||
if(this.taskIdKeyCacheObj[taskId][i] === key){
|
||||
this.taskIdKeyCacheObj[taskId].splice(i,1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//根据任务id判断是否已启动
|
||||
isStart_old(taskId){
|
||||
//如果任务id缓存为空,那么就是任务未启动
|
||||
if(!this.taskIdKeyCacheObj[taskId]){
|
||||
return;
|
||||
}
|
||||
//任务任务id缓存不为空,那么任务就是启动状态。
|
||||
let arr=this.taskIdKeyCacheObj[taskId];
|
||||
//
|
||||
if(arr && arr.length)return true;
|
||||
}
|
||||
|
||||
isStart(taskId){
|
||||
if(!this.taskIdCacheObj[taskId]){
|
||||
this.logger.info("任务不是启动状态")
|
||||
return false;
|
||||
}else{
|
||||
this.logger.info("任务启动状态");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
//获取任务的拉人条件,方便不管单群拉人使用或者多群拉人使用,统一规范
|
||||
//不然多群拉人或者拉人任务就变成了通过任务id获取拉人条件,变成不统一了。
|
||||
async getUserFilterByTaskId(taskId){
|
||||
let pullMemberTask = await MPullMemberTaskService.getInstance().findById(taskId);
|
||||
let userFilter=new UserFilter();
|
||||
if(pullMemberTask) {
|
||||
//userFilter.usernameIncludeKeywords = pullMemberTask.includeKeywords;
|
||||
//userFilter.usernameExcludeKeywords = pullMemberTask.filterKeywords;
|
||||
userFilter.setFirstNameIncludeKeywords(pullMemberTask.includeKeywords);
|
||||
userFilter.setFirstNameExcludeKeywords(pullMemberTask.filterKeywords);
|
||||
userFilter.setLastNameIncludeKeywords(pullMemberTask.includeKeywords);
|
||||
userFilter.setLastNameExcludeKeywords(pullMemberTask.filterKeywords);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
//获取某个群符合条件的用户,返回用户数组 //nodejs如何传参一个类的实例?
|
||||
//有的过滤条件在拉人之前就可以过滤,有的是必须在拉人过程中判断的。
|
||||
//要不要改成通过群组id来获取用户?
|
||||
async getSuitableUserListByTaskIdAndMemberList(taskId,memberList){
|
||||
let result = [];
|
||||
let pullMemberTask=await MPullMemberTaskService.getInstance().findById(taskId);
|
||||
this.logger.info("查询成员数量="+memberList.length + "开始遍历这个群的成员,筛选条件");
|
||||
for(let m=0;m<memberList.length;m++){
|
||||
//判断包含的词
|
||||
if(pullMemberTask.includeKeywords){
|
||||
let allNo=true;
|
||||
let includeArr=pullMemberTask.includeKeywords.split(",");
|
||||
for(let aaa=0;aaa<includeArr.length;aaa++){
|
||||
if(!includeArr[aaa])continue;
|
||||
let name="";
|
||||
if(memberList[m].firstname)name+=memberList[m].firstname;
|
||||
if(memberList[m].lastname)name+=memberList[m].lastname;
|
||||
if(name && name.length>0){
|
||||
if(name.indexOf(includeArr[aaa])!==-1){
|
||||
allNo=false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if(allNo)continue;
|
||||
}else{
|
||||
//this.logger.info("没有要包含的词");
|
||||
}
|
||||
//判断不包含
|
||||
if(pullMemberTask.filterKeywords) {
|
||||
let filterArr = pullMemberTask.filterKeywords.split(",");
|
||||
let isContinue = true;
|
||||
for (let aaa = 0; aaa < filterArr.length; aaa++) {
|
||||
if (!filterArr[aaa]) continue;
|
||||
let name = "";
|
||||
if (memberList[m].firstname) name += memberList[m].firstname;
|
||||
if (memberList[m].lastname) name += memberList[m].lastname;
|
||||
if(name && name.length >0){
|
||||
if (name.indexOf(filterArr[aaa]) !== -1) { //
|
||||
isContinue = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!isContinue) continue;
|
||||
}else{
|
||||
//this.logger.info("没有要排除的词");
|
||||
}
|
||||
try {
|
||||
//this.logger.info("pullMemberTask.scriptProjectId :"+ pullMemberTask.scriptProjectId);
|
||||
//判断是否拉过项目
|
||||
if(await MProjectInviteLogService.getInstance().findByUsernameAndScriptProjectId(pullMemberTask.scriptProjectId,memberList[m].username)){
|
||||
//this.logger.info("用户" + memberList[m].username + "已经拉过项目了,跳过");
|
||||
continue;
|
||||
}else{
|
||||
//this.logger.info("用户" + memberList[m].username + "没有拉过项目,添加到结果中");
|
||||
}
|
||||
} catch (e) {
|
||||
this.logger.error("判断是否拉过项目出错了,错误信息:" + e);
|
||||
return null; //不return的话又鸡巴执行下去了。
|
||||
}
|
||||
//判断国家语言是否符合
|
||||
//let firstNameLanguage = await AliCloudUtil.getInstance().languageDetect(memberList[m].firstname);
|
||||
//this.logger.info("firstNameLanguage="+firstNameLanguage);
|
||||
|
||||
|
||||
//暂时不要username为空的 //TODO 后期处理UID的需要处理这里
|
||||
if(!memberList[m].username){
|
||||
//this.logger.info("用户" + memberList[m].username + "的username为空,跳过");
|
||||
continue;
|
||||
}
|
||||
//符合条件的成员
|
||||
//this.logger.info("符合条件的成员="+memberList[m].username);
|
||||
result.push(memberList[m]);
|
||||
}//end for
|
||||
this.logger.info("遍历群集合完毕,符合条件的成员已经有" + result.length + "个")
|
||||
//遍历群集合完毕。
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 整理要拉的人,并且进行账号和任务分配
|
||||
* @param taskId 任务id
|
||||
* @param canCollection 是否可以采集群成员 ?
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async pushData(taskId,canCollection=false){
|
||||
this.logger.info("开始pullMemberTask============================");
|
||||
let pullMemberTask=await MPullMemberTaskService.getInstance().findById(taskId);
|
||||
if(!pullMemberTask){
|
||||
this.logger.error("拉人任务不存在");
|
||||
return ;
|
||||
}else {
|
||||
this.logger.info("拉人任务存在");
|
||||
}
|
||||
//如果拉人任务已经启动,不能重复启动
|
||||
if(this.taskIdCacheObj[taskId]){
|
||||
this.logger.error("拉人任务已经启动,不能重复启动");
|
||||
return ;
|
||||
}else{
|
||||
this.logger.info("拉人任务没有启动,可以启动");
|
||||
this.taskIdCacheObj[taskId] = true;
|
||||
}
|
||||
//成员数组
|
||||
let mArr=[];
|
||||
//群成员数组
|
||||
//获取群组集合,从某个群集合拉人,搞成从群成员列表拉人
|
||||
const groupMuster = await MGroupMusterService.getInstance().findById(pullMemberTask.musterId);
|
||||
if(!groupMuster || !groupMuster.groupIds){
|
||||
this.logger.info("找不到群集合");
|
||||
return;
|
||||
}else{
|
||||
this.logger.info("找到群集合,群集合数量="+groupMuster.groupIds.length);
|
||||
}
|
||||
|
||||
let groupIdsArr=groupMuster.groupIds.split(",");
|
||||
if(!groupIdsArr.length){
|
||||
this.logger.info("群集合为空");
|
||||
return;
|
||||
}else{
|
||||
this.logger.info("groupIdsArr群集合不为空");
|
||||
}
|
||||
let isToCollect=false;
|
||||
//遍历群集合
|
||||
for(let i=0;i<groupIdsArr.length;i++) {
|
||||
this.logger.info("PullMemberTaskBus 群集合总共有" + groupIdsArr.length + "个群" + ",现在是第" + i + "个");
|
||||
//检查群组是否存在
|
||||
let group = await MGroupService.getInstance().findById(groupIdsArr[i]);
|
||||
|
||||
if (!group) {
|
||||
this.logger.info("群不存在,跳过,看下一个群。");
|
||||
continue;
|
||||
} else {
|
||||
this.logger.info(groupIdsArr[i] + "群存在");
|
||||
}
|
||||
//获取群成员列表 从mongo数据库
|
||||
//let memberArr=await GroupMembersService.getInstance().findByGroupId(groupIdsArr[i]);
|
||||
|
||||
//获取群成员列表改成从tg_user表中获取群成员列表,设置过滤条件
|
||||
// let filter={
|
||||
// groupId:{$in:groupIdsArr},
|
||||
// status:{$in:[UserStatus.normal,UserStatus.black]}
|
||||
// };
|
||||
|
||||
//从tg_user表获取群成员列表
|
||||
let memberArr = null;
|
||||
try {
|
||||
this.logger.info("通过群组id查找tg_user里面有多少user,当前群组id为: " + groupIdsArr[i])
|
||||
memberArr = await MUserService.getInstance().findByGroupId(groupIdsArr[i]);
|
||||
} catch (e) {
|
||||
this.logger.error("获取群成员列表失败", e);
|
||||
continue;
|
||||
}
|
||||
//如果这个群组没有成员,则去采集。如果采集成功了,那么就过滤合适的到mArr中,如果采集失败了,那么就跳过这个群组
|
||||
//如果采集发现是频道,则跳过,并且从群集合里面删除这个“群组”
|
||||
|
||||
//如果这个群没有群成员,让自动采集去采集
|
||||
if (!memberArr || !memberArr.length) {
|
||||
this.logger.info("没有从tg_user获取到群成员")
|
||||
//这个是否可采集,在哪里设置,怎么看到的都是false
|
||||
if (canCollection) {//可以采集
|
||||
this.logger.info("canCollection为true");
|
||||
this.logger.info("没有成员去自动采集")
|
||||
this.logger.info(group.link)
|
||||
//自动采集
|
||||
//都遍历群组了,应该是采集指定群,而不是采集群集合吧。
|
||||
//这个直接放到群集合那里吧,增加一个按钮采集这个群集合的群成员。
|
||||
//然后还可以看到这个群集合采集的状况
|
||||
//一个群采集完成后,然后拉人,还是所有群采集完成过后拉人?
|
||||
//如果这个群没有群成员,应该是直接采集这个群,怎么会是采集这个群集合? 那有两万个未采集的群,就调用两万次这个群集合的采集是不合理的。
|
||||
//要么就是在所有操作之前,进行群集合的成员采集。而不是放在这个单个群的循环里面。
|
||||
//await MusterCollectionMemberBus.getInstance().start(pullMemberTask.musterId, taskId, false);
|
||||
let collectResult = await GroupCollectionMemberBus.getInstance().start(groupIdsArr[i]);
|
||||
if (collectResult && !collectResult.err) {
|
||||
memberArr = collectResult;
|
||||
} else if (!collectResult) { //采集解为为空
|
||||
this.logger.warn("采集结果为空,跳过");
|
||||
continue;
|
||||
} else { //采集结果错误
|
||||
this.logger.error("采集结果的错误是:" + collectResult.err.toString());
|
||||
//进行处理
|
||||
//从某个群集合删除某个群id
|
||||
if (collectResult.err.indexOf("CHAT_ADMIN_REQUIRED")) { //其他比如群组不存在啥的也需要处理
|
||||
this.logger.info("CHAT_ADMIN_REQUIRED 说明这个是频道,从群集合中移除。移除前群组数量: " + await MGroupMusterService.getInstance().getCountOfGroupByMusterId(pullMemberTask.musterId));
|
||||
await MGroupMusterService.getInstance().deleteGroupIdFromMuster(groupIdsArr[i], pullMemberTask.musterId);
|
||||
this.logger.info("移除后数量:" + await MGroupMusterService.getInstance().getCountOfGroupByMusterId(pullMemberTask.musterId));
|
||||
}
|
||||
}
|
||||
//采集完毕之后需要做什么呢?
|
||||
//如果返回是一个频道,那么应该如何处理呢?群集合剔除这个频道
|
||||
isToCollect = true;
|
||||
//
|
||||
//break;
|
||||
} else {//不可以采集
|
||||
this.logger.info("没有成员,不自动采集,然后应该如何处理???");
|
||||
}
|
||||
} else {//如果这个群有群成员
|
||||
|
||||
}
|
||||
mArr.push(...memberArr); //为了兼容单群和复用单群采集的类,那么这里就一个群一个群弄。而不是所有群都弄好了整理好数据了再来。
|
||||
//在这里整理出符合条件的用户
|
||||
this.logger.info("未过滤的时候,有的个数:"+ mArr.length);
|
||||
mArr= await this.getSuitableUserListByTaskIdAndMemberList(taskId,mArr);
|
||||
this.logger.info("过滤后的个数:"+mArr.length);
|
||||
if(mArr.length===0){
|
||||
this.logger.info("获取到合适的为零个");
|
||||
continue;
|
||||
}
|
||||
|
||||
//进行分组
|
||||
this.logger.info("开始进行分组");
|
||||
//数组平分
|
||||
let arr = [];
|
||||
//每次要截取的个数
|
||||
let sCount=Math.floor(mArr.length/Number(pullMemberTask.accountCount));
|
||||
for(let i=0;i<Number(pullMemberTask.accountCount);i++){
|
||||
arr.push(mArr.slice(i*sCount,i*sCount+sCount));
|
||||
}
|
||||
//如果有取余加到最后
|
||||
if(mArr.length%Number(pullMemberTask.accountCount)>0){
|
||||
let lastArr=mArr.slice(-( (mArr.length) % (Number(pullMemberTask.accountCount))));
|
||||
arr[arr.length-1].push(...lastArr);
|
||||
}
|
||||
this.logger.info("平分数量"+pullMemberTask.accountCount);
|
||||
this.logger.info("arr长度="+arr.length);
|
||||
this.logger.info("mArr长度="+mArr.length);
|
||||
this.logger.info(this.taskIdKeyCacheObj[taskId]);
|
||||
|
||||
//进行拉人。一个群拉完了再下一个群?
|
||||
//多个拉人线程同时进行还是一个个进行呢? 即时先一个个进行也没关系。 //单个群的整理数据pushData
|
||||
let pullResult =await PullMembersBus.getInstance().pushData(pullMemberTask.groupId,groupIdsArr[i], arr, pullMemberTask.scriptProjectId, pullMemberTask.dayMaxCount, pullMemberTask.userStatusStr);
|
||||
//如果拉人成功
|
||||
if(pullResult && pullResult.msg==="success"){
|
||||
this.logger.info("拉人成功");
|
||||
//如果是单个群的情况。那么就可以结束任务了。
|
||||
//如果要他明天接着自动给这个群拉人,需要怎么做呢?
|
||||
//比如可以设定一个定时器,明天的什么时候进行拉人。计算获取可以拉人的时间。
|
||||
//直接sleep肯定是不科学的吧。
|
||||
}else{
|
||||
//this.logger.info("拉人失败");
|
||||
this.logger.info("拉人结果"+ pullResult);
|
||||
if(pullResult && pullResult.err &&pullResult.err.indexOf("RPCError: 403: CHAT_WRITE_FORBIDDEN (caused by channels.InviteToChannel)")!==-1){
|
||||
this.logger.info("拉人失败,这个群禁止拉人"); //有一次要拉的目标群弄成别人的了。而且别人禁止拉群。
|
||||
//看来要规范一下,不能设置错成外部群都没发现。
|
||||
|
||||
//要不要把没拉完的群成员返回来,然后继续拉?
|
||||
|
||||
}
|
||||
|
||||
//比如说没拉人账号了,那么就直接结束任务,然后通知主人
|
||||
}
|
||||
//拉人完了之后,需要把这个群id从群集合中删除//这里是否需要删除呢?
|
||||
|
||||
//拉人任务什么时候算结束。//如果拉人那个群今天达标了,那么如何处理,结束还是等时间到了自己继续拉人?
|
||||
|
||||
}//遍历群集合完毕
|
||||
//会到这里就是本来那个群就有成员,或者通过获取有了成员。
|
||||
|
||||
//整理出所有的拉人条件
|
||||
|
||||
//通过条件过滤出符合的user,然后去拉人。
|
||||
//拉人的是否要使用上队列,不然程序停止一下又要重新开始
|
||||
//使用队列也更容易控制并发和任务分配。
|
||||
|
||||
//进行分组,按组拉人
|
||||
}
|
||||
|
||||
//拉人前检查
|
||||
async checkBeforePull(){
|
||||
//检查任务是否存在
|
||||
|
||||
//检查任务是否已运行
|
||||
|
||||
//
|
||||
}
|
||||
|
||||
async start2(){
|
||||
|
||||
//间隔执行拉人
|
||||
|
||||
//select * from tg_user where
|
||||
//user所在的群的昵称包含指定一个关键字
|
||||
//一次获取一条还是获取几条呢?
|
||||
//获取过滤user条件
|
||||
//通过taskId获取
|
||||
//群名称包含关键字
|
||||
//群名称语言
|
||||
//昵称包含
|
||||
//昵称不包含
|
||||
|
||||
//通过条件从tg_user表获取要拉的人
|
||||
|
||||
//执行拉人
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动开始拉人。一个群拉人,多个群同时拉人 。多个群多个线程同时进行拉人。
|
||||
* 代理ip的获取QPS限制有可能会吃不消
|
||||
*
|
||||
* @param keyName
|
||||
* @param taskId
|
||||
* @param groupId
|
||||
* @param pullMemberTask 这个是任务的配置
|
||||
* @returns {Promise<null>}
|
||||
*/
|
||||
}
|
||||
|
||||
module.exports=PullMemberTaskBus;
|
||||
715
backend/src/bus/PullMembersBus.js
Normal file
@@ -0,0 +1,715 @@
|
||||
const logger = require("@src/util/Log4jUtil");
|
||||
const MTgAccountService = require("@src/service/MTgAccountService");
|
||||
const AccountUsage = require("@src/util/AccountUsage");
|
||||
const MGroupService = require("@src/service/MGroupService");
|
||||
const MApiDataService = require("@src/service/MApiDataService");
|
||||
const ClientBus = require("@src/client/ClientBus");
|
||||
const {Api} = require("telegram");
|
||||
const ProjectPullLogService=require("@src/dbService/ProjectPullLogService");
|
||||
const MProjectPullLog = require("@src/dbModes/MProjectPullLog");
|
||||
const RedisUtil = require("@src/util/RedisUtil");
|
||||
const MParamService = require("@src/service/MParamService");
|
||||
const {sleep} = require("@src/util/Util");
|
||||
const ParamKey = require("@src/static/ParamKey");
|
||||
const MPullMemberLogService = require("@src/service/MPullMemberLogService");
|
||||
const MPullMemberStatisticService = require("@src/service/MPullMemberStatisticService");
|
||||
const MPullMemberProjectStatisticService = require("@src/service/MPullMemberProjectStatisticService");
|
||||
const GroupMembersService = require("@src/dbService/GroupMembersService");
|
||||
const MongodUtil = require("@src/util/MongodbUtil");
|
||||
const UserStatus = require("@src/static/UserStatus");
|
||||
const MUserService = require("@src/service/MUserService");
|
||||
const RedisService = require("@src/service/RedisService");
|
||||
|
||||
const AccountController = require("@src/controller/AccountController");
|
||||
const axios = require("axios");
|
||||
const MProjectInviteLogService = require("@src/service/MProjectInviteLogService");
|
||||
|
||||
//从某个群拉人
|
||||
//从tg_user表拉人
|
||||
|
||||
//1 不使用队列方案
|
||||
//2 使用队列的方案
|
||||
|
||||
//单个群拉人和多个群拉人是可以互相继承的
|
||||
//然后从用用户池拉人区分出来
|
||||
|
||||
//准备好要拉的人,比如是一个数组。 或者是拉的时候一个个获取符合条件的
|
||||
|
||||
//多个号同时拉,类似于多线程如何实现?
|
||||
|
||||
//拉人策略,一个账号连续拉多少人还是所有账号进行轮询拉人?
|
||||
|
||||
//他是用分组和循环来实现类似于多线程的方式,还是用队列的方式?
|
||||
|
||||
//如何同一个群多个线程拉人的时间间隔不要超过限制,防止触发风控呢???
|
||||
//其实如果的确全自动,那么间隔拉得很长就好了,很多东西都不会触发发送。
|
||||
|
||||
//拿着所有的账号进行循环,就没有那么容易风控了。
|
||||
|
||||
//一个账号拉到限制或者拉到PeerFlood之后应该如何处理????
|
||||
|
||||
//拉人接口--从tg_user表拉人
|
||||
//队列拉人
|
||||
//群组拉人
|
||||
|
||||
//要拉的人的条件
|
||||
|
||||
//目前不知道一个账号大概能拉多少人?
|
||||
//是不是有很多账号没拉一个人就死号了呢?
|
||||
|
||||
//返回值: 之前单个群组只要处理好了,设置不需要返回值,但是多个群组拉人,就需要返回值,方便多个群组拉人的时候,统一处理。
|
||||
//1. 拉人成功
|
||||
//2. 拉人失败
|
||||
//3. 拉人超时
|
||||
//4. 拉人超过限制
|
||||
//5. 拉人超过PeerFlood
|
||||
//6. 拉人超过频率
|
||||
|
||||
//拉人的策略:
|
||||
//1. 单个账号拉人
|
||||
//2. 多个账号拉人
|
||||
//3. 单个群组拉人
|
||||
//4. 多个群组拉人
|
||||
//5. 单个群组拉人,多个账号拉人
|
||||
//6. 多个群组拉人,多个账号拉人
|
||||
|
||||
|
||||
class PullMembersBus{
|
||||
//单例模式,是不是这一个就不允许多个实例了?
|
||||
//也就是比如说如果那个群集合拉人的,自动拉人要用这个,就是不能再利用这个创建实例了?
|
||||
//群集合拉人也可以用这个才对。多个群就用群组id分开就是了。
|
||||
static getInstance() {
|
||||
if (!PullMembersBus.instance) {
|
||||
PullMembersBus.instance = new PullMembersBus();
|
||||
}
|
||||
return PullMembersBus.instance;
|
||||
}
|
||||
constructor() {
|
||||
this.logger=logger.getLogger("PullMembersBus");
|
||||
this.statisticService=MPullMemberStatisticService.getInstance();
|
||||
//正在拉群的群,key群id+下标,value是数组
|
||||
this.pullData={}; //这个是在哪里初始化的
|
||||
//key是群id,value是数组[key,key]
|
||||
this.groupdIdCacheObj={};
|
||||
//client缓存,key群id+下标,value是client
|
||||
this.clientCache={}
|
||||
}
|
||||
//拉人的来源
|
||||
//1 从哪个群到哪个群
|
||||
//2 从哪个群集合到哪个群
|
||||
//3 从用户池到哪个群
|
||||
|
||||
|
||||
|
||||
async getMemberListByGroupId(){
|
||||
|
||||
}
|
||||
|
||||
//拉僵尸粉函数
|
||||
|
||||
|
||||
//要拉的人的整理
|
||||
//通过群id获取群成员
|
||||
//通过群集合id获取群成员
|
||||
|
||||
//通过条件过滤出符合条件的用户
|
||||
|
||||
//把拉人条件设置在项目上,还是说像目前群组一样也可以灵活呢?
|
||||
|
||||
|
||||
//开始拉人要执行此方法 整理拉人数据,然后赋值到pullData数组进去。
|
||||
//arr是二维数组[[ {用户名username等} ]]
|
||||
//targetGroupId是选择的某个群的成员的群
|
||||
//userStatusStr:允许拉人的用户状态的列表字符串
|
||||
//整理 向数组推入要拉的人
|
||||
//如果多个群要拉人,要如何处理???
|
||||
/***
|
||||
*
|
||||
* @param groupId 要拉到哪个群去
|
||||
* @param targetGroupId 这个实际上好像都没有用上。
|
||||
* @param arr
|
||||
* @param scriptProjectId
|
||||
* @param dayMaxCount
|
||||
* @param userStatusStr
|
||||
* @returns
|
||||
*/
|
||||
async pushData(groupId,targetGroupId,arr,scriptProjectId,dayMaxCount,userStatusStr){
|
||||
//汇报一下拉人账号得概况
|
||||
//有多少个账号没封号的。
|
||||
//每个账号拉人限制多少人。
|
||||
//24小时内已经拉了多少人请求。
|
||||
//理论上还可以拉多少请求
|
||||
|
||||
//获取所有未封号的拉人账号 //在普通函数里面不能用await功能。所以得把pushData函数改成async函数,他得内部才可以使用await
|
||||
// let accounts = await MTgAccountService.getInstance().getAllAvailableInviteAccounts(); //所有未封号的。
|
||||
let inviteAccountsStatus= await AccountController.getInstance().getAllInviteAccountStatus();
|
||||
if(inviteAccountsStatus){
|
||||
//通过机器人接口发送消息,请求http接口
|
||||
//https://api.telegram.org/bot5092961456:AAHEt48QJN6fBhseBiK0IfNHf395-M_JRKs/sendMessage?chat_id=1102887169&text=%E6%8B%BF%E6%8D%8Fok666666
|
||||
//get请求上面的地址
|
||||
let url="https://api.telegram.org/bot5092961456:AAHEt48QJN6fBhseBiK0IfNHf395-M_JRKs/sendMessage?chat_id=1102887169&text="+JSON.stringify(inviteAccountsStatus);
|
||||
//http请求这个url
|
||||
axios.get(url).then();
|
||||
}
|
||||
|
||||
//遍历要拉的人数组。//arr的长度就是几个分组,也可以说是几个线程。
|
||||
for(let a=0;a<arr.length;a++){
|
||||
this.logger.info("现在进行的分组是: "+a);
|
||||
let membersArr=arr[a];
|
||||
//群组id和序号作为key
|
||||
let key=groupId.toString()+a.toString();
|
||||
if(this.pullData[key]){
|
||||
//如果里面已经有数据了,那么就展开并且追加进去
|
||||
this.pullData[key].push(...membersArr);
|
||||
}else{//
|
||||
//如果这个分组没有数据,那么就直接赋值。
|
||||
this.pullData[key]=membersArr
|
||||
}
|
||||
//如果这个群组的任务缓存存在数据,
|
||||
if(this.groupdIdCacheObj[groupId]){
|
||||
//
|
||||
this.groupdIdCacheObj[groupId].push(key);
|
||||
}else{
|
||||
//
|
||||
this.groupdIdCacheObj[groupId]=[key];
|
||||
}
|
||||
//这个要同步执行,还是异步执行呢?
|
||||
let inviteResult = await this.start(groupId, a.toString(), targetGroupId, scriptProjectId, dayMaxCount, userStatusStr);
|
||||
if(inviteResult&&inviteResult.msg){
|
||||
if(inviteResult.msg.indexOf("")){
|
||||
}
|
||||
}else{
|
||||
this.logger.info("inviteResult"+inviteResult);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/***
|
||||
* 成功则保存成功的,失败则保存失败的原因。拉人的过程的日志是否也会保存
|
||||
* 还是一次拉人请求智慧保存一个日志呢
|
||||
*
|
||||
* @param account
|
||||
* @param taskGroupId
|
||||
* @param targetGroupId
|
||||
* @param memberNickname
|
||||
* @param status
|
||||
* @param remark
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async savePullLog(account,taskGroupId,targetGroupId,memberNickname,status,remark){
|
||||
//
|
||||
//let redisKey=RedisUtil.maxPullMemberKey+account.id;
|
||||
//let dayCount=await MPullMemberLogService.getInstance().getCache(redisKey);
|
||||
let dayCount=RedisService.getInstance().getCountOfAccountInviteIn24HoursByPhone(account.phone); //会account为空为啥
|
||||
await MPullMemberLogService.getInstance().create({
|
||||
accountId:account.id,
|
||||
taskGroupId,
|
||||
targetGroupId,
|
||||
memberNickname,
|
||||
status,
|
||||
count:dayCount?dayCount:0,
|
||||
//count:0,
|
||||
remark
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
//获取账号,肯定是不要获取到超过限制的账号了。
|
||||
//实际上拉人不需要优先获取什么加过群的账号。反正都要进行一次是否在群里的判断
|
||||
|
||||
//获取client
|
||||
async getClient(key,groupId,account,targetGroupId){
|
||||
if(!account)return;
|
||||
this.logger.info("开始获取client");
|
||||
if(this.clientCache[key])return this.clientCache[key];
|
||||
//let client = await ClientBus.getInstance().getClient(account, item.apiId, item.apiHash, false);
|
||||
//let client = await require("@src/client/ClientBus").getInstance().getClient(account, item.apiId, item.apiHash, true);
|
||||
let client = await require("@src/client/ClientBus").getInstance().getClientByAccount(account,true);
|
||||
if(!client){
|
||||
this.logger.error("群拉人无Client");
|
||||
await this.savePullLog(account,groupId,targetGroupId,"",0,"群拉人无apiId");
|
||||
return null;
|
||||
}
|
||||
this.logger.info("getClient正在连接client");
|
||||
try{
|
||||
await client.connect();
|
||||
}catch(e){
|
||||
this.logger.error("getClient连接失败",e);
|
||||
}
|
||||
//
|
||||
this.clientCache[key]=client;
|
||||
this.logger.info("获取到client")
|
||||
return client;
|
||||
}
|
||||
|
||||
//结束。因为key算是分成多个线程的key,所以要结束的时候,要把key对应的client给结束掉。
|
||||
finished(groupId,key){
|
||||
delete this.pullData[key];
|
||||
if(this.clientCache[key]){
|
||||
//ClientBus.getInstance().deleteCache(this.clientCache[key]);
|
||||
//删除这个账号异步线程的client缓存
|
||||
require("@src/client/ClientBus").getInstance().deleteCache(this.clientCache[key]).then();
|
||||
|
||||
delete this.clientCache[key];
|
||||
}
|
||||
//
|
||||
if(this.groupdIdCacheObj[groupId] && this.groupdIdCacheObj[groupId].length){
|
||||
for(let i=0;i<this.groupdIdCacheObj[groupId].length;i++){
|
||||
if(this.groupdIdCacheObj[groupId][i] === key){
|
||||
this.groupdIdCacheObj[groupId].splice(i,1);
|
||||
}
|
||||
}
|
||||
}
|
||||
//
|
||||
if(this.groupdIdCacheObj[groupId] && this.groupdIdCacheObj[groupId].length===0){
|
||||
delete this.groupdIdCacheObj[groupId];
|
||||
}
|
||||
}
|
||||
|
||||
//根据群组id停止 。这个是删除所有的线程
|
||||
finishedByGroupId(groupId){
|
||||
if(!this.groupdIdCacheObj[groupId])return;
|
||||
let arr=this.groupdIdCacheObj[groupId];
|
||||
for(let i=0;i<arr.length;i++){
|
||||
this.finished(groupId,arr[i]);
|
||||
}
|
||||
//删除所有client
|
||||
for(let key in this.clientCache){
|
||||
if(this.clientCache[key].groupId===groupId){
|
||||
//ClientBus.getInstance().deleteCache(this.clientCache[key]);
|
||||
//删除这个账号异步线程的client缓存
|
||||
require("@src/client/ClientBus").getInstance().deleteCache(this.clientCache[key]).then();
|
||||
delete this.clientCache[key];
|
||||
}
|
||||
}
|
||||
//delete this.groupdIdCacheObj[groupId];//删除群组id
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* //本次最大拉人,每天最多拉多少人 如果是0则不限制,肯定不能不限制
|
||||
* @param groupId 群id 从哪个群拉人 ,如果从群集合采集,则为空,还是传入群集合ID
|
||||
* @param index 拉人分组的索引 index key
|
||||
* @param targetGroupId 目标群id 这个是来自哪个群组??
|
||||
* @param scriptProjectId 脚本项目id ,用户判断这个项目是否拉过这个用户。涉及到拉人任务中途可能修改了项目,所以需要判断
|
||||
* @param dayMaxCount 每日最大拉人数
|
||||
* @param userStatusStr 用户状态
|
||||
* @returns
|
||||
*/
|
||||
//拉人函数,是否整理好要拉的人了
|
||||
//是否过滤了项目id了
|
||||
//用户状态肯定是要传的,因为要拉的时候判断
|
||||
async start(groupId,index,targetGroupId,scriptProjectId,dayMaxCount,userStatusStr) {
|
||||
try{
|
||||
this.logger.info("开始群拉人 bus/PullMemberBus/start");
|
||||
let key=groupId.toString()+index.toString();//key是群id和分组索引
|
||||
let groupInfo;
|
||||
let account=null;
|
||||
let client=null;
|
||||
|
||||
//已经在运行了,不再启动 //这个群组的 这个线程 已经有客户端缓存
|
||||
if (this.clientCache[key]) {
|
||||
this.logger.info("已经在运行了,不再启动");
|
||||
return {
|
||||
code: 0,
|
||||
msg: "已经在运行了,不再启动"
|
||||
};
|
||||
}else {
|
||||
this.logger.info("开始启动拉人");
|
||||
}
|
||||
//判断
|
||||
let group=await MGroupService.getInstance().findById(groupId);
|
||||
if(!group){
|
||||
this.logger.error("xx群组不存在,退出拉人");
|
||||
return{
|
||||
code:0,
|
||||
msg:"XX群组不存在,退出拉人"
|
||||
};
|
||||
}
|
||||
//删除第一个元素
|
||||
let shift=()=>{
|
||||
this.pullData[key].shift();
|
||||
if(!this.pullData[key] || this.pullData[key].length===0){//如果没有待拉的人了,则结束
|
||||
this.finished(groupId,key);
|
||||
}
|
||||
}
|
||||
//
|
||||
let deleteClient=()=>{
|
||||
//ClientBus.getInstance().deleteCache(this.clientCache[key]);
|
||||
require("@src/client/ClientBus").getInstance().deleteCache(this.clientCache[key]);
|
||||
delete this.clientCache[key];
|
||||
account=null; //释放资源 不然client缓存是消除了。等下容易出现问题,比如直接使用这个进行获取client
|
||||
groupInfo=null;
|
||||
}
|
||||
//从数据都读取拉人停顿时间
|
||||
this.logger.info("开始获取拉人停顿时间");
|
||||
this.logger.info("ParamKey.inviteIntervalOfOneAccount:"+ParamKey.inviteIntervalOfOneAccount);
|
||||
let pullPauseTimeParam=await MParamService.getInstance().findByKey(ParamKey.inviteIntervalOfOneAccount);
|
||||
//let pullPauseTimeParam=await MParamService.getInstance().findByKey("inviteIntervalOfOneAccount");
|
||||
//拉人停顿时间
|
||||
let pauseTime=3000;
|
||||
if(pullPauseTimeParam){//如果有设置,则使用设置的值
|
||||
this.logger.info("拉人停顿时间:"+pullPauseTimeParam.value);
|
||||
pauseTime=Number(pullPauseTimeParam.value);
|
||||
}
|
||||
//通过死循环拉人,可以改成定时器。不然程序会卡死
|
||||
while (this.pullData[key] && this.pullData[key].length>0){ //如果有待拉的人,则继续拉 否则结束
|
||||
this.logger.info("进入while循环,拉人开始");
|
||||
this.logger.info("拉人数量:"+this.pullData[key].length);
|
||||
this.logger.info("pauseTime:"+pauseTime);
|
||||
await sleep(pauseTime);
|
||||
if(!this.pullData[key] || !this.pullData[key].length){
|
||||
this.logger.info("待拉人数量为0,退出");
|
||||
break;
|
||||
}
|
||||
let item=this.pullData[key][0];//获取第一个待拉的人。因为不符合条件都是从前面删除了,所以第一个就是要拉的人
|
||||
//获取账号要放在最前面,不然写入日志的时候一直报错没有account.id
|
||||
if(!account){//如果账号为空,获取一个
|
||||
account= await MTgAccountService.getInstance().getRandomAccountByUsageId(AccountUsage.群拉人);
|
||||
if(!account){ //如果获取不到账号,退出
|
||||
this.logger.error("群拉人找不到账号");
|
||||
await this.savePullLog("",groupId,targetGroupId,"",0,"群拉人找不到账号,退出");
|
||||
break;
|
||||
}else{
|
||||
this.logger.info("获取到账号:"+account.phone);
|
||||
}
|
||||
}
|
||||
//判断是否邀请过到项目 尽量前面少一些网络操作 防止操作额度的浪费和浪费网络流量
|
||||
//if(await MPullMemberLogService.getInstance().checkUserIsJoinByProjectIdAndUsername(scriptProjectId,item.username)){
|
||||
//if(await ProjectPullLogService.getInstance().findByUsernameAndScriptProjectId(item.username,scriptProjectId)){
|
||||
if(await MProjectInviteLogService.getInstance().findByUsernameAndScriptProjectId(scriptProjectId,item.username)){
|
||||
this.logger.info("加过项目,跳过")
|
||||
await this.savePullLog(account,groupId,targetGroupId,item.username,0,"加过项目,跳过");
|
||||
//return;
|
||||
shift();
|
||||
continue;
|
||||
}
|
||||
//判断群每天拉人数量是否超标
|
||||
//查询指定群今天已经拉了多少人
|
||||
if(await RedisService.getInstance().checkCountOfGroupInviteIn24HoursIsOutOfLimitByGroupId(groupId)){
|
||||
this.logger.info("群每天拉人数量超标,退出");
|
||||
await this.savePullLog(account,groupId,targetGroupId,item.username,0,"群每天拉人数量超标,退出");
|
||||
this.finished(groupId,key);
|
||||
return{
|
||||
code:0,
|
||||
msg:"群每天拉人数量超标,退出"
|
||||
};
|
||||
}else{
|
||||
this.logger.info("群每天拉人数量未超标,当前群组拉人数量为:"+await RedisService.getInstance().getCountOfGroupInviteIn24HoursByGroupId(groupId));
|
||||
}
|
||||
//判断每日最大
|
||||
if(dayMaxCount && dayMaxCount>0){
|
||||
//记录每日最大
|
||||
this.logger.info("每日最大拉人数量为:"+dayMaxCount);
|
||||
let dayMaxCountKey=RedisUtil.pullDayMaxCount+groupId;
|
||||
let pullDayMaxCount=await RedisUtil.getCache(dayMaxCountKey);
|
||||
if(pullDayMaxCount && pullDayMaxCount>=dayMaxCount){
|
||||
await this.savePullLog("",groupId,targetGroupId,"",0,"手动拉人=超过每日最大拉人数");
|
||||
break;
|
||||
}
|
||||
}
|
||||
//这是每次都获取,有点问题吧。//没有错,因为缓存处理已经封装到这个函数进去了。
|
||||
let client=await this.getClient(key,groupId,account,targetGroupId);
|
||||
if(!client){ //如果client为空,那么就获取一次,然后再次判断
|
||||
this.logger.error("拉人账号获取不到client"); //为什么获取不到????
|
||||
await this.savePullLog(account,groupId,targetGroupId,"",0,"拉人账号获取不到client");
|
||||
await this.finished(groupId,key);
|
||||
return {
|
||||
code:0,
|
||||
msg:"拉人账号获取不到client"
|
||||
};
|
||||
}else{
|
||||
this.logger.info("获取到client:"+client.phone);
|
||||
//await client.sendMessageByUsernameAndMessageContent("@sandy3306", "大哥,我开始拉人开始打工了。");
|
||||
//即使间隔十秒,还是很容易导致PEER_FLOOD,所以就注释掉吧。
|
||||
}
|
||||
//处理用户名undefined,为什么会有这种情况。
|
||||
//读取用户信息之前判断这个账号resolveUsername api是否超过限制
|
||||
//这个判断要在获取client之后,在获取用户信息或者群组信息之前
|
||||
if(client && client.phone){
|
||||
let checkResult = await RedisService.getInstance().checkCountOfResolveUsernameIn24HoursIsOutOfLimitByPhone(client.phone);
|
||||
if(checkResult){
|
||||
this.logger.info("账号读取resolveUsername api超过限制,删除客户端,跳过");
|
||||
await this.savePullLog(account,groupId,targetGroupId,"",0,"账号读取api超过限制,删除客户端,并跳过");
|
||||
deleteClient();
|
||||
continue;
|
||||
}
|
||||
}else{
|
||||
this.logger.info("client is null or client.phone is null");
|
||||
}
|
||||
let channelId;
|
||||
let accessHash;
|
||||
if(!groupInfo || groupInfo.err){
|
||||
this.logger.info("开始获取群信息");
|
||||
groupInfo=await client.getGroupInfoByLink(group.link);
|
||||
if (!groupInfo || groupInfo.err) {
|
||||
this.logger.error("获取不到群链接信息");
|
||||
//判断是不是client的问题
|
||||
if(!client.canContinue()){ //如果不能继续
|
||||
deleteClient();
|
||||
}else{ //如果可以继续
|
||||
shift();
|
||||
//return;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
//
|
||||
channelId=groupInfo.id;
|
||||
accessHash=groupInfo.accessHash;
|
||||
if(!client.groupIsJoin(groupInfo)){
|
||||
//需要先进行退群,防止因为已经加入的群组太多导致无法加入
|
||||
await client.leaveChannelExcept(group.username);//是否要处理返回一下退出了几个群啥的。
|
||||
let joinRes = await client.joinGroup(group,group.link);
|
||||
//判断是否有拉人的权限,有的群组设置了不允许拉人。
|
||||
if(!joinRes || joinRes.err){
|
||||
this.logger.error("加群失败");//为什么加群失败呢? 然后如何处理呢?
|
||||
if(joinRes.err){
|
||||
await this.savePullLog(account,groupId,targetGroupId,"",0,joinRes.err);
|
||||
}
|
||||
if(!client.canContinue()){
|
||||
deleteClient();
|
||||
client=null;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}else {
|
||||
channelId=groupInfo.id;
|
||||
accessHash=groupInfo.accessHash;
|
||||
}
|
||||
//
|
||||
let channelPeer=new Api.InputPeerChannel({
|
||||
channelId:channelId,
|
||||
accessHash:accessHash
|
||||
});
|
||||
|
||||
let userInfo=null;
|
||||
if(!item.username) {
|
||||
this.logger.info("拉人用户名为空 undefined");
|
||||
await this.savePullLog(account, groupId, targetGroupId, item.username, 0, "被拉的的用户username为空");
|
||||
//continue;
|
||||
//判断是不是有access_hash
|
||||
if(!item.access_hash){
|
||||
this.logger.info("拉人用户名为空,没有access_hash");
|
||||
await this.savePullLog(account, groupId, targetGroupId, item.username, 0, "被拉的的用户username为空,没有access_hash");
|
||||
continue;
|
||||
}else{
|
||||
this.logger.info("通过uid和access_hash读取用户信息")
|
||||
this.logger.info("id:"+item.id+" access_hash:"+item.access_hash)
|
||||
this.logger.info("user_id: " + item.user_id)
|
||||
//userInfo=await client.searchByUserId(item.id,item.access_hash);
|
||||
userInfo=await client.searchByUserId(item.user_id,item.access_hash);
|
||||
this.logger.info("searchByUserId userInfo:"+JSON.stringify(userInfo))
|
||||
}
|
||||
}else{
|
||||
//通过userid获取用户信息 //这个是不行,先通过有username的来拉
|
||||
//let userInfo=await client.searchByUserId(item.user_id);
|
||||
this.logger.info("通过username读取用户信息")
|
||||
userInfo=await client.getUserInfoByUsername(item.username);
|
||||
//每天读取的次数不能超过限制
|
||||
this.logger.info("getUserInfoByUsername userInfo="+JSON.stringify(userInfo));
|
||||
}
|
||||
|
||||
//如果没有获取到用户信息或者报错了。
|
||||
if(!userInfo || userInfo.err){
|
||||
this.logger.error("获取用户信息失败");//为什么失败呢? 然后如何处理呢?
|
||||
//if(userInfo.err){ //TypeError: Cannot read properties of undefined (reading 'err')
|
||||
if(userInfo && userInfo.err){
|
||||
await this.savePullLog(account,groupId,targetGroupId,item.username,0,userInfo.err);
|
||||
}
|
||||
//
|
||||
if(!client.canContinue()){
|
||||
deleteClient();
|
||||
client=null;
|
||||
//break;
|
||||
}else{
|
||||
shift();
|
||||
}
|
||||
continue;
|
||||
}
|
||||
//如果用户状态不为空
|
||||
if(userInfo.status){
|
||||
let obj={
|
||||
userStatus:userInfo.status.className,
|
||||
updateStatusTime:new Date().getTime()
|
||||
}
|
||||
//如果用户状态的className为离线 Offline的情况
|
||||
if(userInfo.status.className.toLowerCase() === UserStatus.UserStatusOffline.toLowerCase()){
|
||||
obj.lastOnlineTime=userInfo.status.wasOnline; //这个是时间是秒
|
||||
let date=new Date();
|
||||
date.setTime(obj.lastOnlineTime*1000)
|
||||
this.logger.info("用户的最后上线时间是:"+ date.toLocaleString()+ date.toLocaleDateString()+ date.toLocaleTimeString())
|
||||
let nowTimestamp=new Date().valueOf();
|
||||
//打印当前的北京时间
|
||||
let nowDate=new Date();
|
||||
this.logger.info('当前时间是'+nowTimestamp+'obj.stamp'+obj.lastOnlineTime);
|
||||
let TimePeriod=nowTimestamp - obj.lastOnlineTime*1000;
|
||||
//TimePeriod相当于是几天几小时几分钟几秒,按格式输出
|
||||
let days=Math.floor(TimePeriod/(24*3600*1000));
|
||||
let hours=Math.floor(TimePeriod%(24*3600*1000)/(3600*1000));
|
||||
let minutes=Math.floor(TimePeriod%(3600*1000)/(60*1000));
|
||||
let seconds=Math.floor(TimePeriod%(60*1000)/1000);
|
||||
this.logger.info("用户的最后上线时间是:"+ days+"天"+hours+"小时"+minutes+"分钟"+seconds+"秒")
|
||||
//console.log('timePeriod'+ TimePeriod);
|
||||
if(days>3){ //混着拉会不会更好。专门写一个类来处理这个和猜测封号原因
|
||||
this.logger.info("用户的最后上线"+days+"天,超过3天,滚犊子吧,不拉");
|
||||
await this.savePullLog(account,groupId,targetGroupId,item.username,0,"用户的最后上线"+days+"天,超过3天,滚犊子吧,不拉");
|
||||
shift();
|
||||
continue;
|
||||
}
|
||||
}
|
||||
//await GroupMembersService.getInstance().updateById(MongodUtil.getId(item._id),obj);
|
||||
//更新用户的最后上线时间 //userInfo.id还是userInfo._id
|
||||
//await MUserService.getInstance().updateLastOnlineTime(userInfo.id,obj.lastOnlineTime);
|
||||
}
|
||||
//过滤状态
|
||||
let userStatusList=null;
|
||||
if(userStatusStr){
|
||||
userStatusList=userStatusStr.split(",");
|
||||
this.logger.info("userStatusList:"+userStatusList);
|
||||
//把userStatusList全部转换成小写
|
||||
userStatusList=userStatusList.map(item=>{
|
||||
return item.toLowerCase();
|
||||
});
|
||||
}else{
|
||||
userStatusList=[];
|
||||
}
|
||||
//如果用户状态不为空
|
||||
if(userStatusList && userStatusList.length){
|
||||
if(!userInfo.status){
|
||||
this.logger.info("用户状态为空");
|
||||
await this.savePullLog(account, groupId, targetGroupId, item.username, 0, "用户状态为空");
|
||||
shift();
|
||||
continue;
|
||||
}
|
||||
//如果用户状态的className为离线
|
||||
if(userInfo.status && userInfo.status.className && userStatusList.indexOf((userInfo.status.className).toLowerCase())===-1){
|
||||
this.logger.info("用户状态不符合要求"+userInfo.status.className);
|
||||
//userStatusList:userStatusEmpty,userStatusOnline,userStatusOffline,userStatusRecently
|
||||
// UserStatusRecently 居然因为
|
||||
await this.savePullLog(account,groupId,targetGroupId,item.username,0,"用户状态不符合要求"+userInfo.status.className);
|
||||
shift();
|
||||
continue;
|
||||
}else{
|
||||
this.logger.info("用户状态符合要求,状态是:"+userInfo.status.className);
|
||||
}
|
||||
}else{
|
||||
this.logger.info("用户状态为空");
|
||||
}
|
||||
let userPeer=new Api.InputPeerUser({
|
||||
userId:userInfo.id,
|
||||
accessHash:userInfo.accessHash
|
||||
});
|
||||
const tgAccount=client.tgAccount;
|
||||
let inviteRes=await client.inviteToChannel(channelPeer,[userPeer]);
|
||||
await RedisService.getInstance().saveInviteRecord(client.phone, groupId, new Date().getTime());
|
||||
// let redisKey=RedisUtil.maxPullMemberKey+tgAccount.id;
|
||||
//let dayCount=await MTgAccountService.getInstance().getCache(redisKey);
|
||||
let alreadyInviteCountIn24Hour= await RedisService.getInstance().getCountOfAccountInviteIn24HoursByPhone(client.phone);
|
||||
|
||||
//let maxPullMember=await MParamService.getInstance().findByKey(ParamKey.maxPullMember);
|
||||
let maxInviteNumberOfPerAccountIn24Hours = await MParamService.getInstance().getMaxInviteNumberOfPerAccountIn24hours();
|
||||
if(alreadyInviteCountIn24Hour && alreadyInviteCountIn24Hour > maxInviteNumberOfPerAccountIn24Hours){
|
||||
this.logger.info("账号:"+client.phone+"已经邀请了"+alreadyInviteCountIn24Hour+"个人");
|
||||
this.logger.info("该账号拉人超过每日最大");
|
||||
await this.savePullLog(account,groupId,targetGroupId,item.username,0,"该账号超过每日最大");
|
||||
deleteClient();
|
||||
client=null;
|
||||
continue;
|
||||
}else{
|
||||
this.logger.info("账号:"+client.phone+"已经邀请了"+alreadyInviteCountIn24Hour+"个人");
|
||||
this.logger.info("每日账号最大拉人数是"+ maxInviteNumberOfPerAccountIn24Hours);
|
||||
}
|
||||
//
|
||||
if(inviteRes && !inviteRes.err){//邀请成功
|
||||
//记录
|
||||
let pullLog=new MProjectPullLog();
|
||||
// pullLog.setObject({
|
||||
// username:item.username,
|
||||
// scriptProjectId:scriptProjectId
|
||||
// });
|
||||
//await ProjectPullLogService.getInstance().create(pullLog.getObject());
|
||||
//之前的项目拉人记录是保存到mongodb的。现在改成保存到mysql
|
||||
try{
|
||||
this.logger.info("开始保存拉人项目记录");
|
||||
let res = await MProjectInviteLogService.getInstance().create({
|
||||
username:item.username,
|
||||
scriptProjectId:scriptProjectId,
|
||||
});
|
||||
this.logger.info("保存拉人项目记录成功: "+res);
|
||||
}catch(e){
|
||||
this.logger.error("保存项目拉人记录失败"+e);
|
||||
}
|
||||
|
||||
await this.savePullLog(account,groupId,targetGroupId,item.username,1,"");
|
||||
await MPullMemberProjectStatisticService.getInstance().saveTodayStatistic(scriptProjectId,true);
|
||||
await this.statisticService.saveTodayStatistic(groupId,true);
|
||||
}else{//邀请失败
|
||||
this.logger.info("开始保存拉人项目记录");
|
||||
let res = await MProjectInviteLogService.getInstance().create({
|
||||
username:item.username,
|
||||
scriptProjectId:scriptProjectId,
|
||||
});
|
||||
this.logger.info("保存拉人项目记录成功: "+res);
|
||||
|
||||
this.logger.info("邀请失败");
|
||||
this.logger.info("inviteRes" + JSON.stringify(inviteRes));
|
||||
//if(inviteRes.err){ //把这个换成else,不然 TypeError: Cannot read properties of null (reading 'err'
|
||||
//await this.savePullLog(account,groupId,targetGroupId,item.username,0,inviteRes.err);
|
||||
//有一次读取不到account.id, 然后报错了,不知道为什么。先删除写入日志
|
||||
await this.statisticService.saveTodayStatistic(groupId,false);
|
||||
await MPullMemberProjectStatisticService.getInstance().saveTodayStatistic(scriptProjectId,false);
|
||||
|
||||
if(inviteRes.err.indexOf("RPCError: 400: USER_BANNED_IN_CHANNEL (caused by channels.InviteToChannel)")!==-1){
|
||||
this.logger.info("USER_BANNED_IN_CHANNEL限制,换账号");
|
||||
await MTgAccountService.getInstance().updateNextTimeByPhone(client.phone,86400*1000);
|
||||
//await this.savePullLog(account,groupId,targetGroupId,item.username,0,"USER_BANNED_IN_CHANNEL限制,换账号");
|
||||
deleteClient();
|
||||
client=null;
|
||||
//await MTgAccountService.getInstance().setCache(redisKey,9999,86400000);
|
||||
continue;
|
||||
}
|
||||
|
||||
if(inviteRes.err.indexOf("RPCError: 403: CHAT_WRITE_FORBIDDEN (caused by channels.InviteToChannel)")!==-1){
|
||||
//这个是群组不允许采集,换账号都没有用。
|
||||
this.logger.info("RPCError: 403: CHAT_WRITE_FORBIDDEN (caused by channels.InviteToChannel) 换账号都没用,结束这个群的拉人");
|
||||
//await this.finishedByGroupId(groupId);//这样子会不会太粗暴导致其他线程无法正常返回?
|
||||
await this.finished(groupId,key);
|
||||
return {
|
||||
err:"RPCError: 403: CHAT_WRITE_FORBIDDEN (caused by channels.InviteToChannel)",
|
||||
}
|
||||
}
|
||||
//其他错误
|
||||
await this.savePullLog(account,groupId,targetGroupId,item.username,0,inviteRes.err);
|
||||
}
|
||||
if(!client.canContinue()){//账号不能继续拉人
|
||||
await MTgAccountService.getInstance().updateNextTimeByPhone(client.phone,86400*1000);
|
||||
//准备换账号
|
||||
this.logger.info("无法继续,换账号");
|
||||
//为什么无法继续,应该进行什么处理?
|
||||
deleteClient();
|
||||
client=null;
|
||||
//await MTgAccountService.getInstance().setCache(redisKey,9999,86400000);
|
||||
}
|
||||
//最后
|
||||
shift();
|
||||
}//end while
|
||||
//最后 是队列的人拉完了是一种情况
|
||||
//
|
||||
//如果是某个群不允许拉人应该如何处理?所有的对应的client都要删除掉
|
||||
//
|
||||
await this.finished(groupId,key);
|
||||
return {
|
||||
code:1,
|
||||
msg:"success", //success may have may possibility //one to one ,one to many . many is object, and in object is one to one or one to many and loop.so world is easy .it is just key and value
|
||||
err:null,
|
||||
data:null
|
||||
}
|
||||
}catch (e) {
|
||||
this.logger.error("PullMembersBus start里面报错:"+ e.toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports=PullMembersBus;
|
||||
2050
backend/src/client/BaseClient.js
Normal file
552
backend/src/client/Client.js
Normal file
@@ -0,0 +1,552 @@
|
||||
const MApiDataService = require("@src/service/MApiDataService");
|
||||
const MTgAccountService = require("@src/service/MTgAccountService");
|
||||
const SimUtil = require("@src/util/SimUtil");
|
||||
const BaseClient = require("@src/client/BaseClient");
|
||||
const NameGenerationService = require("@src/service/NameGenerationService");
|
||||
const StringUtil = require("@src/util/StringUtil");
|
||||
const UUID = require('uuid');
|
||||
const MGroupService = require("@src/service/MGroupService");
|
||||
const MGroupListenerService = require("@src/service/MGroupListenerService");
|
||||
const AmqpQueueName = require("@src/amqp/AmqpQueueName");
|
||||
const {NewMessage} = require("telegram/events");
|
||||
const ProxyUtil = require("@src/util/ProxyUtil");
|
||||
const RandomAvatarUtil=require("@src/util/RandomAvatarUtil");
|
||||
const MTGLoginCodeLogService = require("@src/service/MTGLoginCodeLogService");
|
||||
const RedisUtil = require("@src/util/RedisUtil");
|
||||
const MTGRegisterLogService = require("@src/service/MTGRegisterLogService");
|
||||
|
||||
|
||||
/**
|
||||
* 继承父类 基类BaseClient,然后增加强化BaseClient的功能。
|
||||
*
|
||||
*
|
||||
*/
|
||||
class Client extends BaseClient {
|
||||
|
||||
/**
|
||||
* 构造函数,初始化一些基础的参数
|
||||
* @param tgClient
|
||||
* @param tgAccount accounts表的某个账号的记录对应的对象
|
||||
* @param otherParam
|
||||
*/
|
||||
constructor(tgClient, tgAccount="",otherParam={}) {
|
||||
super(tgClient, tgAccount,otherParam);
|
||||
this.otherParam=otherParam;
|
||||
//唯一名字
|
||||
this.name = UUID.v1();
|
||||
|
||||
this.tggClient = tgClient
|
||||
this.checkIpLock=false;
|
||||
|
||||
//账号 账号的session还是什么???
|
||||
this.tgAccount=tgAccount;
|
||||
|
||||
//账号id 数据库里面主键id
|
||||
this.accountId="";
|
||||
if(tgAccount && tgAccount.id){
|
||||
this.accountId=tgAccount.id;
|
||||
}
|
||||
//手机号
|
||||
this.phone="";
|
||||
if(tgAccount && tgAccount.phone){
|
||||
this.phone=tgAccount.phone;
|
||||
}
|
||||
|
||||
//api参数的实例
|
||||
this.apiDataDao = MApiDataService.getInstance();
|
||||
|
||||
//自毁定时器,不为空则说明启动着
|
||||
this.selfDestroyTimer=null;
|
||||
|
||||
|
||||
//检测ip定时器
|
||||
this.checkIpTimer=null;
|
||||
|
||||
//是否已启动监听登陆码
|
||||
this.isStartListenerLoginCode=false;
|
||||
|
||||
|
||||
//检查ip //检查代理ip的连接情况???
|
||||
if(otherParam.proxy){
|
||||
//this._startCheckIp();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***
|
||||
* 开始检测ip ,这个是检查client的ip是否错误???
|
||||
* @private
|
||||
*/
|
||||
_startCheckIp(){
|
||||
this.logger.info("_startCheckIp")
|
||||
|
||||
if(this.checkIpTimer){
|
||||
//如果已经存在定时器则清除定时器,并结束返回
|
||||
clearInterval(this.checkIpTimer);
|
||||
return;
|
||||
}
|
||||
//设定检查ip的定时器 //检查ip的什么? 连接状态?
|
||||
//如果是那个代理IP挂了呢?
|
||||
this.checkIpTimer=setInterval(async ()=>{
|
||||
//
|
||||
if(this.checkIpLock)return;
|
||||
this.logger.info("_startCheckIp this.otherParam.proxy.timeout",this.otherParam.proxy.timeout); //超时时间 这个超时时间是怎么来的
|
||||
//把这个时间戳转换成北京时间展示
|
||||
let time=new Date(this.otherParam.proxy.timeout);
|
||||
let timeStr=time.toLocaleString();
|
||||
this.logger.info("_startCheckIp timeStr",timeStr);
|
||||
//如果代理的超时时间小于当前时间
|
||||
if(this.otherParam.proxy.timeout<=new Date().getTime()){
|
||||
this.checkIpLock=true;
|
||||
//await this.tgClient.disconnect(); //这个不会删除代理信息而需要重新设置tgClient._proxy吧????
|
||||
this.logger.info("checkIpTimer里面执行了 this.tgClient.disconnect()")
|
||||
//自动重试10000次?????
|
||||
//this.tgClient.connectionRetries=10000;
|
||||
try{
|
||||
await this.connect();
|
||||
}catch (e) {
|
||||
this.logger.error("_startCheckIp"+e.toString())
|
||||
}
|
||||
|
||||
}else{
|
||||
this.logger.info("_startCheckIp this.otherParam.proxy.timeout>new Date().getTime()")
|
||||
}
|
||||
},5000);
|
||||
}
|
||||
|
||||
/**
|
||||
* 开始自毁倒计时
|
||||
* 这个是为了自动离线那个登陆码监听的吧?????
|
||||
* @param time
|
||||
*/
|
||||
startSelfDestroyTimer(time){
|
||||
this.selfDestroyTimer=setTimeout(()=>{
|
||||
//自毁
|
||||
this.logger.info("client自毁"+this.phone);
|
||||
//这个自毁之后,后台还在不断接收消息,说明没有自毁成功。也就是很多地方导致duplicate的原因估计
|
||||
this.getClientBus().getInstance().deleteCache(this);
|
||||
},time);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***
|
||||
* 注册Telegram账号
|
||||
* @param obj 一个对象,包含了手机号,密码,登陆码
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async doRegisterByPhone(obj) {
|
||||
//
|
||||
async function setCancelState(){
|
||||
try{
|
||||
let bean=await SimUtil.getBean();
|
||||
bean.setStatus(obj.id, false).then();
|
||||
}
|
||||
catch (e) {
|
||||
this.logger.error("setCancelState error: " + e);
|
||||
}
|
||||
}
|
||||
//
|
||||
try {
|
||||
//apiId和apiHash更新注册使用次数
|
||||
let apiData = await this.apiDataDao.findOneByParam({
|
||||
where:{
|
||||
apiId: this.tgClient.apiId
|
||||
}
|
||||
});
|
||||
//api注册数增加1
|
||||
await this.apiDataDao.updateById(apiData.id, {
|
||||
registerCount: apiData.registerCount + 1
|
||||
});
|
||||
//短信id
|
||||
const id = obj.id;
|
||||
//手机号
|
||||
const phone = obj.phone;
|
||||
//账号用途Id
|
||||
const usageId = obj.usageId;
|
||||
//查询本地是否已经注册过
|
||||
let has = await MTgAccountService.getInstance().findByPhone(phone);
|
||||
if (has) {
|
||||
this.logger.error(`本地已注册过该手机:phone=${phone}`);
|
||||
await setCancelState();
|
||||
return;
|
||||
}
|
||||
//连接什么 client
|
||||
try {
|
||||
await this.connect();
|
||||
}catch (e) {
|
||||
this.logger.error(e.toString())
|
||||
}
|
||||
this.logger.info("开始发送短信")
|
||||
let sendRes = await this.sendCode(phone);
|
||||
this.logger.info("发送结束");
|
||||
if (!sendRes || sendRes.err) {
|
||||
this.logger.error(`发送验证码失败:id=${id}:phone=${phone}`);
|
||||
await setCancelState();
|
||||
return;
|
||||
}
|
||||
let phoneCodeHash = sendRes.phoneCodeHash;
|
||||
if (sendRes["type"]["className"] === "auth.SentCodeTypeApp") {
|
||||
let reSendRes = await this.reSendCode(phone, phoneCodeHash);
|
||||
if(!reSendRes || reSendRes.err){
|
||||
this.logger.error(`重新发送验证码失败:id=${id}:phone=${phone}`);
|
||||
await setCancelState();
|
||||
return;
|
||||
}
|
||||
phoneCodeHash = reSendRes.phoneCodeHash;
|
||||
}
|
||||
//等待验证码
|
||||
let bean=await SimUtil.getBean();
|
||||
//获取验证码
|
||||
let code = await bean.getSmsCode(id,phone);
|
||||
if (!code || code.err) { //验证码为空或者有错误
|
||||
await setCancelState();
|
||||
return;
|
||||
}
|
||||
//用手机号和验证码登录账号
|
||||
let loginRes = await this.signIn(phone, phoneCodeHash, code);
|
||||
this.logger.info("登陆结果"+JSON.stringify(loginRes));
|
||||
if(!loginRes || loginRes.err){
|
||||
this.logger.error(`登录失败:id=${id}:phone=${phone}`);
|
||||
return;
|
||||
}
|
||||
//随机姓名
|
||||
let firstName = "";
|
||||
let lastName = "";
|
||||
let psetting = "";
|
||||
|
||||
//未注册
|
||||
if (loginRes["className"] === "auth.AuthorizationSignUpRequired") { //
|
||||
// 使用新的统一姓名生成服务
|
||||
try {
|
||||
const nameOptions = {
|
||||
platform: 'telegram',
|
||||
culture: this.mapCountryToCulture(obj.country || '1'),
|
||||
gender: 'neutral',
|
||||
ageGroup: 'adult',
|
||||
prompt: obj.namePrompt || null
|
||||
};
|
||||
|
||||
const generatedName = await NameGenerationService.getInstance().generateName(nameOptions);
|
||||
firstName = generatedName.firstName || 'User';
|
||||
lastName = generatedName.lastName || 'Unknown';
|
||||
|
||||
this.logger.info(`使用 ${generatedName.generator} 生成的名字: ${firstName} ${lastName} (文化: ${nameOptions.culture})`);
|
||||
} catch (error) {
|
||||
this.logger.error("姓名生成服务失败,使用默认名字: " + error.toString());
|
||||
firstName = 'User';
|
||||
lastName = 'Unknown';
|
||||
}
|
||||
this.logger.info("执行注册")
|
||||
let regRes = await this.signUp(phone, phoneCodeHash, firstName, lastName);
|
||||
if(!regRes || regRes.err){
|
||||
this.logger.error(`注册失败:id=${id}:phone=${phone}`);
|
||||
await setCancelState();
|
||||
return;
|
||||
}
|
||||
this.logger.info("执行注册结束")
|
||||
this.logger.info("regRes: " + regRes)
|
||||
|
||||
//注册后已授权了再获取getPassword
|
||||
psetting = await this.getPassword();
|
||||
if(!psetting || psetting.err){
|
||||
this.logger.error(`获取getPassword错误:id=${id}:phone=${phone}`);
|
||||
await setCancelState();
|
||||
return;
|
||||
}
|
||||
} else { //
|
||||
//已注册,且授权了
|
||||
psetting = await this.getPassword();
|
||||
//已经设置过密码
|
||||
if (psetting.hasPassword) {
|
||||
this.logger.info(`phone=${phone}已设置过二步验证,跳过`);
|
||||
await setCancelState();
|
||||
return;
|
||||
}
|
||||
const user = this.getFullUser();
|
||||
if(!user || user.err){
|
||||
this.logger.info(`获取当前用户错误:id=${id}:phone=${phone}`);
|
||||
await setCancelState();
|
||||
return;
|
||||
}
|
||||
//获取已经设置的名字
|
||||
if(user && user.user && user.user.firstName){
|
||||
firstName = user.user.firstName;
|
||||
}
|
||||
if(user && user.user && user.user.lastName){
|
||||
lastName = user.user.lastName;
|
||||
}
|
||||
}
|
||||
//随机密码
|
||||
//let newPassword = StringUtil.getRandomStr(6);
|
||||
let newPassword = StringUtil.getRandomNumPwd(6); //换成数字密码方便一点
|
||||
|
||||
//设置2FA密码
|
||||
const faRes = await this.setPassword("", newPassword, psetting);
|
||||
if (!faRes || faRes.err) {
|
||||
this.logger.error(`phone=${phone}设置2FA密码失败`);
|
||||
await setCancelState();
|
||||
return;
|
||||
}
|
||||
//设置随机头像
|
||||
//let avatarPath=RandomAvatarUtil.randomAvatar();
|
||||
//更新头像
|
||||
//await this.updateAvatarByPath(avatarPath);
|
||||
|
||||
//设置账号的个性签名
|
||||
|
||||
//记录到数据库
|
||||
await MTgAccountService.getInstance().create({
|
||||
firstname: firstName,
|
||||
lastname: lastName,
|
||||
phone: phone,
|
||||
password: newPassword,
|
||||
usageId: usageId,
|
||||
session: this.tgClient.session.save()
|
||||
});
|
||||
|
||||
//记录今天已经注册的次数
|
||||
let dayCount=RedisUtil.getCache(RedisUtil.dayMaxRegisterCount);
|
||||
await RedisUtil.setCache(RedisUtil.dayMaxRegisterCount,dayCount?1:dayCount+1,86400000);
|
||||
|
||||
//更新注册日志
|
||||
await MTGRegisterLogService.getInstance().update({
|
||||
state:1,
|
||||
},{
|
||||
where:{
|
||||
phoneId:id
|
||||
}
|
||||
});
|
||||
|
||||
await setCancelState();
|
||||
} catch (e) {
|
||||
this.logger.error("执行注册报错了");
|
||||
this.logger.error(e);
|
||||
//处理报错。
|
||||
//如果报错了就先退出吧。方便注册处理。
|
||||
}finally {
|
||||
//取消客户端
|
||||
//await this.getClientBus().getInstance().deleteCache(this); //删除缓存
|
||||
await require("@src/client/ClientBus").getInstance().deleteCache(this); //删除缓存
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
{
|
||||
"flags": 264,
|
||||
"out": false,
|
||||
"mentioned": false,
|
||||
"mediaUnread": false,
|
||||
"silent": false,
|
||||
"post": false,
|
||||
"fromScheduled": false,
|
||||
"legacy": false,
|
||||
"editHide": false,
|
||||
"pinned": false,
|
||||
"noforwards": false,
|
||||
"id": 950657,
|
||||
"fromId": {
|
||||
"userId": "5288089777",
|
||||
"className": "PeerUser"
|
||||
},
|
||||
"peerId": {
|
||||
"channelId": "1520718769",
|
||||
"className": "PeerChannel"
|
||||
},
|
||||
"fwdFrom": null,
|
||||
"viaBotId": null,
|
||||
"replyTo": {
|
||||
"flags": 0,
|
||||
"replyToMsgId": 950652,
|
||||
"replyToPeerId": null,
|
||||
"replyToTopId": null,
|
||||
"className": "MessageReplyHeader"
|
||||
},
|
||||
"date": 1650395586,
|
||||
"message": "𝘈𝘹𝘢 𝘫𝘰𝘯𝘪𝘻 𝘴𝘰𝘨 𝘣𝘶𝘭𝘴𝘯🙄",
|
||||
"media": null,
|
||||
"replyMarkup": null,
|
||||
"entities": null,
|
||||
"views": null,
|
||||
"forwards": null,
|
||||
"replies": null,
|
||||
"editDate": null,
|
||||
"postAuthor": null,
|
||||
"groupedId": null,
|
||||
"reactions": null,
|
||||
"restrictionReason": null,
|
||||
"ttlPeriod": null,
|
||||
"className": "Message"
|
||||
}
|
||||
*/
|
||||
/***
|
||||
* 监听验证码消息
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async addListenerLoginCode(){
|
||||
//已开启监听的不再监听
|
||||
if(this.isStartListenerLoginCode)return;
|
||||
this.isStartListenerLoginCode=true;
|
||||
this.logger.info(this.phone+"的登陆码开始监听")
|
||||
|
||||
//添加事件处理器
|
||||
this.tgClient.addEventHandler((event)=>{
|
||||
const message=event.message;
|
||||
this.logger.info("监听到消息"+JSON.stringify(message))
|
||||
//JSON 数据为:
|
||||
|
||||
//私人消息
|
||||
if(event.isPrivate){ //应该是监听官方的那个账号,可以不担心语言问题 //监听码还没有不能转发或者发送的官方风控规则
|
||||
const msg=message.message;
|
||||
|
||||
//如果不是英语的就可能监听不到了。
|
||||
if(msg.indexOf("Login code:")!==-1){
|
||||
//登陆码
|
||||
const code=msg.slice(msg.indexOf(":")+2,msg.indexOf("."));
|
||||
this.logger.info("获取到登陆码:"+code);
|
||||
MTGLoginCodeLogService.getInstance().create({
|
||||
code:code,
|
||||
phone:this.phone
|
||||
}).then();
|
||||
}
|
||||
}
|
||||
},new NewMessage({}));
|
||||
}
|
||||
|
||||
/***
|
||||
* 开始监听群组消息,开启所有群组的监听。应该抽离出来按个监听的。
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async startListenerGroupMsg() {
|
||||
//查询需要监听的群数组
|
||||
// 每个群组增加是否启用的字段就可以单独开启关闭某个群的是否监听了。
|
||||
|
||||
const arr = await MGroupListenerService.getInstance().findAll();
|
||||
//获取所有需要监听的群组
|
||||
if (!arr || !arr.length) {
|
||||
return this.logger.error("无群组需要监听消息");
|
||||
}
|
||||
//判断只监听哪些消息
|
||||
let channelIds=[];
|
||||
|
||||
//循环查询有没有加入群,没有就加群
|
||||
for (let i = 0; i < arr.length; i++) {
|
||||
let group = await MGroupService.getInstance().findById(arr[i].groupId);
|
||||
if (!group) {
|
||||
this.logger.error("监听消息函数:查询群组为空");
|
||||
//删除这个群的监听记录
|
||||
continue;
|
||||
}
|
||||
let groupInfo = await this.getGroupInfoByLink(group.link);
|
||||
if(await this.getIsBan())return;
|
||||
if(!groupInfo || groupInfo.err){
|
||||
this.logger.error("查询不到群组");
|
||||
continue;
|
||||
}
|
||||
channelIds.push(String(groupInfo.id).trim());
|
||||
|
||||
//未加入群组
|
||||
if (!super.groupIsJoin(groupInfo)) {
|
||||
await super.joinGroup(group,group.link);
|
||||
}
|
||||
}
|
||||
|
||||
//监听消息
|
||||
this.logger.info("开始监听");
|
||||
this.tgClient.addEventHandler(async (update) => {
|
||||
//判断是否已被封号
|
||||
if(await this.getIsBan()){
|
||||
//取消监听数据
|
||||
this.getClientBus().getInstance().stopListenerGroupMsg();
|
||||
return;
|
||||
}
|
||||
if(!update.message)return;
|
||||
if(!update.message.peerId || !update.message.peerId.channelId || channelIds.indexOf(String(update.message.peerId.channelId)) ===-1){
|
||||
//不是要监听的群
|
||||
return;
|
||||
}
|
||||
|
||||
//监听到的不是群组新消息,return 还有个人消息还有什么消息呢
|
||||
if (update.originalUpdate.className !== "UpdateNewChannelMessage") {
|
||||
return;
|
||||
}
|
||||
this.logger.info("监听到群组新消息");
|
||||
let entities = update.message.entities || [];
|
||||
if (!entities.length) return;
|
||||
for (let i in entities) {
|
||||
let item = entities[i];
|
||||
//如果不是链接跳过
|
||||
if (item.className !== "MessageEntityTextUrl") continue;
|
||||
if(item.url.indexOf("https") === -1) continue;
|
||||
//不是有效的tg链接
|
||||
if (item.url.indexOf("t.me") === -1) continue;
|
||||
let after = item.url.slice(item.url.indexOf("t.me/") + 5);
|
||||
//有可能是http://t.me/username/xxx,所以一下操作
|
||||
if (after.indexOf("/") !== -1) {
|
||||
after=after.slice(0, after.indexOf("/"))
|
||||
}
|
||||
//用户名
|
||||
let username = after;
|
||||
let link=`https://t.me/${username}`;
|
||||
const old=await MGroupService.getInstance().findOneByUsername(username);
|
||||
if(old){
|
||||
this.logger.error("群监听-群组已存在",link);
|
||||
continue;
|
||||
}
|
||||
this.logger.info(link)
|
||||
//添加链接到群组队列
|
||||
await this.getAmqpBus().getInstance().send(AmqpQueueName.getInstance().importTg, Buffer.from(link));
|
||||
}
|
||||
},new NewMessage({}));
|
||||
}
|
||||
|
||||
/**
|
||||
* 将国家代码映射到文化代码
|
||||
* @param {string} country 国家代码(电话区号)
|
||||
* @returns {string} 文化代码
|
||||
*/
|
||||
mapCountryToCulture(country) {
|
||||
const countryToCultureMap = {
|
||||
'1': 'us', // 美国/加拿大
|
||||
'7': 'ru', // 俄罗斯
|
||||
'44': 'us', // 英国
|
||||
'49': 'us', // 德国
|
||||
'33': 'us', // 法国
|
||||
'39': 'us', // 意大利
|
||||
'34': 'es', // 西班牙
|
||||
'351': 'pt', // 葡萄牙
|
||||
'31': 'us', // 荷兰
|
||||
'81': 'jp', // 日本
|
||||
'82': 'kr', // 韩国
|
||||
'86': 'cn', // 中国
|
||||
'886': 'cn', // 台湾
|
||||
'852': 'cn', // 香港
|
||||
'853': 'cn', // 澳门
|
||||
'65': 'us', // 新加坡
|
||||
'60': 'us', // 马来西亚
|
||||
'66': 'th', // 泰国
|
||||
'84': 'vn', // 越南
|
||||
'63': 'ph', // 菲律宾
|
||||
'91': 'in', // 印度
|
||||
'55': 'pt', // 巴西
|
||||
'52': 'es', // 墨西哥
|
||||
'54': 'es', // 阿根廷
|
||||
'56': 'es', // 智利
|
||||
'57': 'es', // 哥伦比亚
|
||||
'51': 'es', // 秘鲁
|
||||
'20': 'us', // 埃及
|
||||
'966': 'us', // 沙特阿拉伯
|
||||
'971': 'us', // 阿联酋
|
||||
'972': 'us', // 以色列
|
||||
'98': 'us', // 伊朗
|
||||
'90': 'us' // 土耳其
|
||||
};
|
||||
|
||||
return countryToCultureMap[country] || 'us'; // 默认美国文化
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
module.exports = Client;
|
||||
710
backend/src/client/ClientBus.js
Normal file
@@ -0,0 +1,710 @@
|
||||
const {TelegramClient, Api} = require('telegram');
|
||||
const {StringSession} = require('telegram/sessions');
|
||||
const Client = require("@src/client/Client");
|
||||
const MApiDataService = require("@src/service/MApiDataService");
|
||||
const MTgAccountService = require("@src/service/MTgAccountService");
|
||||
const Logger = require("@src/util/Log4jUtil")
|
||||
const BaseRouter = require("@src/routers/BaseRouter");
|
||||
const AccountUsage = require("@src/util/AccountUsage");
|
||||
const MMessageMusterService = require("@src/service/MMessageMusterService");
|
||||
const schedule = require('node-schedule');
|
||||
const MGroupMusterService = require("@src/service/MGroupMusterService");
|
||||
const AmqpBus = require("@src/amqp/AmqpBus");
|
||||
const MGroupTaskService = require("@src/service/MGroupTaskService");
|
||||
const StringUtil = require("@src/util/StringUtil");
|
||||
const GroupTaskHandler = require("@src/amqp/handles/GroupTaskHandler");
|
||||
const ProxyUtil = require("@src/util/ProxyUtil");
|
||||
const {sleep} = require("@src/util/Util");
|
||||
const PullMembersBus = require("@src/bus/PullMembersBus");
|
||||
|
||||
|
||||
//客户端总线
|
||||
/**
|
||||
*
|
||||
* client的连接是否也放在这里,还是说放在调用的地方。
|
||||
* 我觉得放在这里面吧。更方便管理。
|
||||
*/
|
||||
class ClientBus {
|
||||
|
||||
//单例
|
||||
static getInstance() {
|
||||
if (!ClientBus.instance) {
|
||||
ClientBus.instance = new ClientBus();
|
||||
}
|
||||
return ClientBus.instance;
|
||||
}
|
||||
|
||||
constructor() {
|
||||
this.logger = Logger.getLogger("ClientBus");
|
||||
|
||||
//缓存所有已使用的号码,每一项是Client.原来是一个对象数组哦
|
||||
//防止一个账号被多个客户端使用,看下是否可以在这里处理
|
||||
this.cache = {};
|
||||
|
||||
//正在执行的任务(各类型任务),key:任务名,value:schedule.scheduleJob
|
||||
this.jobCache = {};
|
||||
|
||||
//任务处理缓存,key:任务名,value任务处理器
|
||||
this.taskHandlerCache={};
|
||||
|
||||
//正在监听消息的客户端,为空说明没运行
|
||||
this.listenerClient = null;
|
||||
}
|
||||
|
||||
|
||||
//添加缓存
|
||||
async addCache(client) {
|
||||
this.logger.info("addCache函数 client.name: " + client.name)
|
||||
this.cache[client.name]=client;
|
||||
//this.logger.info(this.cache);
|
||||
// if (client.phone) {
|
||||
// let has = false;
|
||||
// for (let i = 0; i < this.cache.length; i++) {
|
||||
// if (this.cache[i].name === client.name) {
|
||||
// has = true;
|
||||
// break;
|
||||
// }
|
||||
// }
|
||||
// if (!has) {
|
||||
// this.cache.push(client);
|
||||
// }
|
||||
// } else {
|
||||
// this.cache.push(client);
|
||||
// }
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除缓存?删除什么缓存?缓存在哪里?
|
||||
* 不删除会怎么样?
|
||||
* @param client 这个是客户端对象,哪个类的client呢
|
||||
* @returns {Promise<undefined|*>}
|
||||
*/
|
||||
async deleteCache(client) {
|
||||
if (!client) {
|
||||
this.logger.info("deleteCache函数 client为空");
|
||||
}else{ //client不为空
|
||||
//this.isDeleteCache是啥? 没有看到声明
|
||||
//可以通过this.变量名 直接声明变量?
|
||||
//使用 destroy 还是disconnect?
|
||||
//client.destroy();
|
||||
|
||||
if(this.isDeleteCache){
|
||||
this.logger.info("deleteCache函数 isDeleteCache为true");
|
||||
await sleep(5000);
|
||||
return this.deleteCache(client);
|
||||
}else{
|
||||
client.destroy(); //之前没有加这个,账号被占用了,会报错。消息也不断在拉取
|
||||
this.logger.info("deleteCache函数 isDeleteCache为 false");
|
||||
this.logger.info("deleteCache函数 client.name: " + client.name);
|
||||
this.isDeleteCache=true;
|
||||
this.cache[client.name]=null;
|
||||
delete this.cache[client.name];
|
||||
this.isDeleteCache=false;
|
||||
}
|
||||
}
|
||||
|
||||
// console.log("删除缓存client,删除前数量"+this.cache.length);
|
||||
// if (this.cache.length > 0) {
|
||||
// for (let i = 0; i < this.cache.length; i++) {
|
||||
// if ((this.cache[i].phone == client.phone) || (this.cache[i].name == client.name) ) {
|
||||
// console.log("找到销毁了的"+client.phone);
|
||||
// this.cache[i].destroy();
|
||||
// this.cache.splice(i, 1);
|
||||
// break;
|
||||
// }
|
||||
// }
|
||||
// } else {
|
||||
// client.destroy();
|
||||
// client = null;
|
||||
// }
|
||||
// console.log("删除后数量"+this.cache.length);
|
||||
}
|
||||
|
||||
//检查account是否已经有对应的client在缓存中,通过phone判断,是否在allUsedPhoneArr中
|
||||
async checkClientIsExistByPhone(phone){ //account是一个对象,包含phone和name 数据库对象
|
||||
//判断account.phone是否在allUsedPhoneArr中
|
||||
this.logger.info("checkClientIsExistByPhone函数 phone: " + phone);
|
||||
//打印有多少个已经在使用的phone
|
||||
this.logger.info("this.allUsedPhoneArr : "+this.allUsedPhoneArr());
|
||||
if (this.allUsedPhoneArr().indexOf(phone) >= 0) {
|
||||
this.logger.info("checkClientIsExist函数 phone已经存在client: " + phone);
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 所有正在使用的号码数组
|
||||
* @returns {*[]}
|
||||
*/
|
||||
allUsedPhoneArr() {
|
||||
let arr = [];
|
||||
// for (let i = 0; i < this.cache.length; i++) {
|
||||
// if (this.cache[i].phone) {
|
||||
// arr.push(this.cache[i].phone);
|
||||
// }
|
||||
// }
|
||||
for (let i = 0; i < Object.keys(this.cache).length; i++) {
|
||||
let item=Object.keys(this.cache)[i];
|
||||
if (this.cache[item]) {
|
||||
arr.push(this.cache[item].phone);
|
||||
}
|
||||
}
|
||||
return arr;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据手机号从缓存中获取client
|
||||
* @param phone
|
||||
* @returns {null|*}
|
||||
*/
|
||||
getClientByCacheAndPhone(phone){
|
||||
// for (let i = 0; i < this.cache.length; i++) {
|
||||
// if (this.cache[i].phone == phone) {
|
||||
// return this.cache[i];
|
||||
// }
|
||||
// }
|
||||
for (let i = 0; i < Object.keys(this.cache).length; i++) {
|
||||
let item=Object.keys(this.cache)[i];
|
||||
if (this.cache[item] && this.cache[item].phone === phone) {
|
||||
return this.cache[item];
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
/***
|
||||
* 获取客户端,获取的是未连接状态的,需要自行连接
|
||||
* 是否需要对client进行连接? 还是嗲用之后自己连接呢?
|
||||
* @param tgAccount 数据库accounts返回某个账号的记录的对象
|
||||
* @param apiId
|
||||
* @param apiHash
|
||||
* @param useProxy
|
||||
* @returns {Promise<null|Client>}
|
||||
*/
|
||||
async getClient(tgAccount, apiId, apiHash,useProxy) {
|
||||
// if(await this.checkClientIsExistByPhone(tgAccount.phone)){
|
||||
// this.logger.info("getClient函数 checkClientIsExistByPhone函数返回true");
|
||||
// return this.getClientByCacheAndPhone(tgAccount.phone);
|
||||
// }
|
||||
|
||||
if(!tgAccount){ //如果没有tgAccount,则返回null
|
||||
this.logger.info("getClient函数 tgAccount为空,注册账号时候需要创建空client");
|
||||
//注册账号的时候tgAccount就传参数是空的,是正常的。
|
||||
//return null;
|
||||
}else{
|
||||
this.logger.info("getClient函数 tgAccount不为空");
|
||||
}
|
||||
|
||||
//this.logger.info("tgAccount: " + tgAccount)
|
||||
this.logger.info("apiId: " + apiId)
|
||||
this.logger.info("apiHash"+ apiHash)
|
||||
|
||||
//Flood Wait 之后,session会读取不到而报错,而导致炒群失败
|
||||
let session = new StringSession(tgAccount.session);
|
||||
|
||||
//判断缓存里面是否有这个client
|
||||
|
||||
//构造TelegramClient参数
|
||||
let tgClientParam={
|
||||
//autoReconnect: true, //默认是true
|
||||
//delay:1000,
|
||||
//retryDelay:1000, //默认是1000
|
||||
deviceModel: "PC",
|
||||
systemVersion: "Windows 11",
|
||||
appVersion: "3.7.1",
|
||||
connectionRetries: 10, //原本只重试了两次。还有一个是.-1是无限次,容易导致程序就停在重试,不过可以设置高一些
|
||||
//50次,如果间隔是10秒,那么就是500秒。 如果是-1,那么就是无限次,但是如果是0,那么就是不重试?
|
||||
//看是不是绑定ip的,如果不是绑定ip的,那么就设计到多久换一个ip的问题,如果断开了。
|
||||
requestRetries:2000, //RPC_CALL_FAIL 只有这种错误的时候才会进行重试
|
||||
timeout:1000,
|
||||
|
||||
//洪水阀值
|
||||
floodSleepThreshold: 1000,
|
||||
};
|
||||
let proxyObj=null;
|
||||
if(useProxy){//如果使用代理的话。
|
||||
//proxyObj=await ProxyUtil.getInstance().getSocks5();
|
||||
//proxyObj=await ProxyUtil.getInstance().getStaticSocks5ByUsageId();
|
||||
proxyObj = await ProxyUtil.getInstance().getOneSocks5ByUsageId(tgAccount.usageId);
|
||||
|
||||
//如果没有获取到ip就返回错误,方便换一个ip重新。或者在底层自动更换
|
||||
|
||||
if(!proxyObj)return null;
|
||||
//没有获取到代理ip这里应该如何处理,上级应该如何处理呢
|
||||
|
||||
//this.logger.info("进入client/ClientBus代理ip流程 (bus/ClientBus还有一个代理的地方)")
|
||||
// 只有当代理有用户名和密码时才使用代理
|
||||
if(proxyObj.username && proxyObj.password) {
|
||||
tgClientParam.proxy={
|
||||
useWSS: false,
|
||||
ip:proxyObj.ip,
|
||||
port:proxyObj.port,
|
||||
username:proxyObj.username,
|
||||
password:proxyObj.password,
|
||||
socksType: 5,
|
||||
//超时时间10秒
|
||||
timeout:10, //默认是10秒
|
||||
MTProxy: false,
|
||||
};
|
||||
} else {
|
||||
this.logger.info("代理没有用户名密码,不使用代理连接");
|
||||
}
|
||||
}
|
||||
|
||||
let tgClient;
|
||||
try {
|
||||
//tgClient = new TelegramClient(tgAccount.phone, session, apiId, apiHash,tgClientParam);
|
||||
tgClient=new TelegramClient(session, Number(apiId), apiHash,tgClientParam);
|
||||
this.logger.info("clientBus getClient new TelegramClient 为 tgClient成功")
|
||||
//这个new一个,又没有连接啥的,这个创建是百分百成功的吧。
|
||||
|
||||
//this.logger.info("执行 tgClient.start();")
|
||||
//let startRes = tgClient.start();
|
||||
|
||||
}catch (e){
|
||||
this.logger.error("获取tgClient报错",e);
|
||||
return null;
|
||||
}
|
||||
|
||||
if(!tgClient){
|
||||
this.logger.info("tgClient为空,退出")
|
||||
return null;
|
||||
}else{
|
||||
this.logger.info("tgClient不为空")
|
||||
}
|
||||
|
||||
//tgClient.
|
||||
//tgClient已经有代理信息了,为啥还要传一个proxy参数呢???
|
||||
//tgClient直接可以用,为什么去搞一个Client类???
|
||||
//如果炒群根本没必要用下面这个Client
|
||||
|
||||
let client = new Client(tgClient, tgAccount,{
|
||||
proxy:proxyObj
|
||||
});
|
||||
|
||||
//new的Client不能传进去一个已经连接的tgClient吗???
|
||||
//调用getClient的地方会进行连接,不需要在这里连接 有得上层没有调用connect
|
||||
|
||||
// this.logger.info("ClientBus中getClient中进行connect")
|
||||
// try{
|
||||
// //连接之前先进行是否已经连接的判断??
|
||||
// let result= await client.connect();
|
||||
// }catch (e) {
|
||||
// //打印报错信息
|
||||
// this.logger.info(e.toString())
|
||||
// }
|
||||
|
||||
await this.addCache(client);
|
||||
if(!client){
|
||||
this.logger.info("clientBus getClient 为空")
|
||||
return null;}
|
||||
else{
|
||||
this.logger.info("client不为空")
|
||||
return client;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
//获取未登录的客户端
|
||||
/***
|
||||
*
|
||||
* @returns {Promise<Client|null>}
|
||||
*/
|
||||
async getGencClient() {
|
||||
//随机apiId
|
||||
let item = await MApiDataService.getInstance().getCanWorkRandomItem();
|
||||
if (!item) return null;
|
||||
let client = await this.getClient("", item.apiId, item.apiHash,true);
|
||||
try {
|
||||
this.logger.info("正在执行clientBus 里面的 getGencClient")
|
||||
await client.connect();
|
||||
await client.invoke(new Api.InitConnection({
|
||||
appVersion: "3.6.1", //appVersion: "2.7.1",
|
||||
//deviceModel: "Nexus 5", //deviceModel: "Nexus 5",
|
||||
deviceModel: "PC",
|
||||
systemVersion: "Windows 11"
|
||||
}));
|
||||
return client;
|
||||
} catch (e) {
|
||||
this.logger.error("获取未登录的客户端异常:" + e);
|
||||
await this.deleteCache(client);
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
/**
|
||||
* 获取可以正常工作的客户端,返回client
|
||||
* 就是已经进行了连接的。
|
||||
* @param usageId
|
||||
* @param useProxy
|
||||
* @returns {Promise<Client|null>}
|
||||
*/
|
||||
async getCanWorkClient(usageId, useProxy = true) {
|
||||
this.logger.info("getCanWorkClient在执行")
|
||||
//随机可工作的telegram账号 已经够是有序分配和调度,而不是随机获取
|
||||
let client=null;
|
||||
const account = await MTgAccountService.getInstance().getRandomAccountByUsageId(usageId);
|
||||
if (!account) {
|
||||
this.logger.info("getCanWorkClient中获取账号失败")
|
||||
return null;
|
||||
}else{
|
||||
this.logger.info("getCanWorkClient中获取账号成功")
|
||||
if(account.apiId && account.apiHash){
|
||||
this.logger.info("getCanWorkClient中获取账号成功,apiId和apiHash都不为空")
|
||||
client = await this.getClient(account, account.apiId, account.apiHash,useProxy);
|
||||
}else{
|
||||
//随机apiId 应该是有序分配,而不是随机
|
||||
let item = await MApiDataService.getInstance().getCanWorkRandomItem();
|
||||
if (!item) return null;
|
||||
client = await this.getClient(account, item.apiId, item.apiHash,useProxy);
|
||||
}
|
||||
try {
|
||||
await client.connect();
|
||||
return client;
|
||||
} catch (e) {
|
||||
this.logger.error("getCanWorkClient函数获取客户端异常,连接报错:" + e.toString());
|
||||
await this.deleteCache(client);
|
||||
}
|
||||
}
|
||||
this.logger.info("getCanWorkClient函数获取客户端结束??????")
|
||||
return null;
|
||||
}
|
||||
|
||||
//获取指定账号的client
|
||||
//account为tgAccount的数据库对象
|
||||
async getClientByAccount(account,useProxy) {
|
||||
//随机apiId 其实应该绑定分配或者均衡分配等规则。
|
||||
//每个账号固定一个apiId和apiHash
|
||||
if(!account){
|
||||
this.logger.info("getClientByAccount中获取账号失败")
|
||||
return null;
|
||||
}
|
||||
if(account.apiId && account.apiHash){
|
||||
return await this.getClient(account,account.apiId,account.apiHash,useProxy);
|
||||
}else{
|
||||
//随机apiId 应该是有序分配,而不是随机
|
||||
let item = await MApiDataService.getInstance().getCanWorkRandomItem();
|
||||
if (!item) return null;
|
||||
await MTgAccountService.getInstance().updateById(account.id,{
|
||||
apiId:item.apiId,
|
||||
apiHash:item.apiHash
|
||||
});
|
||||
return await this.getClient(account, item.apiId, item.apiHash,useProxy);
|
||||
}
|
||||
}
|
||||
|
||||
//这个获取到的client是已经连接的
|
||||
async getClientByUsageId(usageId){
|
||||
this.logger.info("getClientByUsageId在执行")
|
||||
return await this.getCanWorkClient(usageId);
|
||||
}
|
||||
|
||||
async getCollectClientByFunction(){
|
||||
//同样是获取采集类型的客户端
|
||||
}
|
||||
|
||||
async getCollectClientByOperation(operation){
|
||||
//同样是获取采集类型的客户端 //根据操作获取客户端
|
||||
if(operation==="getGroupMember"){
|
||||
|
||||
}
|
||||
|
||||
if(operation==="getGroupInfo"){
|
||||
|
||||
}
|
||||
|
||||
if(operation==="getUserInfo"){
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
async getClientByApiName(){
|
||||
//根据api获取客户端
|
||||
//获取telegram的api列表
|
||||
|
||||
}
|
||||
|
||||
async getAllClientByUsageId(usageId){
|
||||
|
||||
}
|
||||
|
||||
async getClientByUsageIdWithRandomAccount(usageId){
|
||||
await MTgAccountService.getInstance().getRandomAccountByUsageId(usageId);
|
||||
return await this.getCanWorkClient(usageId);
|
||||
}
|
||||
|
||||
async getClientByPhone(phone){
|
||||
let account= await MTgAccountService.getInstance().getAccountByPhone(phone);
|
||||
if(!account){
|
||||
return null;
|
||||
}
|
||||
return await this.getClientByAccount(account);
|
||||
}
|
||||
|
||||
//获取client,获取客户端
|
||||
//key 这个是一个任务组,或者一个key就是一个要拉的分组
|
||||
//groupId
|
||||
//account tg账号表的记录对象
|
||||
//targetGroupId
|
||||
async getClientByAccountAndGroupId(account,groupId){
|
||||
account=await MTgAccountService.getInstance().getPullAccountByGroupId(groupId);
|
||||
if(!account){
|
||||
this.logger.error("群拉人找不到账号");
|
||||
return null;
|
||||
}else{
|
||||
this.logger.info("获取到账号:"+account.phone);
|
||||
}
|
||||
let client=null;
|
||||
if(account.apiId && account.apiHash){
|
||||
client = await ClientBus.getInstance().getClient(account, account.apiId, account.apiHash, false); //拉人居然没有使用代理ip,草泥马。看来配置还有一个更高级的全局配置,这里色湖之了也不生效
|
||||
if(!client){
|
||||
return null;
|
||||
}else{
|
||||
await client.connect();
|
||||
}
|
||||
}else { //如果没有绑定好的,就随机获取一个可用的apiId
|
||||
let item = await MApiDataService.getInstance().getCanWorkRandomItem();
|
||||
if (!item) {
|
||||
this.logger.error("群拉人无apiId");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
console.log("获取到client")
|
||||
return client;
|
||||
}
|
||||
|
||||
async getOneConnectedCollectClient(){
|
||||
this.logger.info("进入getOneConnectedCollectClient");
|
||||
return this.getClientByUsageId(AccountUsage.采集);
|
||||
//await client.connect(); //因为底层会进行连接。
|
||||
}
|
||||
|
||||
//获取拉人的client,不指定groupId,不指定account
|
||||
async getOnePullClient(){
|
||||
//今日拉人数量不能是超过限制的
|
||||
//不能是已经在使用的
|
||||
//其实与client无关,主要去判断底层的account就好了。因为是通过account获取成client的
|
||||
//从redis获取今日已经拉的人数
|
||||
|
||||
let client = null;
|
||||
let account = MTgAccountService.getInstance().getOneAvailablePullAccount();
|
||||
client=await this.getClientByAccount(account);
|
||||
await client.connect();
|
||||
return client;
|
||||
}
|
||||
|
||||
|
||||
|
||||
async getOneInviteClient(){
|
||||
return await this.getOnePullClient();
|
||||
}
|
||||
|
||||
|
||||
//获取炒群的client
|
||||
async getScriptClient(){
|
||||
|
||||
}
|
||||
|
||||
async getClientByGroupId(groupId){
|
||||
}
|
||||
|
||||
|
||||
//取消并删除任务
|
||||
cancelJob(taskName) {
|
||||
console.log("删除缓存任务=" + taskName);
|
||||
if(this.jobCache[taskName]){
|
||||
this.jobCache[taskName].cancel();
|
||||
delete this.jobCache[taskName];
|
||||
}
|
||||
if(this.taskHandlerCache[taskName]){
|
||||
this.taskHandlerCache[taskName].cancel();
|
||||
delete this.taskHandlerCache[taskName];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//根据任务名判断是否已启动
|
||||
taskIsStartByName(taskName) {
|
||||
if (this.jobCache[taskName]) {
|
||||
this.logger.info("this.jobCache[taskName] 是否已经启动" +this.jobCache[taskName]);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
//所有所有任务名数组
|
||||
getAllJobNameArr() {
|
||||
return Object.keys(this.jobCache);
|
||||
}
|
||||
|
||||
//根据参数任务启动任务
|
||||
async startGroupTaskByTask(groupTask) {
|
||||
let res = new BaseRouter();
|
||||
let taskName = groupTask.name;
|
||||
const groupTaskId = groupTask.id;
|
||||
//判断任务是否已经在执行
|
||||
if (this.taskIsStartByName(taskName)) {
|
||||
return res.fail(null, "任务已运行");
|
||||
}
|
||||
|
||||
//任务处理器
|
||||
let taskHandler=new GroupTaskHandler(groupTask);
|
||||
this.taskHandlerCache[groupTask.name]=taskHandler;
|
||||
|
||||
// 发送的轮数
|
||||
let sendRoundsCount = 0;
|
||||
|
||||
//发送的消息下标,顺序和倒序使用
|
||||
let messageIndex = 0;
|
||||
//消息集合
|
||||
let messageMuster = await MMessageMusterService.getInstance().findById(groupTask.messageMusterId);
|
||||
//倒序消息
|
||||
if (groupTask.orderBy === 2) {
|
||||
let idsARrr = messageMuster.messageIds.split(",");
|
||||
//倒序消息下标从最后开始
|
||||
messageIndex = idsARrr[idsARrr.length - 1];
|
||||
}
|
||||
|
||||
//群发任务
|
||||
this.jobCache[taskName] = schedule.scheduleJob(groupTask.cron, async () => {
|
||||
//重新查询--避免后台更新删除等操作导致
|
||||
groupTask = await MGroupTaskService.getInstance().findById(groupTaskId);
|
||||
taskName = groupTask.name;
|
||||
if (!groupTask) {
|
||||
//可能被后台删除导致查不到了
|
||||
await this.cancelJob(taskName);
|
||||
return
|
||||
}
|
||||
//查询是否在任务时间内
|
||||
if(groupTask.startTime && groupTask.endTime){
|
||||
let currentDate=new Date();
|
||||
let currentTime= (currentDate.getHours() * 3600) + (currentDate.getMinutes()*60) + (currentDate.getSeconds());
|
||||
|
||||
let startTimeArr=groupTask.startTime.split(":");
|
||||
let startTime=(Number(startTimeArr[0])*3600) + (Number(startTimeArr[1]) * 60) + Number(startTimeArr[2]);
|
||||
|
||||
let endTimeArr=groupTask.endTime.split(":");
|
||||
let endTime=(Number(endTimeArr[0])*3600) + (Number(endTimeArr[1]) * 60) + Number(endTimeArr[2]);
|
||||
if(currentTime < startTime || currentTime > endTime){
|
||||
console.log("时间不符合");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
//查询群组集合
|
||||
let groupMuster = await MGroupMusterService.getInstance().findById(groupTask.groupMusterId);
|
||||
//群组ids
|
||||
let groupIdsArr = groupMuster.groupIds.split(",");
|
||||
//消息集合id
|
||||
let messageMusterId = groupTask.messageMusterId;
|
||||
//再查询一次消息集合,避免后台修改过集合
|
||||
messageMuster = await MMessageMusterService.getInstance().findById(messageMusterId);
|
||||
if (!messageMuster) {
|
||||
//消息集合找不到,取消任务
|
||||
await this.cancelJob(taskName);
|
||||
return;
|
||||
}
|
||||
//消息ids
|
||||
let messageIdsArr = messageMuster.messageIds.split(",");
|
||||
//无消息
|
||||
if (!messageIdsArr.length) {
|
||||
//无消息取消任务
|
||||
await this.cancelJob(taskName);
|
||||
return;
|
||||
}
|
||||
|
||||
for (let i = 0; i < groupIdsArr.length; i++) {
|
||||
//要发送的消息id
|
||||
let messageId = "";
|
||||
//0是随机消息
|
||||
if (groupTask.orderBy === 0) {
|
||||
//随机消息
|
||||
if (messageIdsArr.length === 1) {
|
||||
//只有一个消息模板的话直接取第一个
|
||||
messageId = messageIdsArr[0];
|
||||
} else {
|
||||
//多个消息模板随机
|
||||
let n = StringUtil.getRandomNum(0, messageIdsArr.length);
|
||||
messageId = messageIdsArr[n];
|
||||
}
|
||||
}
|
||||
|
||||
//顺序消息
|
||||
if (groupTask.orderBy === 1) {
|
||||
messageId = messageIdsArr[messageIndex];
|
||||
++messageIndex;
|
||||
//重置
|
||||
if (messageIndex >= messageIdsArr.length) messageIndex = 0;
|
||||
}
|
||||
//倒序消息
|
||||
if (groupTask.orderBy === 2) {
|
||||
messageId = messageIdsArr[messageIndex];
|
||||
--messageIndex;
|
||||
//重置
|
||||
if (messageIndex < 0) messageIndex = messageIdsArr.length - 1;
|
||||
}
|
||||
|
||||
//发送消息队列
|
||||
this.logger.info("加入消息队列,groupId=", groupIdsArr[i], ",messageId=", +messageId);
|
||||
AmqpBus.getInstance().send(groupTask.name, Buffer.from(JSON.stringify({
|
||||
groupId: groupIdsArr[i],
|
||||
messageId: messageId
|
||||
})));
|
||||
|
||||
}
|
||||
++sendRoundsCount;
|
||||
//判断每天发送最大的轮数
|
||||
if (sendRoundsCount >= groupTask.dayCount) {
|
||||
await this.cancelJob(taskName);
|
||||
console.log("该任务发送结束")
|
||||
}
|
||||
|
||||
|
||||
});
|
||||
|
||||
return res.success();
|
||||
}
|
||||
|
||||
//根据参数任务停止任务,task是群组任务
|
||||
async stopTaskByTask(task) {
|
||||
let res = new BaseRouter();
|
||||
let taskName = task.name;
|
||||
//任务没运行直接返回
|
||||
if (!this.taskIsStartByName(taskName)) {
|
||||
return res.success();
|
||||
}
|
||||
this.cancelJob(taskName);
|
||||
return res.success();
|
||||
}
|
||||
|
||||
//开始监听群组消息
|
||||
async startListenerGroupMsg() {
|
||||
let res = new BaseRouter();
|
||||
if (this.listenerClient) {
|
||||
return res.fail(null, "监听已启动");
|
||||
}
|
||||
let client = await this.getCanWorkClient(AccountUsage.采集, false);
|
||||
if (!client) {
|
||||
this.logger.error("监听消息:无获取空采集账号");
|
||||
return res.fail("监听消息:无获取空采集账号");
|
||||
}
|
||||
this.listenerClient = client;
|
||||
await client.startListenerGroupMsg();
|
||||
return res.success();
|
||||
}
|
||||
|
||||
//停止监听群组消息
|
||||
stopListenerGroupMsg() {
|
||||
this.deleteCache(this.listenerClient).then();
|
||||
this.listenerClient = null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
module.exports = ClientBus;
|
||||
75
backend/src/client/MtTgClient.js
Normal file
@@ -0,0 +1,75 @@
|
||||
const config=require("@src/config/Config");
|
||||
const {TelegramClient, Api} = require('telegram');
|
||||
const {StringSession} = require('telegram/sessions');
|
||||
const MApiDataService = require("@src/service/MApiDataService")
|
||||
const Logger = require("@src/util/Log4jUtil")
|
||||
|
||||
class MtTgClient {
|
||||
|
||||
constructor(phoneNumber, opts) {
|
||||
this.phoneNumber = phoneNumber
|
||||
this.opts = opts
|
||||
this.sessionString = opts['sessionString']
|
||||
this.apiDataService = MApiDataService.getInstance()
|
||||
this.logger = Logger.getLogger('MtTgClient');
|
||||
this.wClient = null
|
||||
}
|
||||
|
||||
async initClient() {
|
||||
let item = await this.apiDataService.getCanWorkRandomItem();
|
||||
this.apiId = item.apiId
|
||||
this.apiHash = item.apiHash
|
||||
let client = new TelegramClient(new StringSession( this.sessionString ) , this.apiId, this.apiHash, {})
|
||||
this.client = client;
|
||||
}
|
||||
|
||||
async isAuthorized() {
|
||||
return await this.client.isUserAuthorized()
|
||||
}
|
||||
|
||||
async isValid() {
|
||||
try {
|
||||
return await this.client.isUserAuthorized()
|
||||
} catch(e) {
|
||||
this.logger.error("Err checking auth:", e)
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async checkAuthorized() {
|
||||
if (!await this.client.isUserAuthorized()) {
|
||||
throw new Error(this.phoneNumber + " Not Authorized")
|
||||
}
|
||||
}
|
||||
// 发送验证码
|
||||
async sendCode() {
|
||||
await this.client.connect()
|
||||
let rst = await this.invoke(new Api.auth.SendCode({
|
||||
phoneNumber: this.phoneNumber,
|
||||
apiId: this.apiId,
|
||||
apiHash: this.apiHash,
|
||||
settings: new Api.CodeSettings({})
|
||||
}))
|
||||
this.sendCodeResult = rst;
|
||||
return rst;
|
||||
}
|
||||
|
||||
async invoke(fun) {
|
||||
await this.client.connect()
|
||||
return await this.client.invoke(fun)
|
||||
}
|
||||
|
||||
//登录验证
|
||||
async signIn(code){
|
||||
let rst = await this.invoke(new Api.auth.SignIn({
|
||||
phoneNumber: this.phoneNumber,
|
||||
phoneCodeHash: this.sendCodeResult.phoneCodeHash,
|
||||
phoneCode: code
|
||||
}));
|
||||
this.loginResult = rst;
|
||||
this.sessionString = this.client.session.save();
|
||||
return rst;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = MtTgClient;
|
||||
91
backend/src/client/MtTgClientBus.js
Normal file
@@ -0,0 +1,91 @@
|
||||
const ClientBus = require("@src/client/ClientBus")
|
||||
const MApiDataService = require("@src/service/MApiDataService")
|
||||
const MTgAccountService = require("@src/service/MTgAccountService");
|
||||
const config=require("@src/config/Config");
|
||||
const {TelegramClient, Api} = require('telegram');
|
||||
const MtTgClient = require("@src/client/MtTgClient")
|
||||
const Logger = require("@src/util/Log4jUtil")
|
||||
|
||||
class MtTgClientBus {
|
||||
|
||||
constructor(clientBus) {
|
||||
this.clientBus = clientBus
|
||||
this.apiDataService = MApiDataService.getInstance()
|
||||
this.accountService = MTgAccountService.getInstance()
|
||||
this.clientMap = {}
|
||||
this.logger = Logger.getLogger('MtTgClientBus');
|
||||
}
|
||||
|
||||
static getInstance(clientBus){
|
||||
if (!MtTgClientBus.instance) {
|
||||
MtTgClientBus.instance = new MtTgClientBus(clientBus);
|
||||
}
|
||||
return MtTgClientBus.instance;
|
||||
}
|
||||
|
||||
async getClient(phoneNumber, opts) {
|
||||
let client = this.clientMap[phoneNumber]
|
||||
if (!client || !(await client.isValid()) || (opts['sessionString'] && client.sessionString !== opts['sessionString'])) {
|
||||
|
||||
if (client && !(await client.isValid())) {
|
||||
await this.clientBus.deleteCache(client.wClient)
|
||||
}
|
||||
|
||||
this.logger.debug("init new client", phoneNumber, opts)
|
||||
let wClient = this.clientBus.getClientByCacheAndPhone(phoneNumber)
|
||||
client = new MtTgClient(phoneNumber, opts)
|
||||
if (wClient) {
|
||||
this.logger.debug("Found client in clientBus cache")
|
||||
client.client = wClient.tggClient
|
||||
client.sessionString = wClient.tggClient.session.save()
|
||||
} else {
|
||||
// 根据手机号获取相关的用户信息
|
||||
const account = await this.accountService.findByPhone(phoneNumber)
|
||||
if (account) {
|
||||
this.logger.debug("Found Db account", account.phone)
|
||||
let item = await this.apiDataService.getCanWorkRandomItem();
|
||||
client.apiId = item.apiId
|
||||
client.apiHash = item.apiHash
|
||||
wClient = await this.clientBus.getClient(account, client.apiId, client.apiHash, false)
|
||||
client.client = wClient.tggClient
|
||||
await wClient.connect()
|
||||
client.sessionString = wClient.tggClient.session.save()
|
||||
} else {
|
||||
this.logger.debug("New client initiating")
|
||||
await client.initClient();
|
||||
}
|
||||
}
|
||||
client.wClient = wClient
|
||||
this.clientMap[phoneNumber] = client
|
||||
}
|
||||
return client
|
||||
}
|
||||
|
||||
async getClient22(ph) {
|
||||
let account = await this.accountService.findByPhone(ph)
|
||||
let client;
|
||||
if (!account) {
|
||||
// throw new Error("not found account")
|
||||
// 数据库中没有数据,去缓存中查看是否有
|
||||
client = this.clientBus.getClientByCacheAndPhone(ph)
|
||||
if (!client) {
|
||||
// 如果没有, 则去登录
|
||||
console.log("RRst:", rst)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (!client) {
|
||||
let item = await this.apiDataService.getCanWorkRandomItem();
|
||||
client = await this.clientBus.getClient(account, item.apiId, item.apiHash, false)
|
||||
}
|
||||
|
||||
let user = await client.invokeFun(new Api.users.GetFullUser({
|
||||
id: new Api.InputUserSelf()
|
||||
}
|
||||
))
|
||||
return user
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = MtTgClientBus;
|
||||
14
backend/src/client/PullMemberBase.js
Normal file
@@ -0,0 +1,14 @@
|
||||
|
||||
//声明一个PullMemberBase类
|
||||
const MTgAccountService = require("@src/service/MTgAccountService");
|
||||
const AccountUsage = require("@src/util/AccountUsage");
|
||||
const MApiDataService = require("@src/service/MApiDataService");
|
||||
const ClientBus = require("@src/client/ClientBus");
|
||||
|
||||
|
||||
//看下把单群拉人和批量拉人的公用函数放在这里
|
||||
class PullMemberBase {
|
||||
//构造函数
|
||||
|
||||
|
||||
}
|
||||
104
backend/src/client/ScriptBus.js
Normal file
@@ -0,0 +1,104 @@
|
||||
const Logger = require("@src/util/Log4jUtil");
|
||||
const ScriptTaskClient=require("@src/client/ScriptTaskClient");
|
||||
const MScriptTaskService = require("@src/service/MScriptTaskService");
|
||||
|
||||
class ScriptBus{
|
||||
|
||||
|
||||
static getInstance() {
|
||||
if (!ScriptBus.instance) {
|
||||
ScriptBus.instance = new ScriptBus();
|
||||
}
|
||||
return ScriptBus.instance;
|
||||
}
|
||||
|
||||
constructor() {
|
||||
this.logger = Logger.getLogger();
|
||||
//script缓存,key:scriptTask的id,value:ScriptTaskClient
|
||||
this.scriptTaskCache={};
|
||||
|
||||
}
|
||||
|
||||
isStart(scriptTaskId){
|
||||
if(!this.scriptTaskCache[scriptTaskId])return;
|
||||
return true;
|
||||
}
|
||||
|
||||
allStartedIds(){
|
||||
return Object.keys(this.scriptTaskCache);
|
||||
}
|
||||
|
||||
async start(scriptTaskId){
|
||||
if(this.isStart(scriptTaskId)){
|
||||
this.logger.info("scriptTaskId:"+scriptTaskId+" is already started");
|
||||
//return;
|
||||
}else {
|
||||
this.logger.info("scriptTaskId:"+scriptTaskId+" is starting");
|
||||
// let scriptTaskClient=new ScriptTaskClient(scriptTaskId);
|
||||
// this.scriptTaskCache[scriptTaskId]=scriptTaskClient;
|
||||
this.scriptTaskCache[scriptTaskId]=new ScriptTaskClient(scriptTaskId);
|
||||
}
|
||||
}
|
||||
|
||||
//启动所有任务
|
||||
async startAllTask(){
|
||||
this.logger.info("startAllTask 开始启动所有任务");
|
||||
let scriptTaskIds=this.allStartedIds();
|
||||
//获取所有的任务,ScriptTask 启动
|
||||
let allScriptTask =await MScriptTaskService.getInstance().getAllScriptTask();
|
||||
if(allScriptTask.length===0){
|
||||
this.logger.info("没有任务");
|
||||
return;
|
||||
}else{
|
||||
this.logger.info("有任务个数为:"+allScriptTask.length);
|
||||
}
|
||||
//启动所有的任务,并缓存,已经启动的不再启动。
|
||||
// allScriptTask.forEach(scriptTask=>{ // TypeError: allScriptTask.forEach is not a function
|
||||
// if(scriptTaskIds.indexOf(scriptTask.id)===-1){
|
||||
// this.logger.info("启动任务:"+scriptTask.id);
|
||||
// this.start(scriptTask.id);
|
||||
// }
|
||||
// });
|
||||
|
||||
for(let i= 0 ;i<allScriptTask.length;i++){
|
||||
if(scriptTaskIds.indexOf(allScriptTask[i].id)===-1){
|
||||
this.logger.info("启动任务:"+allScriptTask[i].id);
|
||||
await this.start(allScriptTask[i].id);
|
||||
}else{
|
||||
this.logger.info("已经启动任务:"+allScriptTask[i].id);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
stopAllTask(){
|
||||
let scriptTaskIds=this.allStartedIds();
|
||||
scriptTaskIds.forEach(scriptTaskId=>{
|
||||
this.stop(scriptTaskId).then();
|
||||
});
|
||||
}
|
||||
|
||||
async resume(scriptTaskId){
|
||||
}
|
||||
|
||||
async pause(scriptTaskId){
|
||||
|
||||
}
|
||||
|
||||
async restart(scriptTaskId){
|
||||
|
||||
}
|
||||
async stop(scriptTaskId){
|
||||
//
|
||||
if(this.scriptTaskCache[scriptTaskId]){
|
||||
await this.scriptTaskCache[scriptTaskId].destroy();
|
||||
}
|
||||
//删除缓存
|
||||
delete this.scriptTaskCache[scriptTaskId];
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
module.exports=ScriptBus;
|
||||
1168
backend/src/client/ScriptTaskClient.js
Normal file
93
backend/src/config/Config.js
Normal file
@@ -0,0 +1,93 @@
|
||||
const dockerConfig = require('./DockerConfig');
|
||||
|
||||
const config = {
|
||||
isDev: process.env.NODE_ENV !== 'production',
|
||||
//JWT密钥
|
||||
jwtSecret: process.env.JWT_SECRET || 'tg-management-system-jwt-secret-2025',
|
||||
//服务端口
|
||||
serverPort: parseInt(process.env.SERVER_PORT || 3000, 10),
|
||||
//socket.io端口 - 使用随机端口避免冲突
|
||||
socketPort: parseInt(process.env.SOCKET_PORT || (3001 + Math.floor(Math.random() * 1000)), 10),
|
||||
//WebSocket服务器配置
|
||||
server: {
|
||||
secret: process.env.JWT_SECRET || 'tg-management-system-jwt-secret-2025'
|
||||
},
|
||||
//redis密码
|
||||
redis:{
|
||||
|
||||
},
|
||||
//redis
|
||||
redisUrl:"localhost",
|
||||
redisProUrl:"localhost",
|
||||
redisPassword:"",
|
||||
|
||||
//socket.io
|
||||
devSocketOrigin:process.env.FRONTEND_ORIGIN || "http://localhost:5173",
|
||||
proSocketOrigin:"https://tgtg.in",
|
||||
|
||||
//rabbitMq
|
||||
devRabbitMq:"amqp://localhost",
|
||||
proRabbitMq:"amqp://localhost",
|
||||
mqProUsername:"",
|
||||
mqProPassword:"",
|
||||
upload:{
|
||||
dev:"/Users/hahaha/telegram-management-system/uploads/",
|
||||
pro:"/upload/tgvip/",
|
||||
},
|
||||
db:{
|
||||
dev:{
|
||||
database:"tg_manage",
|
||||
username:"root",
|
||||
password:"",
|
||||
host:"127.0.0.1",
|
||||
dialect:"mysql"
|
||||
},
|
||||
pro:{
|
||||
database:"tg_manage",
|
||||
username:"tg_manage",
|
||||
password:"exAMScXpZe42zFHc",
|
||||
//host:"rm-2ev8h41x0m7bjt92k.mysql.rds.aliyuncs.com",
|
||||
host:"127.0.0.1",
|
||||
dialect:"mysql"
|
||||
}
|
||||
},
|
||||
mongodb:{
|
||||
dev:{
|
||||
url:"mongodb://localhost:27017/tg_manage"
|
||||
//url:"mongodb://118.107.40.97:28018/imapi"
|
||||
},
|
||||
pro:{
|
||||
url:"mongodb://127.0.0.1:27017/tg_manage"
|
||||
//url:"mongodb://198.11.173.134:27017/tg_manage"
|
||||
|
||||
//url:"mongodb://root:p0cGYPxelr6LymTL@dds-2ev46db4ca65c1741.mongodb.rds.aliyuncs.com:3717,dds-2ev46db4ca65c1742.mongodb.rds.aliyuncs.com:3717/admin?replicaSet=mgset-58981532"
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Apply Docker environment overrides
|
||||
if (process.env.NODE_ENV === 'production' || process.env.DOCKER_ENV) {
|
||||
// Override database configuration
|
||||
config.db.pro = dockerConfig.getDbConfig(config.db.pro);
|
||||
config.db.dev = dockerConfig.getDbConfig(config.db.dev);
|
||||
|
||||
// Override MongoDB configuration
|
||||
config.mongodb.pro = dockerConfig.getMongoConfig(config.mongodb.pro);
|
||||
config.mongodb.dev = dockerConfig.getMongoConfig(config.mongodb.dev);
|
||||
|
||||
// Override Redis configuration
|
||||
const redisConfig = dockerConfig.getRedisConfig();
|
||||
config.redisUrl = redisConfig.host;
|
||||
config.redisProUrl = redisConfig.host;
|
||||
config.redisPassword = redisConfig.password;
|
||||
|
||||
// Override RabbitMQ configuration
|
||||
config.devRabbitMq = dockerConfig.getRabbitMqConfig(true);
|
||||
config.proRabbitMq = dockerConfig.getRabbitMqConfig(false);
|
||||
|
||||
// Override upload path for Docker
|
||||
config.upload.pro = '/app/uploads/';
|
||||
config.upload.dev = '/app/uploads/';
|
||||
}
|
||||
|
||||
module.exports = config;
|
||||
58
backend/src/config/Db.js
Normal file
@@ -0,0 +1,58 @@
|
||||
const Sequelize = require('sequelize');
|
||||
const fs = require("fs");
|
||||
const path=require("path");
|
||||
const config = require("@src/config/Config");
|
||||
|
||||
class Db{
|
||||
|
||||
//单例
|
||||
static getInstance(){
|
||||
if(!Db.instance){
|
||||
Db.instance=new Db();
|
||||
}
|
||||
return Db.instance;
|
||||
}
|
||||
|
||||
constructor() {
|
||||
const dbParam=config.isDev?config.db.dev:config.db.pro;
|
||||
this.db = new Sequelize(dbParam.database, dbParam.username, dbParam.password, {
|
||||
host: dbParam.host,
|
||||
dialect: dbParam.dialect,
|
||||
//logging: (...msg) => console.log(msg), // 显示所有日志函数调用参数
|
||||
logging:false,
|
||||
pool: {
|
||||
max: 50,
|
||||
min: 1,
|
||||
acquire:60000,
|
||||
evict:1000,
|
||||
idle:10000,
|
||||
},
|
||||
});
|
||||
|
||||
//测试数据库链接
|
||||
this.db.authenticate().then(async()=>{
|
||||
console.log("数据库连接成功");
|
||||
//读取生成模型文件
|
||||
fs.readdirSync(path.join(process.cwd(),"src/modes")).filter(function (file) {
|
||||
return (file.indexOf(".") !== 0) && (file !== "Index.js") && file.endsWith('.js');
|
||||
}).forEach(function (file) {
|
||||
//导入
|
||||
require(path.join(process.cwd(),"src/modes",file));
|
||||
});
|
||||
//表不存在则创建表
|
||||
await this.db.sync();
|
||||
|
||||
|
||||
|
||||
}).catch(function(err) {
|
||||
//数据库连接失败时打印输出
|
||||
console.error(err);
|
||||
throw err;
|
||||
});
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
module.exports=Db;
|
||||
43
backend/src/config/DockerConfig.js
Normal file
@@ -0,0 +1,43 @@
|
||||
// Docker environment configuration override
|
||||
module.exports = {
|
||||
// Override database configuration from environment variables
|
||||
getDbConfig: (originalConfig) => {
|
||||
if (process.env.DB_HOST) {
|
||||
return {
|
||||
database: process.env.DB_NAME || originalConfig.database,
|
||||
username: process.env.DB_USER || originalConfig.username,
|
||||
password: process.env.DB_PASS || originalConfig.password,
|
||||
host: process.env.DB_HOST || originalConfig.host,
|
||||
dialect: originalConfig.dialect
|
||||
};
|
||||
}
|
||||
return originalConfig;
|
||||
},
|
||||
|
||||
// Override MongoDB configuration
|
||||
getMongoConfig: (originalConfig) => {
|
||||
if (process.env.MONGO_URL) {
|
||||
return {
|
||||
url: process.env.MONGO_URL
|
||||
};
|
||||
}
|
||||
return originalConfig;
|
||||
},
|
||||
|
||||
// Override Redis configuration
|
||||
getRedisConfig: () => {
|
||||
return {
|
||||
host: process.env.REDIS_HOST || 'localhost',
|
||||
port: process.env.REDIS_PORT || 6379,
|
||||
password: process.env.REDIS_PASSWORD || ''
|
||||
};
|
||||
},
|
||||
|
||||
// Override RabbitMQ configuration
|
||||
getRabbitMqConfig: (isDev) => {
|
||||
if (process.env.RABBITMQ_URL) {
|
||||
return process.env.RABBITMQ_URL;
|
||||
}
|
||||
return isDev ? "amqp://localhost" : "amqp://localhost";
|
||||
}
|
||||
};
|
||||
108
backend/src/controller/AccountController.js
Normal file
@@ -0,0 +1,108 @@
|
||||
|
||||
//账号控制器层
|
||||
// import MScriptArticleService from "@src/service/MScriptArticleService";
|
||||
// import MTgAccountService from "@src/service/MTgAccountService";
|
||||
// import MParamService from "@src/service/MParamService";
|
||||
// import RedisService from "@src/service/RedisService";
|
||||
const MScriptArticleService = require("@src/service/MScriptArticleService");
|
||||
const MTgAccountService = require("@src/service/MTgAccountService");
|
||||
const MParamService = require("@src/service/MParamService");
|
||||
const RedisService = require("@src/service/RedisService");
|
||||
|
||||
|
||||
|
||||
|
||||
const Logger = require("@src/util/Log4jUtil");
|
||||
//require 和 import 区别
|
||||
//require 只能在全局作用域下使用
|
||||
//import 可以在任何作用域下使用
|
||||
|
||||
|
||||
class AccountController{
|
||||
//单例模式
|
||||
static getInstance(){
|
||||
if(!AccountController.instance){
|
||||
AccountController.instance = new AccountController();
|
||||
}
|
||||
return AccountController.instance;
|
||||
}
|
||||
|
||||
constructor() {
|
||||
this.logger=Logger.getLogger("AccountController");
|
||||
}
|
||||
|
||||
|
||||
//汇报一下拉人账号得概况
|
||||
//有多少个账号没封号的。
|
||||
//每个账号拉人限制多少人。
|
||||
//24小时内已经拉了多少人请求。
|
||||
//理论上还可以拉多少请求
|
||||
|
||||
//获取所有未封号的拉人账号 //在普通函数里面不能用await功能。所以得把pushData函数改成async函数,他得内部才可以使用await
|
||||
|
||||
async getAllInviteAccountStatus(){
|
||||
let accounts = await MTgAccountService.getInstance().getAllAvailableInviteAccounts(); //所有未封号的。
|
||||
let accountCount = accounts.length;
|
||||
//计算理论最多可以拉多少请求 //can invite in theory
|
||||
//已经拉得请求总和
|
||||
//未拉的理论请求总和,有些账号虽然还没有超过限制,但是已经拉了一些请求了,要具体计算
|
||||
//遍历accounts
|
||||
//let canInviteCountInTheory = await MParamService.getInstance().getMaxInviteNumberOfPerAccountIn24hours()* accountCount;
|
||||
let maxInviteNumberPerAccountIn24Hours = await MParamService.getInstance().getMaxInviteNumberOfPerAccountIn24hours();
|
||||
this.logger.info("maxInviteNumberPerAccountIn24Hours:"+maxInviteNumberPerAccountIn24Hours);
|
||||
let inviteCountInTheory =maxInviteNumberPerAccountIn24Hours * accountCount;
|
||||
let alreadyInviteCountIn24Hours = 0;
|
||||
let leftInviteCountInTheory = 0; //理论的剩余拉人数
|
||||
|
||||
for(let i=0 ;i<accountCount;i++){
|
||||
let currentAccountInviteCount= await RedisService.getInstance().getCountOfAccountInviteIn24HoursByPhone(accounts[i].phone);
|
||||
alreadyInviteCountIn24Hours+=currentAccountInviteCount;
|
||||
if(currentAccountInviteCount<maxInviteNumberPerAccountIn24Hours){
|
||||
leftInviteCountInTheory+=maxInviteNumberPerAccountIn24Hours-currentAccountInviteCount;
|
||||
}
|
||||
//当前账号还剩多少拉人请求
|
||||
}
|
||||
this.logger.info("accountCount:"+accountCount);
|
||||
this.logger.info("inviteCountInTheory:"+inviteCountInTheory);
|
||||
this.logger.info("alreadyInviteCountIn24Hours:"+alreadyInviteCountIn24Hours);
|
||||
this.logger.info("leftInviteCountInTheory:"+leftInviteCountInTheory);
|
||||
return {
|
||||
accountCount:accountCount,
|
||||
maxInviteNumberPerAccountIn24Hours:maxInviteNumberPerAccountIn24Hours,
|
||||
inviteCountInTheory:inviteCountInTheory,
|
||||
alreadyInviteCountIn24Hours:alreadyInviteCountIn24Hours,
|
||||
leftInviteCountInTheory:leftInviteCountInTheory
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
//账号使用流程
|
||||
//获取Account
|
||||
//通过Account获取client
|
||||
//缓存client
|
||||
//使用client
|
||||
//清除client
|
||||
//清除Account
|
||||
//清除缓存
|
||||
|
||||
//一个调用到AccountService和PerformerService的方法,应该放在哪里呢?
|
||||
//放在AccountController里面
|
||||
//这样做的好处是:
|
||||
//1.调用方法的代码更加简洁
|
||||
//2.调用方法的代码更加易读
|
||||
//3.调用方法的代码更加容易调试
|
||||
//4.调用方法的代码更加容易扩展
|
||||
//5.调用方法的代码更加容易维护
|
||||
//6.调用方法的代码更加容易移植
|
||||
|
||||
async getAccountByPerformerId(performerId){
|
||||
//通过performerId获取AccountId
|
||||
let accountId = await MScriptArticleService.getInstance().getAccountIdByPerformerId(performerId);
|
||||
return await MTgAccountService.getInstance().getAccountById(accountId);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
//导出类
|
||||
module.exports = AccountController;
|
||||
20
backend/src/controller/AccountRaiseController.js
Normal file
@@ -0,0 +1,20 @@
|
||||
//养号控制器
|
||||
class AccountRaiseController{
|
||||
|
||||
|
||||
//一个一个账号养号,还是多个账号同时养号?
|
||||
|
||||
//手动启动养号还是自动启动养号
|
||||
|
||||
//互相发送私信消息
|
||||
|
||||
//在群里发送消息,发送多少条,发送什么内容?
|
||||
|
||||
//存活一定的天数,比如第一天不被获取,或者多少天内的账号不被获取
|
||||
|
||||
//获取对话有几个
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
19
backend/src/controller/AutomationController.js
Normal file
@@ -0,0 +1,19 @@
|
||||
|
||||
|
||||
//自动化的类
|
||||
class AutomationController{
|
||||
|
||||
//操作自动化
|
||||
|
||||
//识别自动化
|
||||
|
||||
//流程自动化
|
||||
|
||||
//自动化设置
|
||||
|
||||
//自动化模板
|
||||
|
||||
//自动化模板管理
|
||||
|
||||
|
||||
}
|
||||
21
backend/src/controller/AvatarController.js
Normal file
@@ -0,0 +1,21 @@
|
||||
|
||||
//AvatarController
|
||||
class AvatarController{
|
||||
//头像控制器
|
||||
//头像处理
|
||||
//头像大小尺寸限制
|
||||
//头像md5值
|
||||
//头像的唯一标识
|
||||
|
||||
//头像设置方案
|
||||
//手动设置
|
||||
//自动设置
|
||||
//设置头像的依据,是否有头像,是否有昵称,是否有性别,是否有生日,是否有手机号码
|
||||
|
||||
|
||||
//头像采集
|
||||
|
||||
//头像缓存
|
||||
|
||||
|
||||
}
|
||||
11
backend/src/controller/BotController.js
Normal file
@@ -0,0 +1,11 @@
|
||||
|
||||
|
||||
|
||||
class BotController{
|
||||
|
||||
constructor(){
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
module.exports = BotController;
|
||||
7
backend/src/controller/CheckController.js
Normal file
@@ -0,0 +1,7 @@
|
||||
|
||||
//检查类
|
||||
class CheckController{
|
||||
constructor(){
|
||||
}
|
||||
|
||||
}
|
||||
4
backend/src/controller/DeviceModelController.js
Normal file
@@ -0,0 +1,4 @@
|
||||
//设备型号管理控制类
|
||||
class DeviceModel{
|
||||
|
||||
}
|
||||