在代码里校验信用卡号,看起来是一件已经被解决的事:清洗一下输入,跑一遍模 10,返回真假。真正上手之后才会发现,麻烦从来不在算法,而在于顺序、边界和提示语——这三样决定了你的校验是帮用户填对,还是把用户挡在门外。
这篇文章面向要动手写这段逻辑的人。读完你会得到一套可以照着落地的校验顺序,以及一份值得在代码评审里逐条对照的坑清单。
校验到底在防什么?
先把目标说清楚。校验卡号能防的只有一件事:录入错误。用户少打一位、把两个数字写反、把卡号记成了另一张卡的号码,这些是它能抓住的。
它防不了的是另一类问题:号码是否真实存在、是否还有额度、是否已被挂失、输入的人是否持卡人。这些问题只能通过向发卡行发起授权来回答。任何声称本地校验能判断「卡是否有效」的说法,都是对校验能力的误解。
把这两类问题分开之后,设计就顺了:本地校验负责快速拦截明显错误的输入,给用户即时反馈;真实的判断交给授权流程,并且要在网络请求失败时留好降级路径。
需要哪些字段一起看
只校验卡号是不够的,因为几个字段之间存在依赖关系:
| 字段 | 与其他字段的关系 | 本机能校验到什么程度 |
|---|---|---|
| 卡号 | 决定卡组织,进而决定安全码位数 | 长度、前缀、校验位 |
| 有效期 | 独立,但影响卡是否可用 | 是否已过期(到月底) |
| 安全码 | 位数随卡组织变化 | 只校验位数与字符类型 |
| 持卡人姓名 | 与卡号无必然对应 | 非空与长度 |
这里最容易被忽略的是第三行。安全码的位数不是固定的,按卡组织的不同可能是三位或四位,所以校验它的位数之前,必须先由卡号确定卡组织。这条依赖关系如果处理反了,就会出现运通卡用户被要求填三位数字的奇怪界面。
安全码的完整规则见CVV 是什么。
正确的校验顺序
建议把顺序固定成九步,每一步失败都有自己专属的提示,不要用一句笼统的「卡号无效」打发所有情况:
- 去掉分隔符:空格、连字符、制表符都不该影响判断。
- 统一字符形式:全角数字要转成半角,这是中文输入环境下最常见的意外输入。
- 检查字符集:剩下的是否全是数字。有字母就说明用户可能把别的编号填进来了。
- 检查长度范围:落在 13 到 19 位之间才继续,超出直接拒绝。
- 识别卡组织:用首位与前置号段匹配,匹配不到也要给出区别于长度错误的提示。
- 检查该卡组织的长度:不同卡组织的合法长度集合不同,不要用统一的一个数字。
- 检查安全码位数:按上一步确定的卡组织判断。
- 检查有效期是否已过期:注意到印刷月份的月底为止。
- 最后跑模 10 校验:放在最后,因为它最容易被误当成万能校验。
第九步放最后是有理由的。如果先跑模 10,你会在长度都不对的输入上浪费计算,更重要的是,你会得到一个无法解释的失败——用户不知道自己该改哪里。按这个顺序走,每一次拒绝都能对应到一句人话。
模 10 的具体算法见Luhn 算法。
用户填错时该说什么
提示语的质量决定了用户能不能自己修好。几条经验:
- 区分「格式不对」和「卡号无效」。前者是能用一句话说清的问题,后者听起来像是怀疑用户。
- 不要说出校验位的存在。告诉用户「校验位不匹配」只会让人困惑;说「请检查卡号是否输入正确」就够了。
- 在失焦之后再提示。用户还在输入时不要报错,那会让人以为每敲一个数字都要通过一次检验。
- 不要在输入满 16 位就立刻提交校验。有些卡组织的长度不是 16 位,自动触发会误伤。
- 保留用户的输入。校验失败后清空输入框,是所有提示方式里最让人恼火的。
还有一条容易被忽略:不要把完整卡号回显在错误信息或页面上。界面上的展示约定是只显示前六位与后四位,具体原因见信用卡号格式。
提示语之外,还有一条和重复提交有关的经验:用户会重复点击。网络慢的时候、按钮没有及时变成加载状态的时候、提交后页面没有明显反馈的时候,他们都会再点一次。如果每次点击都触发一次新的扣款请求,那么一次网络抖动就会变成一笔重复账单。解决办法是给每次提交带一个唯一标识做去重,或者让提交按钮在发出请求后立刻进入不可再点的状态。这两种做法都比事后向用户解释要省事得多。
只在前端校验可以吗?
不可以。前端校验的唯一职责是提升体验,让人不必等一次网络往返才知道自己少打了一位。它不构成任何安全保证,因为浏览器里的逻辑完全在用户控制之下:请求可以被直接构造,前端代码可以被绕过。
因此服务端必须独立完成同一套校验,不能复用前端传过来的结论。一个常见的错误做法是前端校验通过后附加一个标记,服务端看到标记就跳过校验——这等于把校验权交给了调用方。
也不要走到另一个极端,把所有校验都放在服务端、前端什么都不做。那样用户要等一次请求才知道输入格式不对,体验会差很多。正确形态是两边都做,且两边的规则来自同一份约定,保持一致。
给开发者:几个能活过代码评审的坑
下面这些错误之所以常见,是因为它们在多数输入上表现正常,只在特定情况下才暴露:
- 把长度写死。以为合法长度只有 16 位,结果 15 位的卡全被拒。
- 忘记处理加倍后超过九。它只影响部分号码,看起来像随机失败。
- 在奇数位上出错。循环方向写反,导致 15 位号码的校验结果全错。
- 清洗时把数字删坏了。用替换方式去掉非数字字符,会把粘贴进来的其他内容一起吞掉,用户看到的是号码莫名少了几位。
- 先校验后清洗。顺序颠倒会让带空格的输入直接失败,而用户完全看不出问题在哪。
- 把测试号码当成有效号码。测试用的合成号码能通过本地校验,但从未发行给任何人,任何真实授权都会被拒绝。
- 没有准备无效用例。测试集里全是合法号码,等于没有测试校验逻辑。挑选无效用例的方法见测试信用卡号是什么。
最后一个坑不属于校验逻辑,但更致命:把校验函数写成「顺便也返回卡组织、卡类型、是否借记卡」的多功能函数。这类函数会随着需求不断膨胀,最终没人敢改。把「归一化」「识别卡组织」「纯算术校验」拆成三个互不依赖的步骤,测试和复用都会轻松得多。
下一步
动手之前,先把顺序定下来,再把每一步对应的提示语写出来,最后才写具体实现。顺序和提示语才是这段代码的主体,算术只是其中一步。