dasdasd
This commit is contained in:
parent
8855683f2f
commit
1826b81195
@ -6,21 +6,36 @@
|
|||||||
const CARD_TYPES = {
|
const CARD_TYPES = {
|
||||||
unionpay: {
|
unionpay: {
|
||||||
name: '中国银联 (UnionPay)',
|
name: '中国银联 (UnionPay)',
|
||||||
// 多个 9 位银联卡 BIN 前缀(基于真实成功案例和同系列扩展)
|
// 银联前缀配置:支持基础 BIN + 扩展段 + 权重
|
||||||
// 策略:优先使用已验证成功的 622836754 和其同系列变体
|
// 策略:95% 使用真实成功案例系列,5% 探测其他银行(收集数据)
|
||||||
prefixes: [
|
prefixes: [
|
||||||
'622836754', // ✅ 原始成功案例(最高优先级)
|
// ========== 主力军:622836 系列(95% 权重,有真实成功案例支撑)==========
|
||||||
'622836755', '622836756', '622836757', // 同系列变体
|
{
|
||||||
'622836758', '622836759', '622836750', // 继续同系列
|
bin: '622836',
|
||||||
'622836751', '622836752', '622836753', // 更多同系列
|
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 },
|
||||||
|
|
||||||
|
// ========== 探测部队:其他银行BIN(5% 权重,纯随机生成,数据收集用)==========
|
||||||
|
{ 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,
|
length: 16,
|
||||||
cvvLength: 3,
|
cvvLength: 3,
|
||||||
useLuhn: true,
|
useLuhn: true,
|
||||||
|
|||||||
@ -85,87 +85,181 @@ class CardGenerator {
|
|||||||
_generateCardNumberInternal(type, config) {
|
_generateCardNumberInternal(type, config) {
|
||||||
const { prefix, prefixes, length, useLuhn, successfulPatterns, generation } = config;
|
const { prefix, prefixes, length, useLuhn, successfulPatterns, generation } = config;
|
||||||
|
|
||||||
// 支持单个 prefix 或多个 prefixes
|
const prefixInfo = this.selectPrefix(prefix, prefixes);
|
||||||
let selectedPrefix = prefix;
|
const selectedPrefix = prefixInfo.fullPrefix;
|
||||||
if (prefixes && Array.isArray(prefixes) && prefixes.length > 0) {
|
|
||||||
// 从 prefixes 数组中随机选择一个
|
if (!selectedPrefix) {
|
||||||
selectedPrefix = prefixes[randomInt(0, prefixes.length - 1)];
|
throw new ValidationError('card-generator', `卡类型 ${type} 未配置有效的前缀`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 如果有成功案例和生成策略配置,使用三策略混合(优化版)
|
if (selectedPrefix.length >= length) {
|
||||||
if (successfulPatterns && generation) {
|
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();
|
const rand = Math.random();
|
||||||
|
|
||||||
// 策略1(20%):加权生成(基于统计分布)
|
|
||||||
if (rand < 0.2) {
|
if (rand < 0.2) {
|
||||||
return this.generateByWeights(selectedPrefix, length);
|
return this.generateByWeights(selectedPrefix, digitsNeeded, length);
|
||||||
}
|
}
|
||||||
// 策略2(60%):变异真实案例(提高到60%,因为这是最可靠的)
|
if (rand < 0.8) {
|
||||||
else if (rand < 0.8) {
|
return this.generateByMutation(selectedPrefix, successfulPatterns, generation.mutationDigits, digitsNeeded, length);
|
||||||
return this.generateByMutation(selectedPrefix, successfulPatterns, generation.mutationDigits);
|
|
||||||
}
|
}
|
||||||
// 策略3(20%):完全随机(降低到20%)
|
// 其余概率走纯随机策略
|
||||||
}
|
}
|
||||||
|
|
||||||
// 策略3:纯随机生成(默认或回退)
|
|
||||||
if (useLuhn) {
|
if (useLuhn) {
|
||||||
return generateLuhnNumber(selectedPrefix, length);
|
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 {string} prefix - BIN前缀
|
||||||
* @param {Array} patterns - 成功案例的后7位
|
* @param {Array<string>} patterns - 成功案例的后缀(含校验位)
|
||||||
* @param {Array} mutationDigits - 变异数字个数[min, max]
|
* @param {Array<number>} mutationDigits - 变异数字个数[min, max]
|
||||||
|
* @param {number} digitsNeeded - 需要生成的主体位数(不含校验位)
|
||||||
|
* @param {number} totalLength - 卡号总长度
|
||||||
* @returns {string}
|
* @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)];
|
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('');
|
||||||
const changeCount = randomInt(mutationDigits[0], mutationDigits[1]);
|
|
||||||
const mutated = basePattern.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();
|
const changedPositions = new Set();
|
||||||
for (let i = 0; i < changeCount; i++) {
|
for (let i = 0; i < changeCount; i++) {
|
||||||
let pos;
|
let pos;
|
||||||
do {
|
do {
|
||||||
pos = randomInt(0, mutated.length - 1);
|
pos = randomInt(0, bodyDigits.length - 1);
|
||||||
} while (changedPositions.has(pos)); // 避免重复位置
|
} while (changedPositions.has(pos));
|
||||||
|
|
||||||
changedPositions.add(pos);
|
changedPositions.add(pos);
|
||||||
|
|
||||||
// 生成不同的数字
|
|
||||||
let newDigit;
|
let newDigit;
|
||||||
do {
|
do {
|
||||||
newDigit = randomInt(0, 9).toString();
|
newDigit = randomInt(0, 9).toString();
|
||||||
} while (newDigit === mutated[pos]);
|
} while (newDigit === bodyDigits[pos]);
|
||||||
|
|
||||||
mutated[pos] = newDigit;
|
bodyDigits[pos] = newDigit;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 组合前缀和变异后的后7位(去掉最后一位校验位)
|
const partial = prefix + bodyDigits.join('');
|
||||||
const prefix15 = prefix + mutated.join('').slice(0, -1);
|
const checkDigit = this.calculateLuhnCheckDigit(partial);
|
||||||
|
|
||||||
// 重新计算Luhn校验位
|
return partial + checkDigit;
|
||||||
const checkDigit = this.calculateLuhnCheckDigit(prefix15);
|
|
||||||
|
|
||||||
return prefix15 + checkDigit;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 基于统计权重生成卡号(60个成功案例的分布)
|
* 基于统计权重生成卡号(60个成功案例的分布)
|
||||||
* @param {string} prefix - BIN前缀
|
* @param {string} prefix - BIN前缀
|
||||||
* @param {number} length - 总长度
|
* @param {number} digitsNeeded - 需要生成的主体位数(不含校验位)
|
||||||
|
* @param {number} totalLength - 卡号总长度
|
||||||
* @returns {string}
|
* @returns {string}
|
||||||
*/
|
*/
|
||||||
generateByWeights(prefix, length) {
|
generateByWeights(prefix, digitsNeeded, totalLength) {
|
||||||
|
if (digitsNeeded <= 0) {
|
||||||
|
return generateLuhnNumber(prefix, totalLength);
|
||||||
|
}
|
||||||
|
|
||||||
// 基于60个成功案例的位置权重(频率分布)
|
// 基于60个成功案例的位置权重(频率分布)
|
||||||
const positionWeights = [
|
const positionWeights = [
|
||||||
[7, 5, 8, 2, 5, 7, 6, 7, 6, 7], // 位置1
|
[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
|
[7, 3, 7, 2, 9, 6, 4, 6, 9, 7], // 位置7
|
||||||
];
|
];
|
||||||
|
|
||||||
// 生成6位(前15位去掉最后一位校验位)
|
if (digitsNeeded > positionWeights.length) {
|
||||||
|
return generateLuhnNumber(prefix, totalLength);
|
||||||
|
}
|
||||||
|
|
||||||
let pattern = '';
|
let pattern = '';
|
||||||
for (let pos = 0; pos < 6; pos++) {
|
for (let pos = 0; pos < digitsNeeded; pos++) {
|
||||||
const weights = positionWeights[pos];
|
const weights = positionWeights[pos];
|
||||||
const digit = this.weightedRandomDigit(weights);
|
const digit = this.weightedRandomDigit(weights);
|
||||||
pattern += digit;
|
pattern += digit;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 组合前缀
|
const partial = prefix + pattern;
|
||||||
const prefix15 = prefix + pattern;
|
const checkDigit = this.calculateLuhnCheckDigit(partial);
|
||||||
|
|
||||||
// 计算Luhn校验位
|
return partial + checkDigit;
|
||||||
const checkDigit = this.calculateLuhnCheckDigit(prefix15);
|
|
||||||
|
|
||||||
return prefix15 + checkDigit;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user