
做零工和招聘那会儿地图这块差点被高德焊死。坐标解析、POI 搜索全调高德等业务做到别的城市发现高德配额天天被爆换百度要动十几个调用点谁都不想接这活。后来我在脚手架里抽了个qkl-channel模块把地图服务做成注册表 配置切换厂商是实现类切换只改一个配置项连日限额都按厂商按天计。这套东西落地后换厂商十分钟搞定再没求过人。注册表让 Spring 自动收集所有地图厂商核心就一句话把厂商实现扔进 Spring 容器注册表自动收集。写接口各厂商实现Spring 启动时把List注入构造器按 provider 名存进 Map。ComponentpublicclassMapClientRegistry{privatefinalMapclients;publicMapClientRegistry(Listlist){this.clientslist.stream().collect(Collectors.toMap(c-c.provider().toLowerCase(Locale.ROOT),Function.identity(),(a,b)-a));}publicMapClientrequire(Stringprovider){MapClientclientclients.get(providernull?:provider.toLowerCase(Locale.ROOT));if(clientnull){thrownewQklBizException(ErrorCode.MAP_PROVIDER_UNAVAILABLE,不支持的地图厂商: provider);}returnclient;}publicListproviders(){returnclients.keySet().stream().sorted().toList();}}好处立竿见影新接一家厂商 写一个实现类注册表一行不用改。Collectors.toMap的第三个参数(a, b) - a是保底真出现两个同 provider 的实现不至于启动就炸。门面统一 API切换只改配置业务方不关心你背后是高德还是百度它只认geocode(address)、searchPoi(keyword, city)这一套。门面服务里做开关、默认厂商、活跃客户端解析ServiceRequiredArgsConstructorpublicclassMapChannelService{privatefinalMapClientRegistryregistry;privatefinalSysConfigWriterconfigWriter;privatefinalSysConfigReaderconfigReader;privatefinalMapQuotaServicemapQuotaService;publicbooleanisEnabled(){StringenabledconfigReader.get(map,enabled,false);returntrue.equalsIgnoreCase(enabled)||1.equals(enabled);}publicStringcurrentProvider(){returnconfigReader.get(map,provider,amap);}publicMapClientactiveClient(){if(!isEnabled()){thrownewQklBizException(ErrorCode.MAP_MODULE_DISABLED,地图通道未开启);}returnregistry.require(currentProvider());}publicMapGeoResultgeocode(Stringaddress){if(!StringUtils.hasText(address)){thrownewQklBizException(ErrorCode.VALIDATION_ERROR,地址不能为空);}MapClientclientactiveClient();// 调用前先占额度超限直接抛异常不发起真实请求mapQuotaService.consume(currentProvider(),MapApiType.GEOCODE);returnclient.geocode(address);}}门面里每个接口都走校验 → 取活跃客户端 → 占额度 → 调厂商四步顺序不能反额度没占就发请求超限了钱照花校验放最前面空地址就别浪费一次配额。额度用完了怎么办换厂商就是一条配置日额度设计成0 不限是有原因的。地图厂商的免费额度天天变高德某天配额被活动流量打爆我的处理不是去后台调大限额那只是掩耳盗铃而是直接切厂商// 管理员后台一次操作把默认厂商从 amap 切到 tencentconfigWriter.upsert(map,provider,tencent,默认地图厂商,false,text);切换是无感的因为所有业务只依赖MapChannelService.geocode/poi这套门面厂商实现换了、调用方一行不用改。这也是我坚持把额度做成按厂商独立计量的原因每家厂商的配额账单独算切过去的那家配额是满的不会出现切完接着超限的尴尬。还有个细节channelView()里看板把三家 key、四个维度的配额、当前厂商全列出来运营在后台一眼看到高德 geocode 用了 87%、腾讯还没动该切谁一目了然。key 统一脱敏展示避免运维截图把密钥泄露到聊天群里。配置全部走SysConfigReader/Writer开关、默认厂商、三家 key 都存配置中心key 落库前脱敏upsert(..., true, text)那个 true 就是敏感标记。生产环境切厂商就是改一条配置连发布都不用。日限额Redis Hash 按厂商按天计量地图厂商免费额度有限超了要钱还限流。我在调用前先占用一次额度超限直接抛异常并把占的额度还回去publicvoidconsume(Stringprovider,MapApiTypeapi){StringpnormalizeProvider(provider);longlimitdailyLimit(api);StringkeydailyKey(p);// map:quota:20261010:amapLongusedredisTemplate.opsForHash().increment(key,api.code(),1L);touchExpire(key);if(limit0used!nullusedlimit){redisTemplate.opsForHash().increment(key,api.code(),-1L);// 回滚thrownewQklBizException(ErrorCode.MAP_QUOTA_EXCEEDED,api.label()今日调用额度已用尽限额 limit 次);}}接口维度用枚举收敛配置 key 跟枚举绑定想加维度就加一个枚举值publicenumMapApiType{GEOCODE(geocode,地理编码,quota.geocodeDaily),REGEOCODE(regeocode,逆地理编码,quota.regeocodeDaily),POI(poi,POI搜索,quota.poiDaily),PING(ping,连通测试,quota.pingDaily);// code/label/configKey 构造器 getter}0代表不限负数不进配置。Redis Hash 按租户:厂商:自然日建 key凌晨自然过期key 统一加 TTL 防止长尾堆积。踩坑换了厂商坐标系也跟着换了有段时间 POI 搜索结果在小程序地图上整体偏了 200 米排查了两天。现象H5 端搜索南康家具城落地 marker 跟实际位置差一条街。排查过程先怀疑是缓存清了没用再把高德和百度的原始返回打出来对比发现两家返回的经纬度不一样而前端拿百度坐标直接往高德地图上画。定位思路高德返回 GCJ-02百度返回 BD-09两个坐标系差着几百米前端只改了个 provider 字符串坐标却没做转换。最终解决SPI 实现里各厂商返回统一坐标系百度的客户端内部把 BD-09 转成 GCJ-02 再吐给上层前端永远只认一种坐标。publicclassBaiduMapClientimplementsMapClient{OverridepublicStringprovider(){returnbaidu;}OverridepublicMapGeoResultgeocode(Stringaddress){MapGeoResultrawbaiduRest.geocode(address);// BD-09// 统一转成 GCJ-02上层无感知returnraw.withLngLat(bd09ToGcj02(raw.getLng(),raw.getLat()));}}顺带把连通测试也做了个ping维度配好新厂商先 ping 一下key 对不对、网络通不通一次验完。接新厂商的完整动作有人问注册表这么好接一家新的要几步就三步// 1. 实现 SPI 接口provider() 返回唯一标识publicclassMapboxMapClientimplementsMapClient{OverridepublicStringprovider(){returnmapbox;}// geocode / searchPoi / ping 各自实现}第二配置文件里加mapbox.key和配额项quota.geocodeDaily等后台saveChannel接口顺手把新字段透传。第三把默认厂商临时切到 mapbox跑一遍ping和一条真实geocode确认坐标、配额都正常再切回原厂商灰度观察。整个过程不碰任何业务代码这就是注册表模式的底气。还有个容易忽略的点注册表用provider()做 key 时统一小写配toLowerCase(Locale.ROOT)别用默认 locale——土耳其语环境里TENCENT.toLowerCase()会带点变成tenc ent这种鬼样子线上必翻车。可以直接抄走的清单接口隔离业务只依赖MapClient门面厂商是实现细节。注册表收集List构造器注入 provider()做 key新增厂商零改动。配置驱动开关、默认厂商、各厂 key 进配置中心切换不发布。额度前置消费Redis Hashincrement占用、超限回滚抛异常0表示不限。坐标统一各厂商实现内部做坐标系归一前端永远只处理一种坐标。维度枚举化新增计量维度只加枚举值不散落字符串 key。项目源码https://gitee.com/gzqkl/qkl-boot