技术博客
银行卡归属地查询按次计费下怎么省调用:缓存粒度怎么定与并发限流

银行卡归属地查询按次计费下怎么省调用:缓存粒度怎么定与并发限流

作者: 万维易源
2026-09-15
银行卡归属地查询成本优化缓存设计Redis令牌桶限流
# 银行卡归属地查询按次计费下怎么省调用:缓存粒度怎么定与并发限流 > 接口:银行卡归属地查询(apiCode=30)· 接入点:`30-7` · 计费:5 厘/次,查询失败不计费 · 并发上限:10 次/秒 · 适用人群:已接入、要控制调用成本的开发者 · 阅读时间:约 8 分钟 > 最后实测核对:2026-09-15 一句话结论:银行卡归属地查询按次计费,单价 5 厘;想压成本,缓存 key 要用**完整卡号**而不是卡号前 6 位 BIN——2026-09-15 实测同一 BIN 段(`622848`)的两个卡号分别返回 `江苏 - 苏州` 和 `广东 - 江门`,按 BIN 缓存会返回错归属地。 ## 计费口径 | 项 | 值 | |------|------| | 计费单位 | 按次 | | 单价 | 5 厘/次(0.005 元) | | 失败计费 | 不收费。实测三种失败形态的 `showapi_fee_num` 都是 `0` | | 专用资源包 | 仅适用于本接口;50 元档对应本接入点 1 万次 | | 规格档位 | 0 元、50 元、450 元、2000 元、6000 元 | | 有效期 | 一年(接口页表述为 12 个月),自动续期,不限购 | | 通用资源包 | 可替代专用资源包调用本接口,按各接入点单价扣费 | | 并发上限 | 10 次/秒 | `showapi_fee_num` 是每次响应里都会带的字段,可以直接用它做调用量的实时核对,不用另建计数表。 ## 成本先算清楚,再决定要不要缓存 单位成本 0.005 元,日调用量 N 的方法很简单: ``` 日成本 = N × 0.005 元 ``` 缓存带来的节省取决于**卡号重复率** r(同一张卡号在缓存有效期内被重复查询的比例): ``` 缓存后日成本 = N × (1 - r) × 0.005 元 ``` 两个例子说明这个模型怎么用: - 绑卡场景每张卡号基本只查一次,r 接近 0,缓存的收益主要来自「同一用户反复重试同一张卡」,价值有限。 - 对账 / 代付场景同一批卡号每天或每月重复核对,r 可以很高,缓存收益明显。 失败结果不需为成本做缓存——失败本来就不计费,缓存它省的是响应时间和并发占用。 ## 缓存粒度:为什么不能按 BIN 前缀 很多卡号类接口的银行信息由 BIN(前 6 位)决定,按 BIN 缓存看起来很省。这个接口不能这么用。 2026-09-15 实测,两组同 BIN 段的不同卡号: | 卡号 | `card_bin` | `bankName` | `formatBankName` | `area` | `card_digits` | |------|-----------|-----------|------------------|--------|---------------| | `6228480402564890018` | `622848` | 中国农业银行 | 农业银行 | **江苏 - 苏州** | `19` | | `6228480000000000000` | `622848` | 农业银行 | 农业银行 | **广东 - 江门** | `19` | | `6225880000000000000` | (空字符串) | 招商银行 | 招商银行 | 广东 - 深圳 | `19` | 三点结论: - **归属地是卡号级的**。同一 BIN 段的前两个卡号,一个在江苏苏州、一个在广东江门。按 BIN 缓存会把后一张卡的归属地错标成苏州。 - **`card_digits` 也是卡号级的**。同一 BIN 段(`622588`)的另一个 16 位卡号实测返回 `card_digits: "16"`,而不是上表里的 `19`。 - **`bankName` 在同一家银行上写法不统一**(一次 `中国农业银行`、一次 `农业银行`),而 `formatBankName` 两次都是 `农业银行`。做字典映射和缓存比对用 `formatBankName`。 所以缓存 key 用完整卡号。如果不想让原始卡号进入缓存键,用带盐的哈希: ```python import hashlib SALT = "a-long-random-salt-stored-in-env" def cache_key(card_num: str) -> str: """缓存键:用带盐哈希代替明文卡号。""" return "bca:" + hashlib.sha256((SALT + card_num).encode()).hexdigest() ``` ## Redis 缓存设计 ```python import json import hashlib import requests import redis APPKEY = "YOUR_APPKEY" API_URL = "https://route.showapi.com/30-7" SALT = "a-long-random-salt-stored-in-env" CACHE_TTL = 30 * 24 * 3600 # 数据每年不定期更新,缓存 30 天 r = redis.Redis(host="127.0.0.1", port=6379, decode_responses=True) def _key(card_num: str) -> str: return "bca:" + hashlib.sha256((SALT + card_num).encode()).hexdigest() def get_bank_info(card_num: str, need_bin: bool = False) -> dict: key = _key(card_num) cached = r.get(key) if cached: return json.loads(cached) params = {"appKey": APPKEY, "cardNum": card_num} if need_bin: params["needBin"] = "1" resp = requests.get(API_URL, params=params, timeout=30) resp.raise_for_status() data = resp.json() if data.get("showapi_res_code") != 0: raise RuntimeError(f'请求失败:{data.get("showapi_res_error")}') body = data.get("showapi_res_body") or {} if str(body.get("ret_code")) != "0": # 失败不计费,不写缓存,避免把「当时未收录」的结果固定 30 天 return {"ok": False, "reason": body.get("remark") or "未查到归属地"} result = { "ok": True, "area": body.get("area", ""), "bank": body.get("formatBankName") or body.get("bankName", ""), "bank_raw": body.get("bankName", ""), "brand": body.get("brand", ""), "card_type": body.get("cardType", ""), "tel": body.get("tel", ""), "url": body.get("url", ""), "logo": body.get("logo", ""), } r.setex(key, CACHE_TTL, json.dumps(result, ensure_ascii=False)) return result ``` 两个设计点值得说明。 **失败结果不写缓存。** 接口文档写明数据「每年不定期更新」,未收录是暂时的。把失败写进 30 天缓存,等于把一次瞬时状态固定下来。失败本来也不计费,不缓存没有成本代价。 **缓存里只存需要的字段。** 返回体里的 `cardNum` 与请求入参一致,缓存整包等于把明文卡号又存了一份。上面这段只存展示与判断要用的字段。 ### 缓存有效期怎么定 数据更新频率是「每年不定期更新」,没有固定的刷新时点。30 天是一个折中值。如果你更在意归属地的时效性,把 TTL 压到 7 天;如果你更在意成本,可以拉到 90 天。两种情况都不需要为更新做额外的失效逻辑。 ## 10 次/秒并发下的限流 并发上限是 10 次/秒。批量任务不加控制会直接触顶,需要自己排队。 ```python import time import threading class RateLimiter: """令牌桶:capacity 个令牌,每秒补充 rate 个。参数按接口 10 次/秒的上限收紧到 8。""" def __init__(self, rate: float = 8.0, capacity: int = 8): self.rate = rate self.capacity = capacity self.tokens = capacity self.updated = time.monotonic() self.lock = threading.Lock() def acquire(self, timeout: float = 30.0) -> bool: deadline = time.monotonic() + timeout while True: with self.lock: now = time.monotonic() self.tokens = min(self.capacity, self.tokens + (now - self.updated) * self.rate) self.updated = now if self.tokens >= 1: self.tokens -= 1 return True if time.monotonic() >= deadline: return False time.sleep(0.05) limiter = RateLimiter() ``` 批量任务用「漏斗 + 重试」的组合:限流器管住发送速率,单个请求失败时用指数退避重试,但只对形态 A(请求构造问题)之外的情况重试——形态 A 改参数才能解决。 ```python def query_with_retry(card_num: str, max_retry: int = 3) -> dict: for attempt in range(max_retry): limiter.acquire() try: return get_bank_info(card_num) except requests.RequestException: # 网络层异常才退避重试;业务失败由 get_bank_info 内部返回 ok=False if attempt == max_retry - 1: raise time.sleep(0.5 * (2 ** attempt)) return {"ok": False, "reason": "重试次数用尽"} ``` ## 一天的实际调用量怎么估 如果存量卡号有 M 条,去重后剩下 U 条不同卡号,日任务只跑一遍、TTL 30 天,那么: - 首次全量成本 = U × 0.005 元 - 后续 30 天内重复跑的成本 ≈ 0(命中缓存) - 30 天后重新回源 = U × 0.005 元 按 10 次/秒的上限,U 条卡号的首次全量至少需要 `U / 10` 秒。U = 10 万时,最快约 2.8 小时,需要留出任务时间窗。 ## FAQ **Q1:失败调用扣费吗?** 不扣。2026-09-15 实测参数缺失、卡号未收录、归属地未收录三种失败形态,`showapi_fee_num` 都是 `0`。 **Q2:同一张卡号重复查询,结果会变吗?** 2026-09-15 对同一卡号 `6228480402564890018` 做了两次调用(一次带 `needBin`、一次不带),`area`、`bankName`、`formatBankName`、`brand`、`cardType`、`tel`、`url`、`logo`、`simpleCode` 的取值完全一致。缓存同一卡号的结果是可行的。 **Q3:能按卡号前 6 位缓存吗?** 不能。实测同一 BIN 段 `622848` 的两个卡号分别返回 `江苏 - 苏州` 与 `广东 - 江门`。归属地是卡号级数据,按 BIN 缓存会返回错值。 **Q4:缓存里能不能直接存返回体原文?** 技术上可以。返回体里的 `cardNum` 与请求入参一致,如果缓存介质没有加密,等于把卡号存了两份。按数据脱敏要求取舍。 **Q5:专用资源包和通用资源包该买哪个?** 专用资源包只适用于本接口;通用资源包可以调含本接口在内的全站付费接口,按各接入点单价扣费。只调这一个接口、量大,看专用资源包;同时用多个接口,通用资源包更省事。 **Q6:并发超过 10 次/秒会怎样?** 接口的并发量上限是 10 次/秒。批量任务建议把发送速率压到 8 次/秒左右留出余量,超出的请求自己排队。 ## 下一步阅读 - [银行卡归属地查询排错:三种失败形态怎么区分](https://www.showapi.com/guides/bank-card-attribution-error-codes-30)——哪些失败值得重试、哪些重试也没用 - [银行卡归属地查询返回字段逐个说清](https://www.showapi.com/guides/bank-card-attribution-response-fields-30)——缓存里该存哪些字段 - [代付与对账系统批量核对开户行](https://www.showapi.com/guides/bank-card-attribution-payout-reconcile-30)——批量任务的实际节奏安排 - **本系列共 10 篇**:查看[银行卡归属地查询指南总目录](https://www.showapi.com/guides/bank-card-attribution-guides-30)