
简介credit-card-php是一套基于PHP的信用卡号生成与验证工具包定位为轻量级Composer组件主要面向需要在开发环境模拟银行卡号、测试支付逻辑或校验卡号合法性的PHP开发者与测试人员。通过Composer可快速引入项目也支持全局安装后在命令行直接使用ccgenerator和ccvalidator适合集成到自动化测试或支付网关对接流程。包内共15个文件以8个PHP源码文件为核心分别承担生成器与验证器的核心逻辑同时提供两个命令行入口免编码即可调用composer.json、README、许可证等文件补充了依赖声明、使用说明与授权信息另含test测试目录与bin可执行目录便于运行自测和查看调用方式整个压缩包仅12KB结构精简。目前已有2369人学习下载。通过该资源使用者既能在项目中引入类库调用生成与校验方法也能阅读实现理解卡号校验算法生成器可产出符合Luhn算法的随机卡号验证器则快速判断号码是否合法适合支付开发、数据模拟等场景作为轻量工具或学习范例均很有价值。1. 信用卡号生成器和验证器为什么 PHP 实现会成为测试台标配支付联调里最容易被低估的不是签名算法而是那张测试卡号。网上现成的信用卡号生成器页面在沙箱环境里经常被风控拦截或者卡组织和渠道支持的 BIN 不匹配导致返回一堆莫名其妙的报错。信用卡号生成器和验证器如果能在 PHP 代码库里直接跑生成器按你要的 BIN 和长度产出符合 Luhn 规则的测试号验证器再对线上回传的卡号做格式与校验位检查整个联调闭环就踏实了。这套东西适合正在写支付接口的 PHP 后端、要模拟多张卡做自动化测试的 QA以及维护电商支付模块的某公司内部工具开发者投入半天时间换来的是一劳永逸的测试数据来源。2. 卡号生成前必须搞懂的两块地基BIN 结构组成与 Luhn 算法原理2.1 卡号结构BIN、账号位和 Luhn 校验位银行卡号不是一串无规律的随机数字。常见的国际卡组织卡号大体由三部分组成前 6 位是发卡行识别码也就是常说的 BIN中间部分是发卡机构自定义的账号位位数不固定最后 1 位是校验位绝大多数卡组织使用 Luhn 算法。生成器要做的就是给定一个 BIN补齐随机账号位和正确的校验位验证器则反过来对一个完整卡号做 Luhn 校验再识别它属于哪个卡组织。不同卡组织的 BIN 前缀和卡号长度有明显差异这是生成器和验证器都需要维护的第一张表。卡组织常见 BIN 前缀常见卡号长度Visa413、16、19Mastercard51-55、2221-272016American Express34、3715Discover6011、65、644-64916-19UnionPay6216-19这张表是通用的规则实际对接某支付沙箱时渠道文档里往往只放行少量测试 BIN。把 BIN 表作为参数独立维护而不是写死在生成逻辑里是我在这个方向上的第一习惯。因为卡组织规则会更新渠道支持的测试 BIN 也会换参数化的 BIN 表能让生成器跟上变化。2.2 Luhn 算法手工拆解从右往左的加倍与求和Luhn 算法也叫模 10 算法它的核心规则是从卡号最右边一位开始每隔一位数字乘以 2如果乘以 2 后大于 9就减去 9最后把所有数字加在一起总和能被 10 整除卡号就通过校验。这个规则是从右往左定义的和编程里常见的从左往右循环正好相反是很多实现翻车的重灾区。拿声名在外的测试卡号 4242424242424242 来拆解。从右边第一位开始2 不动第二位 4 乘以 2 得 8第三位 2 不动第四位 4 乘以 2 得 8照这样交替下去16 位数字最终会得到 8 组“2 加 8”的组合总和是 8080 除以 10 余数为 0所以这张卡校验通过。如果把某个位置的加倍逻辑搞反得到的总和就会变成 70 或 90仍然能被 10 整除但位置错了的“有效卡”在实际验卡环境中大概率会被判无效。理解 Luhn 的关键在于校验位本身也参与计算。验证器计算整个卡号的校验和生成器则是先算出一串随机账号位最后再补一个能让校验和模 10 为 0 的数字。这个“补位”操作决定了生成器的实现顺序。2.3 为什么生成器必须先把校验位计算放在最后一步生成一张合法卡号的最直接想法是先随机拼出所有数字再调用验证器检查它是否通过 Luhn。这样做不是不行但随机拼出合法卡号的概率是 90%失败一次就要重新生成批量生成时浪费的循环次数非常可观。更高效的做法是先确定 BIN再生成中间的随机账号位最后专门计算校验位把校验位拼在末尾。校验位计算的套路比较统一先把不带校验位的部分拼接一个临时数字 0对这个“带 0 的完整卡号”计算 Luhn 校验和得到总和后取模校验位就是 10 减去这个余数再取模。如果总和本身能被 10 整除校验位就是 0。这个逻辑可以封装成独立的 luhnSum 函数让生成器和验证器共用同一个算法实现避免两套逻辑因为位置判断差异而互相打架。这也解释了为什么标题里“生成器和验证器”总是一起出现没有验证器生成器产出的卡号无法快速自检没有生成器验证器也没有稳定的测试数据来源。两者共享同一个 Luhn 核心才是完整的实现闭环。3. 生成端落地写一个可参数化的 PHP 信用卡号生成器3.1 最小可复现版本用 PHP 生成一张符合规则的卡先写出整个工具的核心也就是 Luhn 校验和计算函数。这个函数输入一串数字字符串返回该校验和生成和验证都依赖它。实现时特别注意从右往左的奇偶位置判定我用一个变量 positionFromRight 来做避免数组下标从 0 开始导致的位序混淆。function luhnSum(string $digits): int { $len strlen($digits); $sum 0; for ($i $len - 1; $i 0; $i--) { $d (int) $digits[$i]; $positionFromRight $len - $i; // 从右边开始数第一位是 1 if ($positionFromRight % 2 0) { $d * 2; if ($d 9) { $d - 9; } } $sum $d; } return $sum; }这个函数里最重要的一行是$positionFromRight $len - $i。如果写成if ($i % 2 0)就会变成从左边开始数的奇偶位置得出的总和和右边对齐后的结果完全不同。我见过不少生成器生成的卡号验证失败最后查下来都是这一行的差异。有了 Luhn 校验和函数生成器本体就顺理成章了。它接受一个 BIN 前缀和总长度先补随机账号位再算校验位。function generateCardNumber(string $bin, int $length 16): string { $prefixLength strlen($bin); if ($prefixLength $length) { throw new InvalidArgumentException(bin length must be less than card length); } $bodyLength $length - $prefixLength - 1; $body $bin; for ($i 0; $i $bodyLength; $i) { $body . random_int(0, 9); } // 先临时补一个 0 作为校验位算出总和 $sum luhnSum($body . 0); $checkDigit (10 - ($sum % 10)) % 10; return $body . $checkDigit; } echo generateCardNumber(4, 16) . PHP_EOL;逻辑很简单前缀是 4总长度 16那么中间要生成 11 位随机数字最后 1 位是校验位。random_int替代rand的原因是支付测试数据不能有规律rand在部分环境下的随机性不够生成大量数据时容易撞车。校验位这一步用临时补 0 的方式计算 Luhn 总和再反推校验位整个过程不需要在循环里重试。3.2 参数怎么设BIN 列表、卡号长度和分段格式化上面的函数虽然能跑但直接把 BIN 前缀写死很难应对多品牌测试需求。实际项目中我会把常见的测试 BIN 做成数组再按场景传入不同参数。$testBins [ visa [4, [13, 16, 19]], mastercard [51, [16]], amex [34, [15]], unionpay [62, [16, 19]], ]; foreach ($testBins as $brand [$bin, $lengths]) { foreach ($lengths as $length) { echo $brand . . $length . 位: . generateCardNumber($bin, $length) . PHP_EOL; } }参数设置有几个关键点。BIN 前缀建议用字符串而不是整数因为某些 BIN 的中间部分可能以 0 开头如果用 int 存储会丢掉前导零。卡号长度必须由调用方显式传入不要用strlen($bin)自动推导。格式化输出是另一个常用参数支付页面录入测试卡号时分段显示更接近真实卡面可以用一个简单的函数处理。function formatCardNumber(string $cardNumber): string { return trim(chunk_split($cardNumber, 4, )); } echo formatCardNumber(generateCardNumber(4, 16)) . PHP_EOL;chunk_split会把字符串按四位一组切开最后补一个空格用trim去掉尾部多余空格。这里只做展示格式化真正传给验证器时必须先把空格和分隔符去掉否则 Luhn 计算会读入非数字字符。3.3 批量生成测试卡号去重、上限和覆盖策略批量生成是测试场景里最常见的需求。一次要生成 50 张不同卡号的测试数据如果直接循环调用generateCardNumber可能产生重复卡号。重复本身不会让 Luhn 校验失败但会让上游系统的风控逻辑误以为你在用同一张卡重复下单。处理办法是用数组键去重。function generateBatch(string $bin, int $length, int $count): array { $cards []; $attempts 0; $maxAttempts $count * 20; while (count($cards) $count $attempts $maxAttempts) { $attempts; $card generateCardNumber($bin, $length); $cards[$card] true; } return array_keys($cards); } $cards generateBatch(4, 16, 10); foreach ($cards as $card) { echo formatCardNumber($card) . PHP_EOL; }去重的原理是把卡号作为数组键键本身不会重复最后用array_keys取回卡号列表。$maxAttempts是一个保险丝防止 BIN 前缀和长度对应的空间太小导致 while 循环永远凑不满数量。比如说你想生成 1000 张卡但 BIN 前缀占掉了 15 位剩余空间只有 10 的 0 次方那生成器的循环就永远不会停。正确做法是提前计算最大可生成数量如果目标数量超过空间上限直接抛异常或用顺序递增的策略去铺满整个空间。另一个常见场景是连续卡号覆盖。风控规则往往会拦截一段时间内出现的大量连续卡号所以生成器默认用随机策略。但如果你想验证某个特定区间的账号位可以改造生成函数把随机填充部分替换成传入的账号位只求校验位。这也是把生成器参数化的价值所在。4. 验证端落地Luhn 校验加 BIN 识别的 PHP 实现4.1 验证器的第一道关卡Luhn 校验和长度过滤验证器的核心逻辑和生成器共用同一个luhnSum函数这样能保证生成端和验证端的算法完全一致。验证时先过滤非数字字符再做长度判断和 Luhn 校验。顺序很重要先长度过滤再 Luhn能避免对超长字符串做无意义的取模计算。function isValidLuhn(string $cardNumber): bool { $digits preg_replace(/\D/, , $cardNumber); if (strlen($digits) 12 || strlen($digits) 19) { return false; } return luhnSum($digits) % 10 0; } var_dump(isValidLuhn(4242424242424242)); // bool(true) var_dump(isValidLuhn(4242424242424241)); // bool(false)这里把最小长度设为 12是因为正规卡组织卡号最短的 American Express 也有 15 位但测试环境里偶尔有自定义的 13 位卡因此我用 12 作为安全下限。最大长度 19 是当前主流卡组织通用上限超过这个长度的输入直接判无效避免后续字符串转 int 时出现精度丢失的问题。preg_replace(/\D/, , $cardNumber)的作用是清除所有非数字字符包括空格、横线、制表符。如果你从 Excel 表格里复制卡号粘贴内容里可能带了不可见字符这一步能统一化解。但要注意如果原始输入包含字母清理后长度会变化必须在清理之前先判断原始字符串不能拿清理后的长度直接覆盖。4.2 识别卡组织BIN 前缀、长度和验证结果的完整结构Luhn 通过只是第一关支付场景还需要知道这张卡属于哪个卡组织。识别逻辑并不复杂根据前几位前缀和长度做匹配。下面是简化但实用的识别函数。function detectBrand(string $cardNumber): ?string { $digits preg_replace(/\D/, , $cardNumber); $len strlen($digits); $first $digits[0] ?? ; $prefix2 (int) substr($digits, 0, 2); $prefix6 (int) substr($digits, 0, 6); if ($first 4 in_array($len, [13, 16, 19], true)) { return visa; } if ($len 16 (($prefix6 510000 $prefix6 559999) || ($prefix6 222100 $prefix6 272099))) { return mastercard; } if ($len 15 ($prefix2 34 || $prefix2 37)) { return amex; } if (($first 6) ($prefix6 601100 $prefix6 601199 || $prefix6 650000 $prefix6 659999)) { return discover; } if ($len 16 $len 19 $prefix2 62) { return unionpay; } return null; }这个函数返回?string识别不出来就返回null。注意 Mastercard 的 2221-2720 范围是 6 位数比较时用$prefix6 222100 $prefix6 272099不是直接用substr($digits, 0, 4)。用 int 转换前缀在大多数场景没问题但 6 位 BIN 可能以 0 开头int 会丢掉前导零所以实际项目我更推荐用字符串比较函数strncmp或者正则表达式避免类型转换带来的边缘错误。验证结果最好用一个结构化的数组返回而不是只返回布尔值这样调用方可以直接用作接口层的数据结构。function validateCardNumber(string $input): array { $digits preg_replace(/\D/, , $input); if (strlen($digits) 12 || strlen($digits) 19) { return [valid false, reason invalid_length]; } if (!isValidLuhn($digits)) { return [valid false, reason luhn_failed]; } $brand detectBrand($digits); if ($brand null) { return [valid false, reason unknown_brand]; } return [valid true, brand $brand]; }valid、reason、brand三个键组成了自然的验证结果契约。有效时reason没有意义无效时brand为空。这样在单元测试里可以精确断言失败原因而不是只看到 false 却不知道卡号是长度错还是校验位错。验证器放在支付模块的工具函数里时还能在日志中记录失败原因方便排查线上联调问题。4.3 输入清洗与边界处理空格、连字符、换行符和异常类型验证器最常见的翻车点不是算法而是输入类型。PHP 是弱类型语言调用方很可能传一个 int 或者 float 进来比如从表单直接取值。一个 16 位数字在 64 位 PHP 里还能被 int 表示但如果你打印出来底层转换容易在超过 19 位时变成科学计数法。因此我习惯在验证器入口做一个严格的清洗函数。function normalizeCardNumber($input): ?string { if (is_int($input) || is_float($input)) { return null; } $digits preg_replace(/\D/, , (string) $input); if ($digits || strlen($digits) 19) { return null; } return $digits; }这个函数清掉了空格、连字符、换行符之外还把int和float类型的输入直接拒掉。因为浮点数在表示大整数时会丢失精度比如4242424242424242.0转成字符串会变成更短的表示Luhn 验算对不上。与其在后续计算里隐藏 bug不如在入口就报错让调用方明确传入字符串。清洗函数返回null时验证器可以直接返回[validfalse, reasoninvalid_input]。注意preg_replace会把所有非数字字符替换成空字符串所以如果一个字符串包含字母清洗结果可能比原字符串短很多。处理完的结果只用于 Luhn 校验和品牌识别不能作为支付请求里的最终卡号因为那些空格和连字符本身不属于卡号。5. 生成与验证过程中的 5 个常见坑和排查方法5.1 批量生成后验证器报错生成器与验证器用了两套 Luhn 逻辑现象批量生成的卡号里偶尔有几张自己验证不过手动用笔算校验位又是对的但程序返回 false。原因生成器里的 Luhn 函数和验证器里的 Luhn 函数不是同一个实现。一个按从右往左的偶数位加倍另一个按从左往右的偶数位加倍两个函数对同一串数字会给出不同的校验和生成的卡自然过不了验证器。解决把luhnSum定义在同一个文件中生成器和验证器都调用它不要复制粘贴成两份。如果需要放到不同文件用require引入共享工具文件。代码审查时重点盯positionFromRight这一行这是 Luhn 实现里最容易写错的地方。5.2 在支付沙箱中被提示“不支持该卡 BIN”现象生成的卡号 Luhn 校验通过BIN 也能识别出卡组织但提交到某支付沙箱后返回invalid_bin或unsupported_card_brand。原因渠道沙箱环境只放行了特定测试 BIN不是所有合法卡号都能通过。比如某支付渠道只支持 62 开头的测试卡你拿 4 开头生成的卡号自然被拒。解决先去读渠道文档里的测试卡列表把列出来的 BIN 前缀配进生成器的白名单而不是把通用卡组织表当成唯一依据。白名单做成配置项放配置文件里不同渠道切换时直接换配置。BIN 表按渠道维护是我认为生成器最值得投入的参数化设计之一。5.3 校验位计算结果对但整张卡被风控系统判定为高风险现象卡号合法、Luhn 通过、BIN 也匹配但沙箱风控返回velocity_limit_exceeded或duplicate_card。原因批量生成时用了同一个 BIN 且账号位是顺序递增的。很多生成器为了省事用str_pad从 0 开始补齐账号位结果一口气生成 100 张连续卡号风控系统会把这些卡号识别成扫号行为。解决生成器使用random_int填充随机账号位不要自己维护自增计数器。批量生成的数量控制在单次测试需要的最小范围比如 10 张就只生成 10 张。如果确实需要连续覆盖区间做边界测试降低调用频率并在每次调用之间加随机延迟。5.4 输入 4242424242424242 时验证结果时对时不时现象同样的测试卡号在本机 PHP 脚本里验证通过换到一个老项目里验证失败甚至同一个项目里调试模式通过、生产环境失败。原因大概率是类型问题。调用方把卡号传成了 int 或 floatPHP 在转换成字符串时把大数字变成了科学计数法或者原始输入带了不可见的 BOM 字符和回车符trim没去掉。解决统一使用normalizeCardNumber做入口清洗拒绝 int 和 float用preg_replace(/\D/, , ...)过滤所有非数字字符。在验证器单元测试里固定几组样本比如4242424242424242必须返回 true4242424242424241必须返回 false。这些固定用例能第一时间暴露类型转换问题。5.5 验证器说不是有效卡号但渠道文档里确实有这个测试卡现象用支付渠道文档里提供的某个测试卡号输进自己的验证器返回validfalseLuhn 不过或者 BIN 识别不了。原因渠道文档里的部分测试卡号是专门模拟异常场景的比如模拟余额不足、持卡人认证失败、发卡行拒绝交易等。这些卡号本身不遵循 Luhn 算法或者使用了非公开的特殊 BIN。解决验证器不要做成支付提交的唯一门禁。遇到渠道文档里的特殊测试卡号单独维护一个special_cards白名单验证器在 Luhn 失败后先查白名单命中则返回validtrue但标记specialtrue。这样既能保持算法严谨又不妨碍沙箱联调。这个坑是区分“格式验证器”和“业务风控”的关键边界需求方一定要理解清楚。6. 把工具封装成 CLI 小库批量测试数据、单元测试集成与扩展思路6.1 一个可以直接使用的 CLI 入口生成器和验证器写完只是第一步真正提高效率的是让它能被命令行直接调用。我习惯把luhnSum、generateCardNumber、generateBatch、validateCardNumber等函数集合到一个CardTool.php文件里然后写一段命令行入口代码。#!/usr/bin/env php ?php require CardTool.php; $command $argv[1] ?? help; switch ($command) { case generate: $bin $argv[2] ?? 4; $length (int) ($argv[3] ?? 16); $count (int) ($argv[4] ?? 1); foreach (generateBatch($bin, $length, $count) as $card) { echo formatCardNumber($card) . PHP_EOL; } break; case validate: $input $argv[2] ?? ; echo json_encode(validateCardNumber($input), JSON_UNESCAPED_UNICODE) . PHP_EOL; break; default: echo 用法: cardtool.php generate [bin] [length] [count]\n; echo cardtool.php validate card_number\n; }命令行入口适合在 CI 脚本里批量生成测试数据也适合本地调试时快速验卡。参数顺序固定为 BIN、长度、数量能覆盖大部分场景。执行php cardtool.php generate 4 16 5会输出 5 张 16 位 Visa 测试卡执行php cardtool.php validate 4242424242424242会输出 JSON 格式的验证结果。6.2 测试与扩展配合 PHPUnit 数据源和渠道 BIN 表另一个容易忽略的落地方法是把生成器作为 PHPUnit 的测试数据源。支付相关的单元测试如果硬编码一张卡号CI 跑多了容易被沙箱风控盯上。每次测试动态生成新卡号是更健康的做法。public function cardNumberProvider(): array { $bins [4, 51, 34, 62]; $cards []; foreach ($bins as $bin) { $cards[] [generateCardNumber($bin, $this-lengthFor($bin))]; } return $cards; } /** * dataProvider cardNumberProvider */ public function testPaymentRequestAcceptsGeneratedCard(string $card): void { $this-assertTrue(validateCardNumber($card)[valid]); }这里我用lengthFor($bin)做了一个参数映射简单情况下直接在数组里写[[4,16], [51,16], [34,15], [62,16]]即可。这样每个测试用例拿到的是新卡号既不重复也不容易被沙箱封掉。渠道 BIN 表建议单独放在config/card_bins.php文件里让支付联调的人可以只改配置不碰代码。至于扩展方向可以给生成器增加返回完整模拟数据的接口比如连同有效期和 CVV 一起生成。需要明确的是CVV 不是算法推导出来的它由发卡行自行决定所以测试环境里的 CVV 必须从渠道文档中读取不能由生成器编造。验证器这边可以增加对渠道特殊测试卡白名单的支持让联调时既能跑通异常流程又不破坏 Luhn 校验的通用性。我现在的习惯是任何需要测试卡号的 PHP 项目都会在首次联调时把生成器和验证器放进工具函数的 helpers 里后续接新渠道只需要改 BIN 配置和特殊卡白名单。希望帮到你。本文还有配套的精品资源点击获取