免费 IP 地址查询 API 接入实战_街道级归属接口调用与封装_ip geolocation api
很多项目里都会存 IP,但真正把 IP 用起来,往往不是“查出城市”就够了。对我这次的业务来说,我需要的不只是一个归属地接口,而是一份可以直接进入风控、日志分析和区域运营的数据结构。
我最后接的是 IP 地址查询(街道级)接口。相比按文档把二十多个字段逐项过一遍,我更想写清楚它在项目里怎么落地更顺:我拿它解决什么问题、我为什么选它、以及我怎么把返回结果整理成业务层真正能用的数据。
这类街道级数据我主要拿来做什么
我这次接这个接口,核心不是为了“知道用户在哪个城市”,而是为了把原本很粗的 IP 信息,变成一份可以直接参与业务判断的数据。
最直接的一个场景是日志地理回填。以前日志里只有 IP,本地分析时最多补到省市,做热点区域分布或者门店覆盖分析时粒度还是偏粗。接了这类街道级接口后,省、市、区、街道四级结构能直接补齐,后面的 BI 分析就容易很多
第二个场景是风控归因。很多请求在业务层看起来都一样,但只要把代理概率、风险等级、代理类型和真实用户率这些字段补进来,很多异常访问的画像就会清楚很多。我这次不是把它当成单独的风控系统,而是把它作为一层风险信号,参与登录、下单和活动请求的判断。
第三个场景是区域匹配。像配送范围、门店服务区、区域合规访问控制这类功能,本质上都需要更细的地理粒度。街道级数据一旦有了,很多原本只能做“城市级兜底”的判断,就能往前再细一步。
我为什么直接选了这个接口
接口地址: https://v1.apizero.cn/api/ip-pro
我最后选它,主要有三个原因。
第一,信息密度够高。普通 IP 库往往到国家、省、市这一级就差不多了,但我这次更在意的是街道级归属,以及后面能不能顺手接到风控和区域分析里。这个接口把四级行政区、经纬度、邮编、区号、运营商、时区、代理识别和风险评分都放在同一份返回里,后端封装时很省事。
第二,适合做统一入口。对工程来说,我不希望“地理信息查询”和“代理风险判断”分成两套接口来接。这个接口的好处是,定位信息和风险信息可以一次请求拿全,后端很容易整理成一个统一 DTO,后面不管是给风控模块还是 BI 模块,消费方式都一致。
第三,比较适合做服务层收敛。像这种字段比较多的接口,如果直接让前端或业务代码去吃原始结果,后面一定会越用越散。我更喜欢先在后端封一层,把字段分成“地理归属”“网络运营”“风险信号”三组,之后谁来调用都比较顺。
我的接入做法
我没有把第三方接口直接暴露给业务层,而是在内部服务里包了一层很轻的查询函数。这样做的重点不是复杂化,而是让所有调用方拿到相同结构的数据,后面不管我换字段名、补缓存还是扩展别的 IP 源,都不会影响上层代码。
我的服务层主要做四件事:
- 接收 IP 参数
- 请求第三方接口
- 按业务分组整理字段
- 返回统一结果
先看我在项目里采用的调用链路:
这个流程不长,但已经把职责切开了。上层业务只关心“这个 IP 的位置和风险是什么”,服务层负责把第三方返回整理成统一结构。这样做之后,登录风控、订单校验、日志回填和 BI 作业都能复用同一个入口。
下面这段代码,就是我在首版里保留的核心封装:
importaxiosfrom"axios";constclient=axios.create({baseURL:"https://v1.apizero.cn/api",timeout:5000,});exportasyncfunctionqueryIpPro(ip:string){try{const{data}=awaitclient.get("/ip-pro",{params:{ip},});if(data?.code!==0||!data?.data){thrownewError(data?.msg||"request failed");}constx=data.data;return{ip,location:{province:x.province||"",city:x.city||"",district:x.district||"",street:x.street||"",lat:x.latitude||"",lng:x.longitude||"",},network:{isp:x.isp||"",timezone:x.timezone||"",areaCode:x.area_code||"",postCode:x.post_code||"",},risk:{isProxy:!!x.is_proxy,proxyType:x.proxy_type||"",riskLevel:x.risk_level||"",riskScore:x.risk_score||0,},};}catch(err){console.error("query ip-pro failed:",err);returnnull;}}queryIpPro("8.8.8.8").then((res)=>console.log(res));这段代码我刻意控制得比较短。它已经包含了直接可跑的请求函数、超时设置、错误处理和调用示例。真实项目里我会再补一层缓存和监控,但首版接入时,先把输出结构定清楚更重要。
返回结果我只保留了三组核心信息
这个接口的字段很多,但我没有选择把所有字段原样透传给业务层。我的做法是先反推使用场景,再决定保留哪些信息。这样后面不管是落库还是提供给前端接口,结构都会清楚很多。
我最终保留的是三组信息。
第一组是地理归属,包括省、市、区、街道、经纬度。这组字段主要给区域分析、LBS 匹配和日志回填用,属于最基础的一层。
第二组是网络信息,包括运营商、时区、区号、邮编。这组字段不一定每次都用,但只要遇到区域服务判断、客服工单定位或者跨时区行为分析,就会很有价值。
第三组是风险信息,包括是否代理、代理类型、风险等级和风险评分。这组字段最适合挂到风控链路上,作为额外信号参与判断,而不是单独决定是否拦截。
我没有把所有返回字段都写进主结构里,而是先把常用字段收紧。这么做的好处很直接:业务代码不会被二十多个原始字段淹没,接口层也更容易长期维护。
两个做完之后会明显顺手的小细节
我把字段按业务语义分组了
如果把第三方返回直接铺平成一个大对象,短期看起来省事,长期其实很难维护。我的做法是直接拆成location、network、risk三组,这样不同调用方一眼就知道该取哪部分数据。
这种分组对后续扩展也很友好。以后如果我想补更多运营商字段、更多风险标签或者更细的区域数据,只需要往对应分组里扩,不会把主结构越写越乱。
我把风险信号当辅助判断,不做单点决策
这个接口里有代理概率、风险等级、真实用户率这类信息,工程上确实很好用。但我的做法不是直接拿某一个字段做拦截,而是把它们当成附加信号,和设备信息、账号行为、访问频率放在一起看。
这样做的好处是判断会更稳。IP 风险信息很适合帮助你做优先级排序、人工审核加权和日志回看,但单独拿来做最终裁决,业务上通常会偏硬。放在组合判断里,价值会更高。
这些场景接上去最容易见到效果
如果你做的是登录风控、活动防刷、广告投放、区域运营、门店分布分析、配送区域匹配或者日志地理回填,这类接口接上去通常很快就能见到效果。因为它补的不是单一字段,而是一整层原本缺失的地理和风险信息。
不过它也有边界。我的理解是,它很适合做“判断辅助”和“区域增强”,但不适合单独承担完整风控决策。因为一个请求是不是高风险,通常还要结合账号历史、设备环境和行为链路一起看。
另外,如果你的业务只需要知道国家或城市级归属,那这种街道级接口就不一定是必要项。它更适合那些已经明确需要更细粒度定位,或者已经在做风控、LBS 和区域分析的项目。用在这些地方,它的价值会非常直接。
4. 后端封装代码
importaxiosfrom"axios";constclient=axios.create({baseURL:"https://v1.apizero.cn/api",timeout:5000,});exportasyncfunctiongetIpProfile(ip:string){try{const{data}=awaitclient.get("/ip-pro",{params:{ip}});if(data?.code!==0||!data?.data){thrownewError(data?.msg||"request failed");}constx=data.data;return{ip,location:{province:x.province||"",city:x.city||"",district:x.district||"",street:x.street||"",},risk:{isProxy:!!x.is_proxy,proxyType:x.proxy_type||"",riskLevel:x.risk_level||"",riskScore:x.risk_score||0,},};}catch(err){console.error("getIpProfile error:",err);returnnull;}}getIpProfile("8.8.8.8").then((res)=>console.log(res));