304 lines
8.5 KiB
JavaScript
304 lines
8.5 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; // 数据库连接(可选)
|
||
}
|
||
|
||
/**
|
||
* 生成卡号(带去重机制 + 数据库查重)
|
||
* @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, length, useLuhn, successfulPatterns, generation } = config;
|
||
|
||
// 如果有成功案例和生成策略配置,使用三策略混合
|
||
if (successfulPatterns && generation) {
|
||
const rand = Math.random();
|
||
|
||
// 策略1(30%):加权生成(基于统计分布)
|
||
if (rand < 0.3) {
|
||
return this.generateByWeights(prefix, length);
|
||
}
|
||
// 策略2(40%):变异真实案例
|
||
else if (rand < 0.7) {
|
||
return this.generateByMutation(prefix, successfulPatterns, generation.mutationDigits);
|
||
}
|
||
// 策略3(30%):完全随机
|
||
}
|
||
|
||
// 策略3:纯随机生成(默认或回退)
|
||
if (useLuhn) {
|
||
return generateLuhnNumber(prefix, length);
|
||
} else {
|
||
const remainingLength = length - prefix.length;
|
||
return prefix + randomDigits(remainingLength);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 基于真实案例变异生成卡号
|
||
* @param {string} prefix - BIN前缀
|
||
* @param {Array} patterns - 成功案例的后7位
|
||
* @param {Array} mutationDigits - 变异数字个数[min, max]
|
||
* @returns {string}
|
||
*/
|
||
generateByMutation(prefix, patterns, mutationDigits) {
|
||
// 随机选择一个成功案例
|
||
const basePattern = patterns[randomInt(0, patterns.length - 1)];
|
||
|
||
// 随机决定改变几个数字
|
||
const changeCount = randomInt(mutationDigits[0], mutationDigits[1]);
|
||
const mutated = basePattern.split('');
|
||
|
||
// 改变指定数量的数字
|
||
const changedPositions = new Set();
|
||
for (let i = 0; i < changeCount; i++) {
|
||
let pos;
|
||
do {
|
||
pos = randomInt(0, mutated.length - 1);
|
||
} while (changedPositions.has(pos)); // 避免重复位置
|
||
|
||
changedPositions.add(pos);
|
||
|
||
// 生成不同的数字
|
||
let newDigit;
|
||
do {
|
||
newDigit = randomInt(0, 9).toString();
|
||
} while (newDigit === mutated[pos]);
|
||
|
||
mutated[pos] = newDigit;
|
||
}
|
||
|
||
// 组合前缀和变异后的后7位(去掉最后一位校验位)
|
||
const prefix15 = prefix + mutated.join('').slice(0, -1);
|
||
|
||
// 重新计算Luhn校验位
|
||
const checkDigit = this.calculateLuhnCheckDigit(prefix15);
|
||
|
||
return prefix15 + checkDigit;
|
||
}
|
||
|
||
/**
|
||
* 基于统计权重生成卡号(60个成功案例的分布)
|
||
* @param {string} prefix - BIN前缀
|
||
* @param {number} length - 总长度
|
||
* @returns {string}
|
||
*/
|
||
generateByWeights(prefix, length) {
|
||
// 基于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
|
||
];
|
||
|
||
// 生成6位(前15位去掉最后一位校验位)
|
||
let pattern = '';
|
||
for (let pos = 0; pos < 6; pos++) {
|
||
const weights = positionWeights[pos];
|
||
const digit = this.weightedRandomDigit(weights);
|
||
pattern += digit;
|
||
}
|
||
|
||
// 组合前缀
|
||
const prefix15 = prefix + pattern;
|
||
|
||
// 计算Luhn校验位
|
||
const checkDigit = this.calculateLuhnCheckDigit(prefix15);
|
||
|
||
return prefix15 + 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);
|
||
|
||
return {
|
||
number,
|
||
month: expiry.month,
|
||
year: expiry.year,
|
||
cvv,
|
||
type: this.cardTypes[type].name
|
||
};
|
||
}
|
||
|
||
/**
|
||
* 批量生成
|
||
* @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;
|