支付收银台接入银行卡归属地查询:绑卡环节的校验链路怎么设计
# 支付收银台接入银行卡归属地查询:绑卡环节的校验链路怎么设计
> 接口:银行卡归属地查询(apiCode=30)· 接入点:`30-7` · 计费:5 厘/次,查询失败不计费 · 适用人群:支付与风控方向的产品经理、全栈工程师 · 阅读时间:约 9 分钟
> 最后实测核对:2026-09-15
一句话结论:在绑卡环节接入银行卡归属地查询,价值是把「一串卡号」翻译成「哪家银行、什么卡种、开户地在哪」,让卡种规则、展示文案和落库字段都有真实数据可依;链路设计的关键是决定查询时机、把三类返回结果映射成三种前端文案、以及落库时存 `formatBankName` 而不是 `bankName`。
## 绑卡环节缺的是什么
用户在收银台输入卡号,提交,后台只拿到 16 到 19 位数字。接下来要做的判断有这些:
- 这张卡是借记卡还是信用卡?业务规则可能只接受其中一种。
- 卡号本身形态是否合法?位数、BIN 段、Luhn 效验能筛掉一部分输错的号码。
- 是哪个银行的?决定要不要走特定的银行通道、要不要显示特定的到账时间说明。
- 展示层面要显示什么?银行标志、客服电话能让用户确认自己没填错。
银行卡归属地查询一次调用能覆盖这四点,返回体里 `cardType`、`card_bin`、`card_digits`、`isLuhn`、`formatBankName`、`logo`、`tel` 都有。
## 数据表怎么设计
先把要落库的字段定下来。存展示用的规范行名,不存 `bankName`。
```sql
CREATE TABLE bank_card_info (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
card_no_hash CHAR(64) NOT NULL COMMENT '卡号加盐哈希,与缓存键同源',
card_no_tail VARCHAR(8) NOT NULL COMMENT '卡号后 4 位,仅用于展示',
bank_code VARCHAR(64) NOT NULL DEFAULT '' COMMENT 'formatBankName,249 项封闭枚举',
bank_full_name VARCHAR(64) NOT NULL DEFAULT '' COMMENT 'bankName 原始值,留档用',
card_type VARCHAR(16) NOT NULL DEFAULT '' COMMENT 'cardType,如 借记卡',
brand VARCHAR(64) NOT NULL DEFAULT '' COMMENT 'brand,仅展示,不做枚举校验',
area VARCHAR(64) NOT NULL DEFAULT '' COMMENT 'area,格式「一级 - 二级」',
tel VARCHAR(32) NOT NULL DEFAULT '' COMMENT '银行客服电话',
homepage VARCHAR(128) NOT NULL DEFAULT '' COMMENT 'url,裸域名,展示前补协议头',
logo_url VARCHAR(255) NOT NULL DEFAULT '' COMMENT 'logo,部分银行为空',
card_digits VARCHAR(4) NOT NULL DEFAULT '' COMMENT 'card_digits,卡号长度',
is_luhn VARCHAR(4) NOT NULL DEFAULT '' COMMENT 'isLuhn:1 / 0 / 空字符串',
query_status VARCHAR(24) NOT NULL DEFAULT 'ok' COMMENT 'ok / card_not_found / area_not_found',
queried_at DATETIME NOT NULL,
UNIQUE KEY uk_card_hash (card_no_hash)
) COMMENT '银行卡归属信息';
```
几个字段的取舍理由:
- `bank_code` 存 `formatBankName`。2026-09-15 实测同一 BIN 段的两个卡号,`bankName` 一次返回 `中国农业银行`、一次返回 `农业银行`,而 `formatBankName` 两次都是 `农业银行`。用它做分组和映射更稳。
- `brand` 只存不用。接口文档写明 `brand` 「可能返回枚举值外的其他值,但概率很小」,把它当成严格枚举做校验,会把成功的调用判成失败。
- `is_luhn` 保留三态。它的取值是 `1`(通过)、`0`(不通过)、空字符串(不支持),不能压成布尔量。
- 不存明文卡号。`card_no_tail` 只留后 4 位用于页面展示。
## 查询时机:失焦查还是提交前查
两种做法,取舍点不同。
**输入框失焦时查。** 优点是在用户还在页面上时就把结果渲染出来,银行标志和行名可以即时显示,用户能立刻发现自己填错了。缺点是用户改一次位数就会触发一次请求。
**提交前查。** 优点是调用次数最少,一张卡一次。缺点是用户要等结果回来才知道卡种不符合规则,体验上多一次往返。
折中做法:失焦时先做本地校验(位数、Luhn 算法可以本地算),通过之后再发起接口调用,并且对同一卡号做去重——用户来回切换输入框时不重复发请求。
```python
import re
def precheck(card_num: str) -> bool:
"""本地预校验:只做位数和纯数字判断,成本为零。"""
return bool(re.fullmatch(r"\d{12,19}", card_num or ""))
def luhn_ok(card_num: str) -> bool:
"""本地实现 Luhn,用于在发起接口调用前先筛掉明显输错的号码。"""
digits = [int(c) for c in card_num][::-1]
total = 0
for i, d in enumerate(digits):
if i % 2 == 1:
d *= 2
if d > 9:
d -= 9
total += d
return total % 10 == 0
```
本地 Luhn 与接口返回的 `isLuhn` 不冲突:本地的用于快速筛,接口的用于留档与对账。
## 三类返回结果怎么映射成前端文案
这是链路里最容易做粗糙的一段。实测的三种失败形态,业务含义完全不同,前端提示也该不同。
| 返回形态 | 判定条件 | 业务含义 | 前端提示 |
|------|------|------|------|
| 成功 | 外层 `showapi_res_code=0` 且 `ret_code=0` | 信息完整 | 正常显示银行标志与行名 |
| 卡号未收录 | 外层 `0`,`ret_code=-1`,带 `remark` | 卡号形态有问题或该 BIN 未收录 | 「卡号有误,请核对后重新输入」 |
| 归属地未收录 | 外层 `0`,`ret_code=-1`,`area` 为 `该卡归属地信息暂未收录 - ` | 银行识别到了,归属地库无记录 | 正常显示行名与标志,隐藏归属地一行 |
| 请求未受理 | 外层 `showapi_res_code=-1` | 参数或鉴权问题 | 「服务暂时不可用」,走重试 |
注意第三行:这一形态 `ret_code` 为 `-1` 但 `bankName`、`formatBankName`、`tel`、`url`、`logo` 都是正常值。绑卡场景只需要行名和标志,这一形态的数据可以直接用,只是不显示归属地。
判定代码:
```python
AREA_PLACEHOLDER = "该卡归属地信息暂未收录"
def classify(data: dict) -> dict:
"""把接口返回映射为页面需要的状态。"""
if data.get("showapi_res_code") != 0:
return {"state": "unavailable", "msg": "服务暂时不可用"}
body = data.get("showapi_res_body") or {}
if str(body.get("ret_code")) == "0":
return {"state": "ok", "body": body}
if body.get("remark"):
return {"state": "card_not_found", "msg": "卡号有误,请核对后重新输入"}
if (body.get("area") or "").startswith(AREA_PLACEHOLDER):
return {"state": "area_not_found", "msg": "", "body": body}
return {"state": "failed", "msg": "未查到归属信息"}
```
占位串用前缀匹配,不写全等——它的完整值是 `该卡归属地信息暂未收录 - `,末尾带分隔符。
## 完整链路
```
用户输入卡号
│
├─ 本地预校验(位数、纯数字)
│ └─ 不通过 → 前端提示,不发请求
│
├─ 本地 Luhn 预筛
│ └─ 不通过 → 记入 soft_flag,仍继续查询(Luhn 不通过的卡号可能不支持效验)
│
├─ 调用 30-7(needBin=1)
│ └─ 返回 → classify() 分流
│
├─ state = ok
│ ├─ cardType 落在业务允许范围 → 落库,展示行名 + 标志 + 客服电话
│ └─ cardType 不在范围 → 提示「暂不支持该卡种」,不入绑卡流程
│
├─ state = card_not_found → 提示核对卡号
├─ state = area_not_found → 落库(行名可用),隐藏归属地一行
└─ state = unavailable → 记录日志,走重试队列
```
状态流转的伪代码:
```python
def bind_card(card_num: str, allowed_types=("借记卡", "信用卡")) -> dict:
if not precheck(card_num):
return {"ok": False, "msg": "请输入 12~19 位银行卡号"}
raw = query_with_retry(card_num) # 带限流与重试的调用封装
result = classify(raw)
if result["state"] == "unavailable":
return {"ok": False, "msg": result["msg"], "retryable": True}
if result["state"] == "card_not_found":
return {"ok": False, "msg": result["msg"], "retryable": False}
body = result.get("body", {})
if body.get("cardType") not in allowed_types:
return {"ok": False, "msg": f'暂不支持{body.get("cardType", "该")}卡'}
save_bank_card_info(card_num, body, status=result["state"])
return {"ok": True, "bank": body.get("formatBankName") or body.get("bankName", ""),
"logo": body.get("logo", ""), "tel": body.get("tel", "")}
```
## 风控侧怎么用这几个字段
`isLuhn` 为 `0` 表示卡号不能通过中国银联 Luhn 效验。这是一个可以进风控特征的值,但要注意它的第三态:空字符串表示该卡号不支持这项效验,不是「不合法」。
`card_digits` 给出的卡号长度可以配合 BIN 段做常识判断。同一 BIN 段不同卡号的长度可能不同——2026-09-15 实测同一 `622588` 段,一个卡号返回 `16`、另一个返回 `19`。
`card_bin` 为空的两种情况要分开看:不开 `needBin` 时字段不出现;开了但该 BIN 未收录时字段存在、值为空字符串。代码里统一用 `.get("card_bin", "")` 取值不会有问题。
## FAQ
**Q1:绑卡时每次都要开 `needBin=1` 吗?**
只在需要做卡号形态校验时开。如果绑卡环节只看行名、卡种和标志,不开也能拿到全部展示字段,响应更快。接口文档对 `needBin=1` 的说明是「返回这些信息将使得查询更加耗时」。
**Q2:`ret_code` 为 `-1` 就一定不能绑卡吗?**
要看具体形态。归属地未收录这一类,`ret_code` 是 `-1` 但 `bankName`、`tel`、`url`、`logo` 都有正常值,绑卡只需要行名和标志时可以用。卡号未收录那一类返回体里只有 `remark` 和 `ret_code`,没有可用数据。
**Q3:归属地能直接当用户的所在地用吗?**
不行。`area` 的粒度是「省/自治区 - 城市」,代表开户行所在地,不等于持卡人当前所在地。需要更细的位置信息要另配其他数据源。
**Q4:用户频繁改卡号会不会产生很多计费调用?**
会有,所以建议在失焦时先做本地预校验,通过再发请求,并对同一卡号做请求去重。失败调用不计费,但成功调用每次都按 5 厘计费。
**Q5:`cardType` 会返回哪些值?**
实测拿到过 `借记卡`。接口文档给出的示例值也是 `借记卡`,文档中没有给出完整的 `cardType` 枚举列表,做卡种规则时建议先按实际返回的取值采样,再定白名单。
**Q6:返回的 `cardNum` 能直接落库吗?**
返回体里的 `cardNum` 与请求入参一致、未做掩码。建议只落哈希与后 4 位,明文卡号按你所在业务的脱敏规范处理。
## 下一步阅读
- [银行卡归属地查询的 cardNum 与 needBin 怎么传](https://www.showapi.com/guides/bank-card-attribution-params-guide-30)——什么时候该开 `needBin`
- [银行卡归属地查询排错:三种失败形态怎么区分](https://www.showapi.com/guides/bank-card-attribution-error-codes-30)——响应形态与判定代码
- [银行卡归属地查询按次计费下怎么省调用](https://www.showapi.com/guides/bank-card-attribution-cache-cost-30)——卡号级缓存与并发限流
- **本系列共 10 篇**:查看[银行卡归属地查询指南总目录](https://www.showapi.com/guides/bank-card-attribution-guides-30)