/**
* 刷新鲸云平台的代理IP并导入到数据库
* @param platform 平台标识
* @returns 刷新结果
*/
async refreshProxyPlatformIp(dto: RefreshProxyPlatformIpDto) {
this.logger.log(`开始刷新平台代理IP: ${dto.platform}`);
// 鲸云代理
if(dto.platform === JINGYUN') {
try {
// 1. 登录鲸云平台获取token
this.logger.log(`尝试登录鲸云平台: ${this.JINGYUN_CONFIG.LOGIN_URI}`);
const loginResponse = await axios.post(this.JINGYUN_CONFIG.LOGIN_URI, {
username: this.JINGYUN_CONFIG.LOGIN_USERNAME,
password: this.JINGYUN_CONFIG.LOGIN_PASSWORD
}, {
// 添加CORS相关配置
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
'Origin': 'https://z.51tcp.com'
},
// 添加超时配置
timeout: 30000,
// 忽略HTTPS证书错误
httpsAgent: new (require('https').Agent)({
rejectUnauthorized: false
})
});
this.logger.log(`登录鲸云平台响应状态: ${loginResponse.status}`);
if (!loginResponse.data || !loginResponse.data.user || !loginResponse.data.user.token) {
this.logger.error(`登录鲸云平台失败: ${JSON.stringify(loginResponse.data)}`);
return {
success: false,
message: '登录鲸云平台失败,无法获取token'
};
}
const token = loginResponse.data.user.token;
this.logger.log(`成功登录鲸云平台,获取token: ${token.substring(0, 15)}...`);
// 2. 获取账户列表
this.logger.log(`尝试获取鲸云账户列表: ${this.JINGYUN_CONFIG.LIST_URI}`);
const accountsResponse = await axios.get(this.JINGYUN_CONFIG.LIST_URI, {
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json',
'Accept': 'application/json',
'Origin': 'https://z.51tcp.com'
},
timeout: 30000,
httpsAgent: new (require('https').Agent)({
rejectUnauthorized: false
})
});
this.logger.log(`获取账户列表响应状态: ${accountsResponse.status}`);
if (!accountsResponse.data || !Array.isArray(accountsResponse.data)) {
this.logger.error(`获取鲸云账户列表失败: ${JSON.stringify(accountsResponse.data)}`);
return {
success: false,
message: '获取鲸云账户列表失败'
};
}
const accounts = accountsResponse.data;
this.logger.log(`获取到${accounts.length}个鲸云账户`);
// 3. 遍历账户,获取节点信息并保存到数据库
let totalProxies = 0;
let savedProxies = 0;
let skippedProxies = 0;
for (const account of accounts) {
// 只处理状态正常的账户
if (account.state !== 'normal') {
this.logger.warn(`账户 ${account.username} 状态异常: ${account.state},跳过`);
continue;
}
// 获取账户下的节点信息
const nodeInfoUrl = `${this.JINGYUN_CONFIG.NODE_INFO_URI}${account.id}`;
this.logger.log(`尝试获取账户节点信息: ${nodeInfoUrl}`);
try {
const nodeInfoResponse = await axios.get(nodeInfoUrl, {
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json',
'Accept': 'application/json',
'Origin': 'https://z.51tcp.com'
},
timeout: 30000,
httpsAgent: new (require('https').Agent)({
rejectUnauthorized: false
})
});
this.logger.log(`获取节点信息响应状态: ${nodeInfoResponse.status}`);
if (!nodeInfoResponse.data || !nodeInfoResponse.data.list || !Array.isArray(nodeInfoResponse.data.list)) {
this.logger.warn(`获取账户 ${account.username} 的节点信息失败: ${JSON.stringify(nodeInfoResponse.data)}`);
continue;
}
const nodes = nodeInfoResponse.data.list;
this.logger.log(`账户 ${account.username} 有 ${nodes.length} 个节点`);
// 将节点信息转换为代理IP格式并保存到数据库
const proxies = nodes.map(node => ({
host: node.xip, // 使用xip作为主机地址
port: node.http_port, // 使用http端口
username: account.username,
password: account.password,
isActive: true,
source: 'JINGYUN',
remark: `鲸云代理 - ${account.username} - ${node.group}`,
location: node.group,
expireAt: new Date(node.expire_time),
tags: `isp:${node.isp},nodeId:${node.id}`
}));
totalProxies += proxies.length;
// 批量保存到数据库
for (const proxy of proxies) {
try {
// 检查是否已存在相同的代理IP
const existingProxy = await this.proxyIpRepository.findOne({
where: {
host: proxy.host,
port: proxy.port,
isDeleted: 0
}
});
if (existingProxy) {
// 更新现有代理的部分信息
await this.proxyIpRepository.update(existingProxy.id, {
username: proxy.username,
password: proxy.password,
expireAt: proxy.expireAt,
tags: proxy.tags
});
skippedProxies++;
} else {
// 创建新代理
await this.proxyIpRepository.save(proxy);
savedProxies++;
}
} catch (error) {
this.logger.error(`保存代理IP ${proxy.host}:${proxy.port} 失败: ${error.message}`);
}
}
} catch (nodeError) {
this.logger.error(`获取账户 ${account.username} 节点信息失败: ${nodeError.message}`);
if (nodeError.response) {
this.logger.error(`状态码: ${nodeError.response.status}, 数据: ${JSON.stringify(nodeError.response.data)}`);
}
}
}
// 清除缓存
await this.clearProxyCache();
return {
success: true,
message: `成功从鲸云平台获取 ${totalProxies} 个代理IP,新增 ${savedProxies} 个,更新 ${skippedProxies} 个`
};
} catch (error) {
this.logger.error(`刷新鲸云代理失败: ${error.message}`);
if (error.response) {
this.logger.error(`状态码: ${error.response.status}, 数据: ${JSON.stringify(error.response.data)}`);
}
return {
success: false,
message: `刷新鲸云代理失败: ${error.message}`
};
}
} else if (dto.platform === 'QINGGUO') {
// 青果平台的实现
return {
success: false,
message: `暂未实现青果平台的代理刷新功能`
};
}
// 如果不是已知平台,返回未实现
return {
success: false,
message: `未知平台 ${dto.platform},无法刷新代理`
};
}
import { Injectable, Logger } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository, In, Like } from 'typeorm';
import { ProxyIpEntity } from './entities/proxy-ip.entity';
import { BatchCreateProxyIpDto, BatchUpdateProxyIpDto, CreateProxyIpDto, QueryProxyIpDto, UpdateProxyIpDto, RefreshProxyPlatformIpDto } from './dto/proxy-ip.dto';
import { BizException, BusinessException } from '~/common/exceptions/biz.exception';
import { ErrorEnum } from '~/constants/error.constant';
import { createPaginationObject } from '~/helper/pagination/create-pagination';
import { CacheService } from '~/shared/redis/cache.service';
import axios from 'axios';
import {
encryptRequestData,
generateSamsSignature,
appendSignatureHeaders,
isDev,
sendWechatBotMessageMarkdown,
samsBaseUrl
} from '~/utils/config.util';
import { TaskService } from '~/modules/system/task/task.service';
import { TaskStatus, TaskType } from '~/modules/system/task/constant';
@Injectable()
export class ProxyService {
private readonly logger = new Logger(ProxyService.name);
// 添加全局代理IP变量
private currentGlobalProxy: ProxyIpEntity | null = null;
private globalProxyLastRefresh: number = 0;
private readonly PROXY_REFRESH_INTERVAL = 300000; // 5分钟刷新一次全局代理
// 鲸云代理的配置
public JINGYUN_CONFIG = {
LOGIN_URI: 'https://z.51tcp.com/v2/login',
LOGIN_USERNAME:'**********',
LOGIN_PASSWORD:'m*****',
LOGIN_TOKEN:'',
LIST_URI:'https://z.51tcp.com/v2/account/list', // 请求一共有多少个账户列表
NODE_INFO_URI:'https://z.51tcp.com/v2/account/nodes?id=', // 请求账户的节点信息
}
constructor(
@InjectRepository(ProxyIpEntity)
private readonly proxyIpRepository: Repository<ProxyIpEntity>,
private readonly cacheService: CacheService,
private readonly taskService: TaskService,
) {}
/**
* 获取代理IP列表
* @param query 查询参数
* @returns 分页数据
*/
async findAll(query: QueryProxyIpDto) {
const { currentPage = 1, pageSize = 10, isActive, source, keyword } = query;
const queryBuilder = this.proxyIpRepository.createQueryBuilder('proxy');
// 默认只查询未删除的记录
queryBuilder.where('proxy.isDeleted = :isDeleted', { isDeleted: 0 });
// 根据是否可用筛选
if (isActive !== undefined) {
queryBuilder.andWhere('proxy.isActive = :isActive', { isActive });
}
// 根据来源筛选
if (source) {
queryBuilder.andWhere('proxy.source = :source', { source });
}
// 关键字搜索
if (keyword) {
queryBuilder.andWhere(
'(proxy.host LIKE :keyword OR proxy.remark LIKE :keyword OR proxy.tags LIKE :keyword)',
{ keyword: `%${keyword}%` }
);
}
// 按创建时间倒序排序
queryBuilder.orderBy('proxy.createdAt', 'DESC');
// 分页
const [items, total] = await queryBuilder
.skip((currentPage - 1) * pageSize)
.take(pageSize)
.getManyAndCount();
return createPaginationObject({
list: items,
currentPage,
pageSize,
total
});
}
/**
* 获取单个代理IP
* @param id 代理IP ID
* @returns 代理IP实体
*/
async findOne(id: number) {
const proxy = await this.proxyIpRepository.findOne({
where: { id, isDeleted: 0 },
});
if (!proxy) {
throw new BusinessException(ErrorEnum.PROXY_NOT_FOUND);
}
return proxy;
}
/**
* 创建代理IP
* @param dto 创建参数
* @returns 创建的代理IP实体
*/
async create(dto: CreateProxyIpDto) {
// 检查是否已存在相同的代理IP
const existingProxy = await this.proxyIpRepository.findOne({
where: {
host: dto.host,
port: dto.port,
isDeleted: 0,
},
});
if (existingProxy) {
throw new BusinessException(ErrorEnum.PROXY_ALREADY_EXISTS);
}
// 创建新的代理IP
const proxy = this.proxyIpRepository.create(dto);
await this.proxyIpRepository.save(proxy);
// 清除缓存
await this.clearProxyCache();
return proxy;
}
/**
* 批量创建代理IP
* @param dto 批量创建参数
* @returns 创建结果
*/
async batchCreate(dto: BatchCreateProxyIpDto) {
const { proxies, remark, source } = dto;
if (!proxies || proxies.length === 0) {
throw new BusinessException(ErrorEnum.PARAMS_INVALID, '代理IP列表不能为空');
}
// 检查数据库中是否已存在相同的代理IP
const existingProxies = await this.proxyIpRepository.find({
where: proxies.map(p => ({
host: p.host,
port: p.port,
isDeleted: 0,
})),
});
// 创建已存在代理的映射,用于快速查找
const existingMap = new Map();
existingProxies.forEach(proxy => {
const key = `${proxy.host}:${proxy.port}`;
existingMap.set(key, proxy);
});
// 过滤出不存在的代理
const newProxies = proxies.filter(proxy => {
const key = `${proxy.host}:${proxy.port}`;
return !existingMap.has(key);
});
if (newProxies.length === 0) {
return {
success: true,
count: 0,
skippedCount: proxies.length,
message: `所有代理IP已存在,已跳过 ${proxies.length} 个重复项`
};
}
// 批量创建代理IP 如果有备注还有来源的话进行添加
const proxyEntities = newProxies.map(p => {
const proxy = this.proxyIpRepository.create({
host: p.host,
port: p.port,
username: p.username,
password: p.password,
isActive: p.isActive,
});
if (remark) {
proxy.remark = remark;
}
if (source) {
proxy.source = source;
}
return proxy;
});
const savedProxies = await this.proxyIpRepository.save(proxyEntities);
return {
success: true,
count: savedProxies.length,
skippedCount: proxies.length - savedProxies.length,
message: `成功添加 ${savedProxies.length} 个代理IP,跳过 ${proxies.length - savedProxies.length} 个重复项`
};
}
/**
* 更新代理IP
* @param id 代理IP ID
* @param dto 更新参数
* @returns 更新后的代理IP实体
*/
async update(id: number, dto: UpdateProxyIpDto) {
const proxy = await this.findOne(id);
// 更新代理IP
Object.assign(proxy, dto);
await this.proxyIpRepository.save(proxy);
// 清除缓存
await this.clearProxyCache();
return proxy;
}
/**
* 批量更新代理IP
* @param dto 批量更新参数
* @returns 更新结果
*/
async batchUpdate(dto: BatchUpdateProxyIpDto) {
const { ids, data } = dto;
if (!ids || ids.length === 0) {
throw new BusinessException(ErrorEnum.PARAMS_INVALID, 'ID列表不能为空');
}
// 检查所有ID是否存在
const existingProxies = await this.proxyIpRepository.find({
where: { id: In(ids), isDeleted: 0 },
});
if (existingProxies.length !== ids.length) {
const existingIds = existingProxies.map(p => p.id);
const missingIds = ids.filter(id => !existingIds.includes(id));
throw new BusinessException(
ErrorEnum.PROXY_NOT_FOUND,
`以下ID不存在: ${missingIds.join(', ')}`
);
}
// 批量更新
await this.proxyIpRepository.update(ids, data);
// 清除缓存
await this.clearProxyCache();
return {
success: true,
count: ids.length,
message: `成功更新 ${ids.length} 个代理IP`
};
}
/**
* 删除代理IP
* @param id 代理IP ID
* @returns 删除结果
*/
async remove(id: number) {
const proxy = await this.findOne(id);
// 软删除
proxy.isDeleted = 1;
await this.proxyIpRepository.save(proxy);
// 清除缓存
await this.clearProxyCache();
return {
success: true,
message: '删除成功'
};
}
/**
* 批量删除代理IP
* @param ids 代理IP ID列表
* @returns 删除结果
*/
async batchRemove(ids: number[]) {
if (!ids || ids.length === 0) {
throw new BusinessException(ErrorEnum.PARAMS_INVALID, 'ID列表不能为空');
}
// 批量软删除
await this.proxyIpRepository.update(
{ id: In(ids), isDeleted: 0 },
{ isDeleted: 1 }
);
// 清除缓存
await this.clearProxyCache();
return {
success: true,
count: ids.length,
message: `成功删除 ${ids.length} 个代理IP`
};
}
/**
* 检测代理IP可用性
* @param id 代理IP ID
* @returns 检测结果
*/
async checkProxyAvailability(id: number) {
const proxy = await this.findOne(id);
const startTime = Date.now();
let isAvailable = false;
let responseTime = 0;
try {
const response = await this.tunnelRequest(
`${samsBaseUrl}/api/v1/sams/configuration/discoverIcon/getOneIcon`,
{
uid:"1818149605660"
},
{
isProxy: true,
proxyId: id, // 使用指定的代理ID进行检测
timeout: 10000, // 10秒超时
maxRetries: 1, // 不进行重试,因为我们是在检测这个代理
currentRetry: 0,
method: 'post',
}
);
// console.log('代理检测结果', response);
if(response && response?.success){
responseTime = Date.now() - startTime;
isAvailable = true;
}
// 更新代理状态
if (isAvailable) {
proxy.successCount += 1;
proxy.isActive = true;
// 更新平均响应时间
if (proxy.avgResponseTime === 0) {
proxy.avgResponseTime = responseTime;
} else {
proxy.avgResponseTime = (proxy.avgResponseTime + responseTime) / 2;
}
} else {
proxy.failCount += 1;
proxy.isActive = false;
}
} catch (error) {
this.logger.error(`代理检测失败: ${proxy.host}:${proxy.port} - ${error.message}`);
proxy.failCount += 1;
proxy.isActive = false;
}
// 更新检测时间
proxy.lastCheckedAt = new Date();
await this.proxyIpRepository.save(proxy);
// 清除缓存
await this.clearProxyCache();
return {
id: proxy.id,
host: proxy.host,
port: proxy.port,
isAvailable,
responseTime: isAvailable ? responseTime : -1,
};
}
/**
* 批量检测代理IP可用性
* @param ids 代理IP ID列表
* @returns 检测结果
*/
async batchCheckProxyAvailability(ids: number[]) {
if (!ids || ids.length === 0) {
throw new BusinessException(ErrorEnum.PARAMS_INVALID, 'ID列表不能为空');
}
const results = [];
// 这里进行异步后台检测
let ip_length = ids.length
let that = this
for(let i =0; i < ip_length; i++) {
setTimeout(() => {
that.checkProxyAvailability(ids[i])
})
}
return {
success: true,
results,
availableCount: results.filter(r => r.isAvailable).length,
totalCount: results.length,
};
}
/**
* 获取随机可用代理
* @param excludeIds 排除的代理ID列表
* @returns 随机代理
*/
async getRandomProxy(excludeIds: number[] = []) {
const cacheKey = 'proxy:available';
let availableProxies = await this.cacheService.get<ProxyIpEntity[]>(cacheKey);
if (!availableProxies) {
availableProxies = await this.proxyIpRepository.find({
where: { isActive: true, isDeleted: 0 },
});
// 缓存5分钟
await this.cacheService.set(cacheKey, availableProxies, 300);
}
// 过滤排除的ID
const filteredProxies = availableProxies.filter(p => !excludeIds.includes(p.id));
if (filteredProxies.length === 0) {
return null;
}
// 随机选择一个代理
const randomIndex = Math.floor(Math.random() * filteredProxies.length);
const selectedProxy = filteredProxies[randomIndex];
// 更新最后使用时间
await this.proxyIpRepository.update(
{ id: selectedProxy.id },
{ lastUsedAt: new Date() }
);
return selectedProxy;
}
/**
* 从文件导入代理IP
* @param file 文件
* @param source 代理来源
* @returns 导入结果
*/
async importFromFile(file: Express.Multer.File, source: string) {
if (!file) {
throw new BusinessException(ErrorEnum.PARAMS_INVALID, '文件不能为空');
}
const content = file.buffer.toString('utf-8');
const lines = content.split('\n').filter(line => line.trim() !== '');
const proxies: CreateProxyIpDto[] = [];
for (const line of lines) {
const parts = line.trim().split(':');
if (parts.length >= 2) {
const host = parts[0];
const port = parseInt(parts[1], 10);
// 验证IP和端口
if (/^(\d{1,3}\.){3}\d{1,3}$/.test(host) && !isNaN(port) && port > 0 && port <= 65535) {
const proxy: CreateProxyIpDto = {
host,
port,
source,
isActive: true,
};
// 如果有用户名和密码
if (parts.length >= 4) {
proxy.username = parts[2];
proxy.password = parts[3];
}
proxies.push(proxy);
}
}
}
if (proxies.length === 0) {
throw new BusinessException(ErrorEnum.PARAMS_INVALID, '文件中没有有效的代理IP');
}
return this.batchCreate({ proxies });
}
/**
* 清除代理缓存
*/
private async clearProxyCache() {
const redis = this.cacheService.getClient();
await redis.del('proxy:available');
}
/**
* 从config.util.ts加载代理列表到数据库
* @returns 导入结果
*/
async syncFromConfigUtil() {
try {
// 动态导入config.util.ts中的函数
const { loadProxyList } = require('~/utils/config.util');
// 加载代理列表
const proxies = loadProxyList(true);
if (!proxies || proxies.length === 0) {
return {
success: false,
message: '没有找到可用的代理IP'
};
}
// 转换为DTO格式
const proxyDtos: CreateProxyIpDto[] = proxies.map(p => ({
host: p.host,
port: p.port,
username: p.username,
password: p.password,
isActive: p.isActive !== false,
source: 'MANUAL',
remark: '从config.util.ts导入'
}));
// 批量创建
return this.batchCreate({ proxies: proxyDtos });
} catch (error) {
this.logger.error('从config.util.ts同步代理失败:', error);
throw new BusinessException(
ErrorEnum.INTERNAL_SERVER_ERROR,
`同步失败: ${error.message}`
);
}
}
/**
* 获取全局代理IP
* @param forceRefresh 是否强制刷新
* @returns 全局代理IP
*/
async getGlobalProxy(forceRefresh: boolean = false): Promise<ProxyIpEntity | null> {
const now = Date.now();
// 如果当前没有全局代理,或者强制刷新,或者超过刷新间隔,则获取新的代理
if (!this.currentGlobalProxy || forceRefresh || (now - this.globalProxyLastRefresh > this.PROXY_REFRESH_INTERVAL)) {
try {
// 获取一个随机可用代理作为全局代理
const proxy = await this.getRandomProxy();
if (proxy) {
this.currentGlobalProxy = proxy;
this.globalProxyLastRefresh = now;
this.logger.log(`已设置全局代理: ${proxy.host}:${proxy.port}`);
} else {
this.logger.warn('无可用代理IP,全局代理设置失败');
this.currentGlobalProxy = null;
}
} catch (error) {
this.logger.error(`获取全局代理失败: ${error.message}`);
this.currentGlobalProxy = null;
}
}
return this.currentGlobalProxy;
}
/**
* 标记当前全局代理为不可用并获取新的代理
*/
async markGlobalProxyAsInvalid(): Promise<ProxyIpEntity | null> {
if (this.currentGlobalProxy) {
this.logger.warn(`标记全局代理 ${this.currentGlobalProxy.host}:${this.currentGlobalProxy.port} 为不可用`);
try {
// 更新代理状态为不可用
await this.proxyIpRepository.update(
{ id: this.currentGlobalProxy.id },
{
isActive: false,
failCount: () => `failCount + 1`
}
);
// 清除缓存
await this.clearProxyCache();
} catch (error) {
this.logger.error(`更新代理状态失败: ${error.message}`);
}
}
// 强制刷新获取新的全局代理
return this.getGlobalProxy(true);
}
/**
* 使用代理发送HTTP请求
*
* @param url 请求URL
* @param data 请求数据
* @param options 请求选项
* @returns Promise<any> 请求响应
*/
async tunnelRequest(
url: string,
data: Record<string, any> = {},
options: {
method?: 'get' | 'post' | 'put' | 'delete';
headers?: Record<string, string>;
timeout?: number;
deviceId?: string;
token?: string;
encryptFields?: string[];
isEncryptBody?: boolean;
isProxy?: boolean;
maxRetries?: number; // 最大重试次数
currentRetry?: number; // 当前重试次数
excludeProxyIds?: number[]; // 排除的代理ID列表
proxyId?: number; // 指定代理ID
} = {}
): Promise<any> {
try {
// 动态导入依赖
const axios = require('axios');
const tunnel = require('tunnel');
// 设置默认值
const method = options.method || 'post';
const timeout = options.timeout || 30000;
const deviceId = options.deviceId;
const token = options.token || '';
const encryptFields = options.encryptFields || ['mobile', 'code', 'smsCode'];
let isProxy = options?.isProxy !== false; // 默认使用代理
const maxRetries = options.maxRetries || 5; // 默认最大重试5次
const currentRetry = options.currentRetry || 0; // 当前重试次数
const excludeProxyIds = options.excludeProxyIds || []; // 已排除的代理
// 获取代理配置
let proxyEntity = null;
if (isProxy) {
// 如果有自定义指定代理ID,则使用指定代理
if (options.proxyId) {
proxyEntity = await this.proxyIpRepository.findOne({
where: { id: options.proxyId},
});
}
else {
// 使用全局代理IP,而不是每次随机选择
proxyEntity = await this.getGlobalProxy();
}
if (!proxyEntity) {
if (currentRetry > 0) {
// 如果所有代理都已尝试过或不可用,但仍在重试,则不使用代理直接请求
if (isDev) {
this.logger.log(`所有代理均已尝试或不可用,尝试直接请求 (重试: ${currentRetry}/${maxRetries})`);
}
isProxy = false;
} else {
sendWechatBotMessageMarkdown('代理IP不足,请添加代理IP 参考码:1001');
throw new Error('请联系客服参考码1001');
}
} else if (isDev) {
this.logger.log(`使用代理: ${proxyEntity.host}:${proxyEntity.port} (重试: ${currentRetry}/${maxRetries})`);
}
}
// 加密请求数据
let encryptedData = data;
let encryptedBody = JSON.stringify(encryptedData);
if (options.isEncryptBody) {
encryptedData = encryptRequestData(data, encryptFields);
encryptedBody = JSON.stringify(encryptedData);
}
// 生成签名信息
const signInfo = generateSamsSignature(encryptedBody, deviceId, "android", "5.0.107", token);
// 创建基础请求头
const baseHeaders = {
'app-version': '5.0.107',
'device-name': 'iPhone 15 Plus',
'device-os-version': '12',
'device-type': 'android',
'language': 'CN',
'latitude': '',
'longitude': '',
'Local-Latitude': '0.0',
'Local-Longitude': '0.0',
'p': '1656120205',
'rcs': '1',
'spv': '2.0',
'sy': '0',
'sny': 'c',
'system-language': 'CN',
'tpg': '1',
'zoneType': '1',
'auth-token': token,
'Content-Type': 'application/json',
'Accept': '*/*',
'User-Agent': 'okhttp/4.8.1',
'Connection': 'keep-alive',
...options.headers
};
// 添加签名信息到请求头
const headers = appendSignatureHeaders(baseHeaders, signInfo);
if (isDev) {
this.logger.log(`请求URL: ${url}`);
this.logger.log(`请求方法: ${method}`);
this.logger.log(`请求数据: ${JSON.stringify(encryptedData)}`);
if (isProxy && proxyEntity) {
this.logger.log(`代理配置: ${proxyEntity.host}:${proxyEntity.port}`);
}
}
// 创建隧道代理
const agent = (isProxy && proxyEntity) ? tunnel.httpsOverHttp({
proxy: {
host: proxyEntity.host,
port: proxyEntity.port,
proxyAuth: proxyEntity.username && proxyEntity.password ?
`${proxyEntity.username}:${proxyEntity.password}` : undefined
}
}) : undefined;
// 发送请求
const startTime = new Date();
let requestConfig = {
method,
url,
headers,
timeout
}
requestConfig["data"] = method !== 'get' ? encryptedData : null;
requestConfig["params"] = method === 'get' ? encryptedData : null;
if(isProxy && agent){
requestConfig["httpsAgent"] = agent;
}
const response = await axios(requestConfig);
const endTime = new Date();
const duration = endTime.getTime() - startTime.getTime();
if (isDev) {
this.logger.log(`请求耗时: ${duration}ms`);
this.logger.log(`响应状态: ${response.status}`);
}
// 如果使用了代理,更新代理使用统计
if (isProxy && proxyEntity) {
await this.proxyIpRepository.update(
{ id: proxyEntity.id },
{
lastUsedAt: new Date(),
successCount: () => `successCount + 1`,
avgResponseTime: () => `(avgResponseTime + ${duration}) / 2`
}
);
}
return response.data;
} catch (error) {
if (isDev) {
this.logger.error(`全局代理请求失败返回信息: ${error}`);
if (error.response) {
this.logger.error(`错误状态码: ${error.response.status}`);
}
}
// 处理代理失败或被阻断的情况
const isProxyError = error.message?.includes('ECONNREFUSED') ||
error.message?.includes('ETIMEDOUT') ||
error.message?.includes('ECONNRESET') ||
error.message?.includes('Timeout') ||
error.message?.includes('tunneling') ||
error.message?.includes('Client network') ||
error.response?.status === 403;
// 如果是代理问题,标记当前全局代理为不可用并获取新的代理
if (isProxyError && this.currentGlobalProxy) {
this.logger.warn(`代理 ${this.currentGlobalProxy.host}:${this.currentGlobalProxy.port} 请求失败,标记为不可用`);
// 更新代理状态为不可用并获取新的全局代理
await this.markGlobalProxyAsInvalid();
}
// 如果是代理问题且未达到最大重试次数,则尝试切换代理重试
if (isProxyError && options.currentRetry < (options.maxRetries || 5)) {
const nextRetry = (options.currentRetry || 0) + 1;
this.logger.log(`代理请求失败,切换代理重试 (${nextRetry}/${options.maxRetries || 5})`);
// 递归调用自身,使用新的代理
return this.tunnelRequest(url, data, {
...options,
currentRetry: nextRetry
});
}
if(error.response && error.response.status === 403){
throw {
message: 'code:403 阻断请求,请稍后再试',
response: { msg: '阻断请求,请稍后再试' }
}
}
// 修改错误处理,确保返回有意义的错误信息
if (error.response && error.response.data) {
throw {
message: error.response.data.msg || error.message,
response: error.response.data
};
} else {
throw {
message: error.message,
response: { msg: '请求失败,请稍后再试' }
};
}
}
}
/**
* 批量检测代理IP可用性(异步任务)
* @param ids 代理IP ID列表
* @returns 任务信息
*/
async batchCheckProxyAvailabilityAsync(ids: number[]) {
// console.log('接收到的IDs:', ids, typeof ids);
// 确保ids是数组且不为空
if (!Array.isArray(ids) || ids.length === 0) {
throw new BusinessException(ErrorEnum.PARAMS_INVALID, 'ID列表不能为空');
}
// 确保所有ID都是数字
const numericIds = ids.map(id => Number(id));
if (numericIds.some(id => isNaN(id))) {
throw new BusinessException(ErrorEnum.PARAMS_INVALID, 'ID列表包含非数字值');
}
// 固定的任务名称
const taskName = `代理IP批量检测`;
// 后台进行批量检测 提醒用户之后进行刷新查看就好了
try {
// 这里进行异步后台检测
let ip_length = ids.length
let that = this
for(let i =0; i < ip_length; i++) {
setTimeout(() => {
that.checkProxyAvailability(ids[i])
},i * 1000)
}
// // 查找是否已存在同名任务
// const existingTasks = await this.taskService.findByName(taskName);
// // 如果存在同名任务,先停止并删除
// if (existingTasks && existingTasks.length > 0) {
// for (const task of existingTasks) {
// try {
// await this.taskService.stop(task.id);
// // 删除任务日志
// await this.taskService.deleteTaskLogs(task.id);
// // 删除任务
// await this.taskService.delete(task.id);
// } catch (error) {
// this.logger.warn(`清理旧任务失败: ${error.message}`);
// }
// }
// }
// // 创建新的一次性任务
// await this.taskService.create({
// name: taskName,
// service: 'ProxyCheckJob.handle', // 对应任务类的名称和方法
// type: TaskType.Interval, // 使用间隔执行类型
// status: TaskStatus.Active, // 激活状态
// data: JSON.stringify({ ids: numericIds }), // 传递ID列表,确保是JSON字符串
// remark: `批量检测${numericIds.length}个代理IP`,
// every: 100, // 100毫秒执行一次
// limit: 1, // 只执行一次
// startTime: new Date(), // 立即开始
// endTime: new Date(Date.now() + 3600000), // 1小时后结束
// cron: '', // 不使用cron表达式
// autoDelete: false, // 标记为自动删除
// });
return {
success: true,
message: `批量检测任务已提交,共 ${numericIds.length} 个代理IP,稍后请刷新查看结果`,
};
} catch (error) {
this.logger.error(`创建批量检测任务失败: ${error.message}`);
throw new BusinessException(ErrorEnum.INTERNAL_SERVER_ERROR, `创建批量检测任务失败: ${error.message}`);
}
}
/**
* 刷新鲸云平台的代理IP并导入到数据库
* @param platform 平台标识
* @returns 刷新结果
*/
async refreshProxyPlatformIp(dto: RefreshProxyPlatformIpDto) {
this.logger.log(`开始刷新平台代理IP: ${dto.platform}`);
// 鲸云代理
if(dto.platform === 'JINGYUN') {
try {
// 1. 登录鲸云平台获取token
this.logger.log(`尝试登录鲸云平台: ${this.JINGYUN_CONFIG.LOGIN_URI}`);
const loginResponse = await axios.post(this.JINGYUN_CONFIG.LOGIN_URI, {
username: this.JINGYUN_CONFIG.LOGIN_USERNAME,
password: this.JINGYUN_CONFIG.LOGIN_PASSWORD
}, {
// 添加CORS相关配置
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
'Origin': 'https://z.51tcp.com'
},
// 添加超时配置
timeout: 30000,
// 忽略HTTPS证书错误
httpsAgent: new (require('https').Agent)({
rejectUnauthorized: false
})
});
this.logger.log(`登录鲸云平台响应状态: ${loginResponse.status}`);
if (!loginResponse.data || !loginResponse.data.user || !loginResponse.data.user.token) {
this.logger.error(`登录鲸云平台失败: ${JSON.stringify(loginResponse.data)}`);
return {
success: false,
message: '登录鲸云平台失败,无法获取token'
};
}
const token = loginResponse.data.user.token;
this.logger.log(`成功登录鲸云平台,获取token: ${token.substring(0, 15)}...`);
// 2. 获取账户列表
this.logger.log(`尝试获取鲸云账户列表: ${this.JINGYUN_CONFIG.LIST_URI}`);
const accountsResponse = await axios.get(this.JINGYUN_CONFIG.LIST_URI, {
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json',
'Accept': 'application/json',
'Origin': 'https://z.51tcp.com'
},
timeout: 30000,
httpsAgent: new (require('https').Agent)({
rejectUnauthorized: false
})
});
this.logger.log(`获取账户列表响应状态: ${accountsResponse.status}`);
if (!accountsResponse.data || !Array.isArray(accountsResponse.data)) {
this.logger.error(`获取鲸云账户列表失败: ${JSON.stringify(accountsResponse.data)}`);
return {
success: false,
message: '获取鲸云账户列表失败'
};
}
const accounts = accountsResponse.data;
this.logger.log(`获取到${accounts.length}个鲸云账户`);
// 3. 遍历账户,获取节点信息并保存到数据库
let totalProxies = 0;
let savedProxies = 0;
let skippedProxies = 0;
for (const account of accounts) {
// 只处理状态正常的账户
if (account.state !== 'normal') {
this.logger.warn(`账户 ${account.username} 状态异常: ${account.state},跳过`);
continue;
}
// 获取账户下的节点信息
const nodeInfoUrl = `${this.JINGYUN_CONFIG.NODE_INFO_URI}${account.id}`;
this.logger.log(`尝试获取账户节点信息: ${nodeInfoUrl}`);
try {
const nodeInfoResponse = await axios.get(nodeInfoUrl, {
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json',
'Accept': 'application/json',
'Origin': 'https://z.51tcp.com'
},
timeout: 30000,
httpsAgent: new (require('https').Agent)({
rejectUnauthorized: false
})
});
this.logger.log(`获取节点信息响应状态: ${nodeInfoResponse.status}`);
if (!nodeInfoResponse.data || !nodeInfoResponse.data.list || !Array.isArray(nodeInfoResponse.data.list)) {
this.logger.warn(`获取账户 ${account.username} 的节点信息失败: ${JSON.stringify(nodeInfoResponse.data)}`);
continue;
}
const nodes = nodeInfoResponse.data.list;
this.logger.log(`账户 ${account.username} 有 ${nodes.length} 个节点`);
// 将节点信息转换为代理IP格式并保存到数据库
const proxies = nodes.map(node => ({
host: node.xip, // 使用xip作为主机地址
port: node.http_port, // 使用http端口
username: account.username,
password: account.password,
isActive: true,
source: 'JINGYUN',
remark: `鲸云代理 - ${account.username} - ${node.group}`,
location: node.group,
expireAt: new Date(node.expire_time),
tags: `isp:${node.isp},nodeId:${node.id}`
}));
totalProxies += proxies.length;
// 批量保存到数据库
for (const proxy of proxies) {
try {
// 检查是否已存在相同的代理IP
const existingProxy = await this.proxyIpRepository.findOne({
where: {
host: proxy.host,
port: proxy.port,
isDeleted: 0
}
});
if (existingProxy) {
// 更新现有代理的部分信息
await this.proxyIpRepository.update(existingProxy.id, {
username: proxy.username,
password: proxy.password,
expireAt: proxy.expireAt,
tags: proxy.tags
});
skippedProxies++;
} else {
// 创建新代理
await this.proxyIpRepository.save(proxy);
savedProxies++;
}
} catch (error) {
this.logger.error(`保存代理IP ${proxy.host}:${proxy.port} 失败: ${error.message}`);
}
}
} catch (nodeError) {
this.logger.error(`获取账户 ${account.username} 节点信息失败: ${nodeError.message}`);
if (nodeError.response) {
this.logger.error(`状态码: ${nodeError.response.status}, 数据: ${JSON.stringify(nodeError.response.data)}`);
}
}
}
// 清除缓存
await this.clearProxyCache();
return {
success: true,
message: `成功从鲸云平台获取 ${totalProxies} 个代理IP,新增 ${savedProxies} 个,更新 ${skippedProxies} 个`
};
} catch (error) {
this.logger.error(`刷新鲸云代理失败: ${error.message}`);
if (error.response) {
this.logger.error(`状态码: ${error.response.status}, 数据: ${JSON.stringify(error.response.data)}`);
}
return {
success: false,
message: `刷新鲸云代理失败: ${error.message}`
};
}
} else if (dto.platform === 'QINGGUO') {
// 青果平台的实现
return {
success: false,
message: `暂未实现青果平台的代理刷新功能`
};
}
// 如果不是已知平台,返回未实现
return {
success: false,
message: `未知平台 ${dto.platform},无法刷新代理`
};
}
}
文章标题:鲸云代理快捷导入,以及Nest使用代理发送请求
文章作者:Cling.
文章链接:[复制]
最后修改时间:2025年 08月 08日 09时11分
商业转载请联系站长获得授权,非商业转载请注明本文出处及文章链接,您可以自由地在任何媒体以任何形式复制和分发作品,也可以修改和创作,但是分发衍生作品时必须采用相同的许可协议。 本文采用CC BY-NC-SA 4.0进行许可。
Copyright © 2023-2025
豫ICP备2022014268号-1
「每想拥抱你一次,天空飘落一片雪,至此雪花拥抱撒哈拉!」
本站已经艰难运行了661天