417 lines
12 KiB
JavaScript
417 lines
12 KiB
JavaScript
/**
|
||
* Card Generator - 核心生成逻辑
|
||
*/
|
||
|
||
const { randomInt, randomDigits, padZero, generateLuhnNumber } = require('../../utils');
|
||
const { CARD_TYPES, EXPIRY_CONFIG } = require('./config');
|
||
const { ValidationError } = require('../../errors');
|
||
|
||
class CardGenerator {
|
||
constructor(database = null) {
|
||
this.cardTypes = CARD_TYPES;
|
||
this.expiryConfig = EXPIRY_CONFIG;
|
||
this.usedNumbers = new Set(); // 去重机制:记录已生成的卡号
|
||
this.database = database; // 数据库连接(可选)
|
||
this.lastBinInfo = null; // 最后使用的BIN信息
|
||
}
|
||
|
||
/**
|
||
* 生成卡号(带去重机制 + 数据库查重)
|
||
* @param {string} type - 卡类型
|
||
* @returns {Promise<string>}
|
||
*/
|
||
async generateCardNumber(type) {
|
||
const config = this.cardTypes[type];
|
||
if (!config) {
|
||
throw new ValidationError('card-generator', `Unknown card type: ${type}`);
|
||
}
|
||
|
||
// 尝试生成唯一卡号(最多100次)
|
||
let attempts = 0;
|
||
const maxAttempts = 100;
|
||
|
||
while (attempts < maxAttempts) {
|
||
const cardNumber = this._generateCardNumberInternal(type, config);
|
||
|
||
// 检查1:内存去重(本次运行)
|
||
if (this.usedNumbers.has(cardNumber)) {
|
||
attempts++;
|
||
continue;
|
||
}
|
||
|
||
// 检查2:数据库去重(历史记录)
|
||
if (this.database) {
|
||
const existsInDb = await this.checkCardNumberInDatabase(cardNumber);
|
||
if (existsInDb) {
|
||
attempts++;
|
||
continue;
|
||
}
|
||
}
|
||
|
||
// 通过所有检查,记录并返回
|
||
this.usedNumbers.add(cardNumber);
|
||
return cardNumber;
|
||
}
|
||
|
||
throw new ValidationError('card-generator', `无法生成唯一卡号(${maxAttempts}次尝试后仍重复)`);
|
||
}
|
||
|
||
/**
|
||
* 检查卡号是否在数据库中已存在
|
||
* @param {string} cardNumber - 卡号
|
||
* @returns {Promise<boolean>}
|
||
*/
|
||
async checkCardNumberInDatabase(cardNumber) {
|
||
try {
|
||
// 检查数据库连接是否已初始化
|
||
if (!this.database || !this.database.pool) {
|
||
return false; // 数据库未初始化,跳过数据库查询
|
||
}
|
||
|
||
const sql = 'SELECT COUNT(*) as count FROM windsurf_accounts WHERE payment_card_number = ?';
|
||
const rows = await this.database.query(sql, [cardNumber]);
|
||
return rows[0].count > 0;
|
||
} catch (error) {
|
||
// 如果数据库查询失败,静默降级(不输出,避免乱码)
|
||
return false;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 内部生成逻辑(不含去重)
|
||
* @param {string} type - 卡类型
|
||
* @param {Object} config - 卡类型配置
|
||
* @returns {string}
|
||
*/
|
||
_generateCardNumberInternal(type, config) {
|
||
const { prefix, prefixes, length, useLuhn, successfulPatterns, generation } = config;
|
||
|
||
const prefixInfo = this.selectPrefix(prefix, prefixes);
|
||
this.lastBinInfo = prefixInfo; // 保存BIN信息
|
||
const selectedPrefix = prefixInfo.fullPrefix;
|
||
|
||
if (!selectedPrefix) {
|
||
throw new ValidationError('card-generator', `卡类型 ${type} 未配置有效的前缀`);
|
||
}
|
||
|
||
if (selectedPrefix.length >= length) {
|
||
throw new ValidationError('card-generator', `前缀长度(${selectedPrefix.length}) 不得大于或等于卡号总长度(${length})`);
|
||
}
|
||
|
||
const digitsNeeded = length - selectedPrefix.length - 1;
|
||
if (digitsNeeded <= 0) {
|
||
throw new ValidationError('card-generator', `前缀配置错误:无法为 ${type} 生成剩余位数`);
|
||
}
|
||
|
||
const patternLength = successfulPatterns && successfulPatterns.length > 0
|
||
? successfulPatterns[0].length
|
||
: null;
|
||
const canUseAdvancedStrategies = Boolean(
|
||
generation &&
|
||
successfulPatterns &&
|
||
successfulPatterns.length > 0 &&
|
||
prefixInfo.supportsPatterns &&
|
||
patternLength === digitsNeeded + 1
|
||
);
|
||
|
||
if (canUseAdvancedStrategies) {
|
||
const rand = Math.random();
|
||
|
||
if (rand < 0.2) {
|
||
return this.generateByWeights(selectedPrefix, digitsNeeded, length);
|
||
}
|
||
if (rand < 0.8) {
|
||
return this.generateByMutation(selectedPrefix, successfulPatterns, generation.mutationDigits, digitsNeeded, length);
|
||
}
|
||
// 其余概率走纯随机策略
|
||
}
|
||
|
||
if (useLuhn) {
|
||
return generateLuhnNumber(selectedPrefix, length);
|
||
}
|
||
|
||
const remainingLength = length - selectedPrefix.length;
|
||
return selectedPrefix + randomDigits(remainingLength);
|
||
}
|
||
|
||
selectPrefix(defaultPrefix, prefixes) {
|
||
const options = [];
|
||
|
||
if (Array.isArray(prefixes) && prefixes.length > 0) {
|
||
prefixes.forEach((item) => {
|
||
if (!item) return;
|
||
|
||
if (typeof item === 'string') {
|
||
options.push({
|
||
fullPrefix: item,
|
||
weight: 1,
|
||
supportsPatterns: true
|
||
});
|
||
return;
|
||
}
|
||
|
||
if (typeof item === 'object') {
|
||
const bin = typeof item.bin === 'string' ? item.bin : '';
|
||
const extension = typeof item.extension === 'string' ? item.extension : '';
|
||
const fullPrefix = typeof item.fullPrefix === 'string' ? item.fullPrefix : `${bin}${extension}`;
|
||
|
||
if (!fullPrefix) {
|
||
return;
|
||
}
|
||
|
||
const weight = Number.isFinite(item.weight) && item.weight > 0 ? item.weight : 1;
|
||
const supportsPatterns = item.allowPatterns !== false;
|
||
const issuer = item.issuer || '未知';
|
||
const country = item.country || 'CN';
|
||
|
||
options.push({ fullPrefix, weight, supportsPatterns, issuer, country });
|
||
}
|
||
});
|
||
}
|
||
|
||
if (options.length === 0 && defaultPrefix) {
|
||
return { fullPrefix: defaultPrefix, supportsPatterns: true };
|
||
}
|
||
|
||
if (options.length === 0) {
|
||
return { fullPrefix: null, supportsPatterns: false };
|
||
}
|
||
|
||
const totalWeight = options.reduce((sum, item) => sum + item.weight, 0);
|
||
let randomValue = Math.random() * totalWeight;
|
||
|
||
for (const option of options) {
|
||
randomValue -= option.weight;
|
||
if (randomValue <= 0) {
|
||
return option;
|
||
}
|
||
}
|
||
|
||
return options[options.length - 1];
|
||
}
|
||
|
||
/**
|
||
* 基于真实案例变异生成卡号
|
||
* @param {string} prefix - BIN前缀
|
||
* @param {Array<string>} patterns - 成功案例的后缀(含校验位)
|
||
* @param {Array<number>} mutationDigits - 变异数字个数[min, max]
|
||
* @param {number} digitsNeeded - 需要生成的主体位数(不含校验位)
|
||
* @param {number} totalLength - 卡号总长度
|
||
* @returns {string}
|
||
*/
|
||
generateByMutation(prefix, patterns, mutationDigits, digitsNeeded, totalLength) {
|
||
if (!Array.isArray(patterns) || patterns.length === 0) {
|
||
return generateLuhnNumber(prefix, totalLength);
|
||
}
|
||
|
||
const basePattern = patterns[randomInt(0, patterns.length - 1)];
|
||
if (typeof basePattern !== 'string' || basePattern.length !== digitsNeeded + 1) {
|
||
return generateLuhnNumber(prefix, totalLength);
|
||
}
|
||
|
||
const bodyDigits = basePattern.slice(0, digitsNeeded).split('');
|
||
|
||
let minChanges = 1;
|
||
let maxChanges = 1;
|
||
if (Array.isArray(mutationDigits) && mutationDigits.length > 0) {
|
||
const [minRaw, maxRaw] = mutationDigits;
|
||
if (Number.isFinite(minRaw)) {
|
||
minChanges = Math.max(0, minRaw);
|
||
}
|
||
if (Number.isFinite(maxRaw)) {
|
||
maxChanges = Math.max(minChanges, maxRaw);
|
||
} else {
|
||
maxChanges = Math.max(minChanges, minChanges);
|
||
}
|
||
}
|
||
|
||
maxChanges = Math.min(maxChanges, digitsNeeded);
|
||
minChanges = Math.min(minChanges, maxChanges);
|
||
const changeCount = maxChanges > 0 ? randomInt(minChanges, maxChanges) : 0;
|
||
|
||
const changedPositions = new Set();
|
||
for (let i = 0; i < changeCount; i++) {
|
||
let pos;
|
||
do {
|
||
pos = randomInt(0, bodyDigits.length - 1);
|
||
} while (changedPositions.has(pos));
|
||
|
||
changedPositions.add(pos);
|
||
|
||
let newDigit;
|
||
do {
|
||
newDigit = randomInt(0, 9).toString();
|
||
} while (newDigit === bodyDigits[pos]);
|
||
|
||
bodyDigits[pos] = newDigit;
|
||
}
|
||
|
||
const partial = prefix + bodyDigits.join('');
|
||
const checkDigit = this.calculateLuhnCheckDigit(partial);
|
||
|
||
return partial + checkDigit;
|
||
}
|
||
|
||
/**
|
||
* 基于统计权重生成卡号(60个成功案例的分布)
|
||
* @param {string} prefix - BIN前缀
|
||
* @param {number} digitsNeeded - 需要生成的主体位数(不含校验位)
|
||
* @param {number} totalLength - 卡号总长度
|
||
* @returns {string}
|
||
*/
|
||
generateByWeights(prefix, digitsNeeded, totalLength) {
|
||
if (digitsNeeded <= 0) {
|
||
return generateLuhnNumber(prefix, totalLength);
|
||
}
|
||
|
||
// 基于60个成功案例的位置权重(频率分布)
|
||
const positionWeights = [
|
||
[7, 5, 8, 2, 5, 7, 6, 7, 6, 7], // 位置1
|
||
[7, 5, 7, 4, 11, 4, 2, 8, 8, 4], // 位置2
|
||
[5, 4, 4, 9, 6, 7, 7, 6, 5, 7], // 位置3
|
||
[12, 5, 8, 7, 7, 5, 4, 6, 5, 1], // 位置4: 数字0占20%
|
||
[9, 6, 7, 3, 5, 6, 3, 9, 7, 5], // 位置5
|
||
[10, 4, 5, 9, 7, 8, 4, 5, 6, 2], // 位置6
|
||
[7, 3, 7, 2, 9, 6, 4, 6, 9, 7], // 位置7
|
||
];
|
||
|
||
if (digitsNeeded > positionWeights.length) {
|
||
return generateLuhnNumber(prefix, totalLength);
|
||
}
|
||
|
||
let pattern = '';
|
||
for (let pos = 0; pos < digitsNeeded; pos++) {
|
||
const weights = positionWeights[pos];
|
||
const digit = this.weightedRandomDigit(weights);
|
||
pattern += digit;
|
||
}
|
||
|
||
const partial = prefix + pattern;
|
||
const checkDigit = this.calculateLuhnCheckDigit(partial);
|
||
|
||
return partial + checkDigit;
|
||
}
|
||
|
||
/**
|
||
* 按权重随机选择数字
|
||
* @param {Array} weights - 权重数组(10个元素,对应数字0-9)
|
||
* @returns {string}
|
||
*/
|
||
weightedRandomDigit(weights) {
|
||
const total = weights.reduce((sum, w) => sum + w, 0);
|
||
let random = Math.random() * total;
|
||
|
||
for (let i = 0; i < weights.length; i++) {
|
||
random -= weights[i];
|
||
if (random <= 0) return i.toString();
|
||
}
|
||
|
||
return randomInt(0, 9).toString();
|
||
}
|
||
|
||
/**
|
||
* 计算Luhn校验位
|
||
* @param {string} partial - 不含校验位的卡号
|
||
* @returns {string}
|
||
*/
|
||
calculateLuhnCheckDigit(partial) {
|
||
let sum = 0;
|
||
let isEven = true;
|
||
|
||
for (let i = partial.length - 1; i >= 0; i--) {
|
||
let digit = parseInt(partial[i]);
|
||
|
||
if (isEven) {
|
||
digit *= 2;
|
||
if (digit > 9) digit -= 9;
|
||
}
|
||
|
||
sum += digit;
|
||
isEven = !isEven;
|
||
}
|
||
|
||
return ((10 - (sum % 10)) % 10).toString();
|
||
}
|
||
|
||
/**
|
||
* 生成有效期
|
||
* @returns {{month: string, year: string}}
|
||
*/
|
||
generateExpiry() {
|
||
const month = randomInt(this.expiryConfig.minMonth, this.expiryConfig.maxMonth);
|
||
const year = randomInt(this.expiryConfig.minYear, this.expiryConfig.maxYear);
|
||
|
||
return {
|
||
month: padZero(month, 2),
|
||
year: padZero(year, 2)
|
||
};
|
||
}
|
||
|
||
/**
|
||
* 生成CVV安全码
|
||
* @param {string} type - 卡类型
|
||
* @returns {string}
|
||
*/
|
||
generateCVV(type) {
|
||
const config = this.cardTypes[type];
|
||
const cvvLength = config.cvvLength;
|
||
const maxValue = Math.pow(10, cvvLength) - 1;
|
||
const cvv = randomInt(0, maxValue);
|
||
return padZero(cvv, cvvLength);
|
||
}
|
||
|
||
/**
|
||
* 生成完整的信用卡信息
|
||
* @param {string} type - 卡类型,默认'unionpay'
|
||
* @returns {Promise<Object>}
|
||
*/
|
||
async generate(type = 'unionpay') {
|
||
const number = await this.generateCardNumber(type);
|
||
const expiry = this.generateExpiry();
|
||
const cvv = this.generateCVV(type);
|
||
|
||
// 获取银行和国家信息
|
||
const issuer = this.lastBinInfo?.issuer || '未知';
|
||
const country = this.lastBinInfo?.country || 'CN';
|
||
const countryName = country === 'MO' ? '澳门' : '中国';
|
||
|
||
return {
|
||
number,
|
||
month: expiry.month,
|
||
year: expiry.year,
|
||
cvv,
|
||
type: this.cardTypes[type].name,
|
||
issuer, // 发卡银行
|
||
country, // 国家代码 (CN/MO)
|
||
countryName // 国家名称
|
||
};
|
||
}
|
||
|
||
/**
|
||
* 批量生成
|
||
* @param {number} count - 数量
|
||
* @param {string} type - 卡类型
|
||
* @returns {Promise<Array>}
|
||
*/
|
||
async generateBatch(count, type = 'unionpay') {
|
||
const cards = [];
|
||
for (let i = 0; i < count; i++) {
|
||
cards.push(await this.generate(type));
|
||
}
|
||
return cards;
|
||
}
|
||
|
||
/**
|
||
* 获取所有支持的卡类型
|
||
* @returns {Array}
|
||
*/
|
||
getSupportedTypes() {
|
||
return Object.keys(this.cardTypes).map(key => ({
|
||
id: key,
|
||
name: this.cardTypes[key].name
|
||
}));
|
||
}
|
||
}
|
||
|
||
module.exports = CardGenerator;
|