一个国家的税号体系通常要同时容纳两类主体:个人与法人。巴西的做法是分成两套号码,各自独立编号、各自带校验位,长度与权重也不相同。它们的校验思路属于同一个算法家族,所以放在一起看反而更容易记住——差别只在参数,不在骨架。
CPF 和 CNPJ 分别是什么?
CPF 是巴西用于标识自然人的税号。CNPJ 用于标识法人,也就是公司、机构一类的主体。两者都是纳税与开票场景中的常用标识,因此在表单里经常同时出现:个人填前者,企业填后者。
它们在体系里是并列关系,不是包含关系。一个自然人不会因为开了公司就换掉自己的税号,一家公司也不会用某个自然人的号码代替自己的法人号码。表单若只提供一个字段却不区分主体类型,几乎必然会出现填错栏位的情况。
因此在实际系统里,「你在验哪一种」这个信息往往比算法本身更重要。同一个输入框里,用户可能贴进任何一种号码,处理逻辑应当先判断形状属于哪一套。
两种号码的结构差异
从结构上看,两套号码长度不同,法人号码明显更长,这也是肉眼区分它们最直接的方式。除此之外,两套号码都由数字组成,末尾都带两位校验位。
两位校验位不是随手放的:它们分别由前面的位按各自规则算出,第二位校验位的计算会把第一位校验位也纳入加权范围。这种「校验位依赖前一位校验位」的写法在多套体系里都能见到,作用是让改动任何一位都更容易被察觉。
| 对比项 | 自然人税号 | 法人税号 |
|---|---|---|
| 标识对象 | 自然人 | 法人或其他组织 |
| 长度 | 较短 | 较长 |
| 校验位 | 末尾两位 | 末尾两位 |
| 权重安排 | 一套逐位变化的权重 | 另一套不同的权重 |
两套体系在长度上不重叠,这一点对实现很友好:解析时通常可以只靠长度先分流,再用字符集兜底,不必在没有把握的情况下猜。
校验位的算法思路:加权求和再取模
两者的算法属于同一家族:把前面的位与一组逐位变化的权重相乘、求和,对十一取模,再把余数按规则映射成一个数字。两套号码的权重取值不同,这是它们唯一的实质区别。
映射规则里有一个细节值得留意:当取模得到的余数落在某个特定取值上时,结果不能直接当作数字使用,标准会规定它写成零或其他指定值。这类约定在实现中极易被漏掉,漏掉之后大多数输入仍然能通过,只有落在那个取值上的输入会稳定算错,排查起来非常费时。
第二位校验位的计算会把第一位的结果拼进待计算的位里,所以两套计算不能并行写成一个函数的两半,必须串行。这也是为什么很多实现选择把校验位的计算单独封装成一个纯函数,供两套号码各自调用。
为什么全同数字要单独排除?
把所有权重相加再取模,很容易得到一个看起来成立的结论,即使输入是所有位都相同的那种值。这不是算法缺陷,而是取模运算的性质:当加权和恰好是模数的整数倍时,余数落在规定值上,校验自然通过。
所以正式规则通常显式规定,所有位相同的输入一律排除在合法号码之外,无论它的校验位算出来是什么。这一条必须靠形状检查补上,而不是靠算术层发现。实现时若只做算术校验,就会出现明显不合理的输入被判为通过的情况,这类结果对使用者是严重的误导。
这也说明一个更普遍的道理:校验位算法抓的是算术一致性,形态上的退化输入需要额外的规则去过滤。两层各司其职,缺一不可。
格式校验与税号登记状态是两件事
本地校验能确认的只有字符自洽。这个税号是否真的登记过、属于哪家企业、当前是否处于正常状态、能否用于开票,都要到登记体系里查。前者是算术,后者是查询。
查询层有它自己的限制。可能因为网络或对方系统而超时,可能对短时间内的密集调用做限制,返回的状态也可能随时间变化。因此在设计流程时,建议把校验结论与查询结论分开保存:校验通过只说明输入格式没问题,查询结果才是业务判断的依据。
反过来,如果校验失败,应当提示用户核对输入,而不是断言这个税号不存在。这两句话对用户的意义完全不同,写错会把人引向错误的自救方向。在跨系统交换数据时还要留意字符层面的差异:号码可能带着格式化字符一起入库,两个系统对同一主体的记录因此对不上,去重与关联都会失效。入库前统一规范化,比事后补写匹配规则便宜得多。
给开发者:清洗、掩码与错误提示
实装时先归一化:去掉用户习惯性输入的点、斜杠与连字符,再统一处理大小写。这一步做完才谈长度与算术判断,否则正确的号码也会因为多了一个符号而失败。
界面上的掩码要谨慎。掩码能降低输入错误率,但也可能让用户粘贴时被截断或错位;折中做法是允许直接粘贴整串,粘贴后自动重排格式,并同时回显规范化之后参与校验的那一串字符。
错误提示建议按层的结论来写:字符集不对、长度不对、校验位不成立,这三类的处理办法不同。落到具体功能上,几件事值得按顺序做完:
- 输入框允许直接粘贴,不要强制用户在打字时逐段跳格。
- 粘贴后自动去掉点、斜杠与连字符,并统一字母大小写。
- 回显规范化结果,让用户看到究竟是哪一串参与了计算。
- 校验失败时区分形状错误与算术错误,分别给出不同的提示语。
- 表单上说明当前填的是自然人还是法人税号,避免填错栏位。
两套号码的参数差别属于算法家族的常见形态,可对照校验位算法家族阅读;如果系统要支持多个国家的税号,先看各国证件号的校验规则为什么统一不了,那篇讲的是规则怎么组织成数据。
下一步
拿一条待测输入粘进号码校验工具,看它给出的是校验通过、校验失败、仅格式还是无规则,再据此决定是修输入还是改流程。若这个字段要接入外部查询,先把校验与查询的结果字段拆开,再谈超时与重试。
本文只讨论公开的校验位思路,不提供任何具体号码的位数与取值规则;文中不出现真实税号,所述校验仅能确认字符是否自洽,不表示任何号码已登记、有效或可用于税务场景。