This commit is contained in:
dengqichen 2025-11-20 21:41:52 +08:00
parent 8855683f2f
commit 1826b81195
2 changed files with 182 additions and 73 deletions

View File

@ -6,21 +6,36 @@
const CARD_TYPES = {
unionpay: {
name: '中国银联 (UnionPay)',
// 多个 9 位银联卡 BIN 前缀(基于真实成功案例和同系列扩展)
// 策略:优先使用已验证成功的 622836754 和其同系列变体
// 银联前缀配置:支持基础 BIN + 扩展段 + 权重
// 策略:95% 使用真实成功案例系列5% 探测其他银行(收集数据)
prefixes: [
'622836754', // ✅ 原始成功案例(最高优先级)
'622836755', '622836756', '622836757', // 同系列变体
'622836758', '622836759', '622836750', // 继续同系列
'622836751', '622836752', '622836753', // 更多同系列
// ========== 主力军622836 系列95% 权重,有真实成功案例支撑)==========
{
bin: '622836',
extension: '754',
weight: 60, // 最高优先级:真实成功案例
allowPatterns: true // 可套用成功案例的后缀模式
},
// 同系列相邻卡段(保留较高权重,与真实案例同源)
{ bin: '622836', extension: '755', weight: 10, allowPatterns: true },
{ bin: '622836', extension: '756', weight: 8, allowPatterns: true },
{ bin: '622836', extension: '757', weight: 8, allowPatterns: true },
{ bin: '622836', extension: '758', weight: 5, allowPatterns: true },
{ bin: '622836', extension: '759', weight: 4, allowPatterns: true },
// ========== 探测部队其他银行BIN5% 权重,纯随机生成,数据收集用)==========
{ bin: '620010', weight: 0.3, allowPatterns: false, issuer: '深圳发展银行' },
{ bin: '620021', weight: 0.3, allowPatterns: false, issuer: '交通银行' },
{ bin: '620048', weight: 0.2, allowPatterns: false, issuer: '中银快付' },
{ bin: '620059', weight: 0.2, allowPatterns: false, issuer: '中国工商银行' },
{ bin: '620086', weight: 0.3, allowPatterns: false, issuer: '工商银行' },
{ bin: '620107', weight: 0.3, allowPatterns: false, issuer: '中国建设银行' },
{ bin: '620148', weight: 0.2, allowPatterns: false, issuer: '中国农业银行' },
{ bin: '620200', weight: 0.4, allowPatterns: false, issuer: '中国银行' },
{ bin: '621214', weight: 0.3, allowPatterns: false, issuer: '招商银行' },
// 备注allowPatterns: false 表示不使用成功案例模式,只用纯随机策略
// 如果这些BIN有成功案例再更新配置并提高权重
],
// 备用:如果同系列都失败,尝试其他真实 BIN
// 暂时注释掉未验证的 BIN减少失败率
// prefixes_backup: [
// '622206000', '622210000', // 工商银行
// '622620000', // 民生银行
// '622588000', // 招商银行
// ],
length: 16,
cvvLength: 3,
useLuhn: true,

View File

@ -85,87 +85,181 @@ class CardGenerator {
_generateCardNumberInternal(type, config) {
const { prefix, prefixes, length, useLuhn, successfulPatterns, generation } = config;
// 支持单个 prefix 或多个 prefixes
let selectedPrefix = prefix;
if (prefixes && Array.isArray(prefixes) && prefixes.length > 0) {
// 从 prefixes 数组中随机选择一个
selectedPrefix = prefixes[randomInt(0, prefixes.length - 1)];
const prefixInfo = this.selectPrefix(prefix, prefixes);
const selectedPrefix = prefixInfo.fullPrefix;
if (!selectedPrefix) {
throw new ValidationError('card-generator', `卡类型 ${type} 未配置有效的前缀`);
}
// 如果有成功案例和生成策略配置,使用三策略混合(优化版)
if (successfulPatterns && generation) {
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();
// 策略120%):加权生成(基于统计分布)
if (rand < 0.2) {
return this.generateByWeights(selectedPrefix, length);
return this.generateByWeights(selectedPrefix, digitsNeeded, length);
}
// 策略260%变异真实案例提高到60%,因为这是最可靠的)
else if (rand < 0.8) {
return this.generateByMutation(selectedPrefix, successfulPatterns, generation.mutationDigits);
if (rand < 0.8) {
return this.generateByMutation(selectedPrefix, successfulPatterns, generation.mutationDigits, digitsNeeded, length);
}
// 策略320%完全随机降低到20%
// 其余概率走纯随机策略
}
// 策略3纯随机生成默认或回退
if (useLuhn) {
return generateLuhnNumber(selectedPrefix, length);
} else {
const remainingLength = length - selectedPrefix.length;
return selectedPrefix + randomDigits(remainingLength);
}
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;
options.push({ fullPrefix, weight, supportsPatterns });
}
});
}
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} patterns - 成功案例的后7位
* @param {Array} mutationDigits - 变异数字个数[min, max]
* @param {Array<string>} patterns - 成功案例的后缀含校验位
* @param {Array<number>} mutationDigits - 变异数字个数[min, max]
* @param {number} digitsNeeded - 需要生成的主体位数不含校验位
* @param {number} totalLength - 卡号总长度
* @returns {string}
*/
generateByMutation(prefix, patterns, mutationDigits) {
// 随机选择一个成功案例
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 changeCount = randomInt(mutationDigits[0], mutationDigits[1]);
const mutated = basePattern.split('');
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, mutated.length - 1);
} while (changedPositions.has(pos)); // 避免重复位置
pos = randomInt(0, bodyDigits.length - 1);
} while (changedPositions.has(pos));
changedPositions.add(pos);
// 生成不同的数字
let newDigit;
do {
newDigit = randomInt(0, 9).toString();
} while (newDigit === mutated[pos]);
} while (newDigit === bodyDigits[pos]);
mutated[pos] = newDigit;
bodyDigits[pos] = newDigit;
}
// 组合前缀和变异后的后7位去掉最后一位校验位
const prefix15 = prefix + mutated.join('').slice(0, -1);
const partial = prefix + bodyDigits.join('');
const checkDigit = this.calculateLuhnCheckDigit(partial);
// 重新计算Luhn校验位
const checkDigit = this.calculateLuhnCheckDigit(prefix15);
return prefix15 + checkDigit;
return partial + checkDigit;
}
/**
* 基于统计权重生成卡号60个成功案例的分布
* @param {string} prefix - BIN前缀
* @param {number} length - 总长度
* @param {number} digitsNeeded - 需要生成的主体位数不含校验位
* @param {number} totalLength - 卡号总长度
* @returns {string}
*/
generateByWeights(prefix, length) {
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
@ -177,21 +271,21 @@ class CardGenerator {
[7, 3, 7, 2, 9, 6, 4, 6, 9, 7], // 位置7
];
// 生成6位前15位去掉最后一位校验位
if (digitsNeeded > positionWeights.length) {
return generateLuhnNumber(prefix, totalLength);
}
let pattern = '';
for (let pos = 0; pos < 6; pos++) {
for (let pos = 0; pos < digitsNeeded; pos++) {
const weights = positionWeights[pos];
const digit = this.weightedRandomDigit(weights);
pattern += digit;
}
// 组合前缀
const prefix15 = prefix + pattern;
const partial = prefix + pattern;
const checkDigit = this.calculateLuhnCheckDigit(partial);
// 计算Luhn校验位
const checkDigit = this.calculateLuhnCheckDigit(prefix15);
return prefix15 + checkDigit;
return partial + checkDigit;
}
/**