Repository Wiki
jason5ng32/MyIP

地理位置、时区与 MaxMind 数据集成

本页说明 MyIP 如何将本地 MaxMind City/ASN 数据库接入 IP 查询 API,以及地理名称本地化、时区中间件挂载、数据就绪检查和读者热替换之间的关系。重点是区分数据库原始查询结果、服务层统一输出与 HTTP 层附加处理。

目的与范围

覆盖 /api/maxmind 的请求链路、lookupMaxMind 与 lookupCountryRange、双数据库生命周期、语言选择、错误语义和可验证的运维行为。其他 IP 数据源仅用于说明共享时区入口,不展开各供应商协议;ASN 拓扑、Radar 国家聚合、全站安全策略和通用数据集调度应由相应专题承载。

**证据边界:**本页核实了核心查询服务、处理器、请求守卫及后端路由片段。时区中间件的转换算法、输出字段,数据集下载器的更新周期、重试和文件替换策略,以及相关测试,未在本次读取的源码中核实;下文不会将这些内容当作实现保证。

概述

MaxMind 查询不在每次请求时访问外部地理位置 API,而是调用已打开的 City 与 ASN reader:City 提供城市、地区、国家和经纬度,ASN 提供自治系统编号和组织名。服务将两条记录转换为统一对象,由 HTTP 处理器交给 res.json。

这里有三个不同层次的“可用”:

  • **数据库就绪:**City 和 ASN reader 都存在。
  • **记录可用:**某个 IP 在数据库中能找到记录;未命中不等于数据库未就绪。
  • **字段可用:**命中记录仍可能缺少名称、坐标或国家代码;格式化逻辑按字段处理。

语言归一化放在服务层,而非处理器中,因此直接调用服务的消费者也会获得一致的语言选择行为。参见 maxmind-service.js、maxmind-service.js。

架构与职责

Loading diagram...

Sources:

图中路由节点表示中间件注册顺序,不表示响应包装函数的内部执行顺序。needsMaxMind 由 requireOfflineData([isMaxMindReady]) 构造;服务内部仍再次检查就绪状态,避免非 HTTP 调用绕过保护。

全站 /api 链路还预先设置 Cache-Control: no-store 并挂载 requireReferer。MaxMind 路由随后声明一天的 cacheable 策略;缓存命中规则及最终响应头细节需要查阅缓存中间件实现,不能仅从注册推断。参见 backend-server.js。

请求与地理位置输出

1. 输入守卫先于本地查询

requirePublicIP(paramName = 'ip') 依次检查参数存在性、IP 格式和公网可用性,分别返回不同的 400 错误。通过后才进入数据库就绪门禁、时区和缓存中间件。

处理器直接读取 req.query.ip 和 req.query.lang;没有把缺失 IP 自动替换成访问者 IP,也没有在处理器内重复地址校验。全局 Referer 守卫可能更早返回 403。参见 guards.js、maxmind.js。

2. City/ASN 查询与格式化

lookupMaxMind(ip, lang) 首先调用 isMaxMindReady()。未就绪时创建 Error('MaxMind database is not ready'),附加 statusCode = 503 后抛出;就绪时依次执行 cityLookup.get(ip) 和 asnLookup.get(ip),再调用 formatMaxMindResult。

两个查询是同步调用,不是每次请求重新打开数据库,也不是 City 命中后才查 ASN。即便 City 无记录,仍会读取 ASN。参见 maxmind-service.js。

服务层字段来源/规则缺失行为
ip原始调用参数原样返回
citycity.city.names本地化名称、英语或 N/A
regioncity.subdivisions[0].names仅第一层 subdivision;否则 N/A
country、country_codecity.country.iso_codecountry 对象不存在时为 N/A
country_namecity.country.names本地化名称、英语或 N/A
latitude、longitudecity.location 内同名字段location 不存在时为 N/A
asn真值编号加 AS 前缀假值时为 N/A
orgautonomous_system_organization假值时为 N/A

**字段级陷阱:**实现只对部分父对象做存在性判断。若 country 存在却没有 iso_code,或者 location 存在却缺少坐标,服务对象中的相应值可能是 undefined,而非 N/A。不能承诺所有字段总有字符串或数值;HTTP 最终序列化还经过中间件。参见 maxmind-service.js。

3. 名称本地化与回退

MaxMind 名称支持集合是 de、en、es、fr、ja、pt-BR、ru、zh-CN,独立于前端 UI 的语言集合。新增 UI 语言不会让数据库自动增加翻译。

normalizeLang 对非字符串直接返回 en;对字符串先 trim(),再由 matchLocale 在上述集合内选择,匹配失败回退 en。服务注释规定精确匹配、基础语言、同语言族的选择顺序,并明确 zh-TW 可选取 zh-CN。记录取名再经过第二层回退:names[lang] || names.en || 'N/A'。因此“请求语言被支持”不保证某条记录具有该语言名称。参见 maxmind-service.js、maxmind-service.js。

4. 时区集成边界

/api/ipinfo、/api/ipapicom、/api/ipsb、/api/ipapiis、/api/ip2location 与 /api/maxmind 都注册 withTimeZone()。这确认时区处理是多个地理位置入口共享的接入点,而不是 MaxMind 处理器专属逻辑。

同时,formatMaxMindResult 没有输出 timezone、UTC offset 或当地时间字段。**不能把 MaxMind 原始记录可能携带的时区信息等同于当前服务实际暴露的字段。**时区是否由坐标推导、如何处理夏令时、错误如何回退、最终响应是否新增字段,均未在已读取源码中确认。参见 backend-server.js、maxmind-service.js。

数据库生命周期与并发一致性

路径和就绪条件

数据库目录由模块所在位置解析为 common/maxmind-db,文件名固定为 GeoLite2-City.mmdb 与 GeoLite2-ASN.mmdb。getMaxMindDbPaths() 返回 { dbDir, cityDbPath, asnDbPath };isMaxMindReady() 返回 Boolean(cityLookup && asnLookup)。

就绪判断不验证文件新鲜度、不检查数据库版本,也不代表某个 IP 一定能命中。持久化介质是 MMDB 文件;已读服务中没有关系数据库写入或查询结果持久化逻辑。参见 maxmind-service.js。

双 reader 加载与热替换

Loading diagram...

Source: maxmind-service.js

Promise.all 等待两份数据库都成功打开,随后在同一个同步回调中连续更新两个模块变量,中间没有 await。这避免在正常单进程事件循环查询中切换到一半新、一半旧的 reader。

reloadPromise 合并同一模块实例内的并发加载,降低重复打开开销;它不是跨进程锁,也不控制下载过程。失败时旧 reader 保留:已有可用数据可以继续服务,首次加载失败则仍未就绪。finally 清空标记,让下一次调用能够再次尝试;这里没有自动重试循环或超时。

后端同时导入 bootstrapDatasets、startDatasetScheduler、watchDatasets 及 datasets,表明数据集管理存在独立接入层,但导入本身不足以证明调度频率、原子下载或监控行为。本页不为这些未读取实现提供配置承诺。参见 backend-server.js。

轻量国家范围查询

lookupCountryRange(ip) 为只需要国家代码和网络前缀长度的调用场景提供单独路径。它只要求 City reader,不要求 ASN reader,也不执行完整响应格式化。源码注释指出 Radar 的 BGP 前缀视图会多次使用这种查询,以网络为单位遍历范围。

返回值有重要区分:

  • null:City reader 未加载,或者底层查询抛错。
  • { country: null, prefixLength }:查询执行成功,但记录没有国家代码。
  • { country: <ISO code>, prefixLength }:查询成功并获得国家归属。

调用者不应把前两种情况当作相同的完整答案。该函数捕获异常后直接返回 null,不会像 HTTP 查询那样抛出 503。参见 maxmind-service.js。

源码使用示例

以下片段均摘自真实实现,展示接入方式和关键行为,不是虚构的调用脚本。

路由接入

javascript
app.get('/api/maxmind', requirePublicIP(), needsMaxMind, withTimeZone(), cacheable(ONE_DAY_CACHE), maxmindHandler);

Source: backend-server.js

该顺序确保 IP 校验与数据就绪门禁位于地理响应处理之前;一天策略使用 24 * 60 * 60 秒。

处理器调用与异常映射

javascript
1 try { 2 res.json(lookupMaxMind(ip, lang)); 3 } catch (e) { 4 logger.error({ err: e, ip, lang }, 'maxmind handler failed'); 5 res.status(e.statusCode || 500).json({ error: e.message }); 6 }

Source: maxmind.js

异常日志携带 IP 和原始语言标签;状态码优先使用异常的 statusCode,否则为 500。

语言归一化

javascript
1export const normalizeLang = (tag) => { 2 if (typeof tag !== 'string') return 'en'; 3 return matchLocale(tag.trim(), SUPPORTED_LANGS) ?? 'en'; 4};

Source: maxmind-service.js

语言选择属于数据服务契约,不需要每个处理器重复实现。

双数据库并行打开

javascript
1export async function openMaxMindReaders(dbPaths = getMaxMindDbPaths()) { 2 const [city, asn] = await Promise.all([ 3 maxmind.open(dbPaths.cityDbPath), 4 maxmind.open(dbPaths.asnDbPath), 5 ]); 6 7 return { city, asn }; 8}

Source: maxmind-service.js

该函数仅返回 reader,不更新当前服务状态。只有 reloadMaxMindDatabases 成功后的回调会发布新 reader。

国家范围查询

javascript
1export const lookupCountryRange = (ip) => { 2 if (!cityLookup) return null; 3 try { 4 const [record, prefixLength] = cityLookup.getWithPrefixLength(ip); 5 return { country: record?.country?.iso_code || null, prefixLength }; 6 } catch { 7 return null; 8 } 9};

Source: maxmind-service.js

配置与固定约定

选项/符号类型默认值/固定值作用
MAXMIND_DB_DIR路径字符串常量模块相对目录 ./maxmind-db默认 MMDB 目录,并非此服务中的环境变量
MAXMIND_CITY_DB字符串常量GeoLite2-City.mmdbCity 文件名
MAXMIND_ASN_DB字符串常量GeoLite2-ASN.mmdbASN 文件名
SUPPORTED_LANGS字符串数组八种数据库名称语言不跟随 UI 语言注册自动扩展
查询参数 lang原始查询值服务归一化后兜底 en决定名称字段语言
openMaxMindReaders 的 dbPaths路径对象getMaxMindDbPaths()可为独立 reader 加载指定 City/ASN 路径
reloadMaxMindDatabases 的 reason参数,通常为字符串'manual'用于加载日志,不改变数据选择
ONE_DAY_CACHE数值,秒86400路由声明的缓存时长
LOG_HTTP环境变量字符串未设时关闭精确等于 'true' 时启用 /api 请求日志

上述常量和默认值分别来自 maxmind-service.js、backend-server.js、backend-server.js。数据集下载凭据的读取方式及更新参数未在已读取实现中确认,不能将其与 reader 服务参数混为一谈。

API 参考

HTTP:GET /api/maxmind

参数位置必填行为
ipQuery是必须通过格式及公网可用性校验
langQuery否原样传入服务,再匹配数据库语言集合

成功路径由处理器发送地理与 ASN 对象;字段来源见前文。共享时区中间件可能影响 HTTP 层输出,但本页没有确认其新增字段。接口没有在处理器中实现参数缺省 IP 探测或外部供应商回退。

服务导出函数

源码为 JavaScript,下面列出原始签名及代码可确认的结果,而非虚构 TypeScript 类型。

签名参数返回值错误语义
getMaxMindDbPaths()无{ dbDir, cityDbPath, asnDbPath }无显式抛错分支
isMaxMindReady()无Boolean无显式抛错分支
openMaxMindReaders(dbPaths = getMaxMindDbPaths())含 City/ASN 文件路径的对象Promise,兑现为 { city, asn }底层打开失败使 Promise 拒绝
reloadMaxMindDatabases(reason = 'manual')加载原因Promise,成功为 true记录并重新抛出打开错误
normalizeLang(tag)原始语言标签支持集合内的语言;兜底 en对非字符串显式回退
lookupMaxMind(ip, lang)IP、原始语言格式化地理/ASN 对象未就绪抛带 503 的 Error;查询异常向上传递
lookupCountryRange(ip)IP国家/前缀对象或 null捕获查询错误并返回 null

参见 maxmind-service.js。服务查询函数本身不调用 IP 校验器;非 HTTP 调用方不能依赖路由守卫替自己验证输入。

失败模式、边界与运维

情况可确认行为排查重点
Referer 不允许/缺失403;分别使用 Access denied/What are you doing?检查全局守卫,而非数据库
缺少 IP400,No IP address provided检查 Query 参数
IP 格式无效400,Invalid IP address检查格式校验
非公网 IP400,Not a public IP address不会进入 MaxMind 查询
服务 reader 未就绪lookupMaxMind 抛 503 错误两份 reader 都必须存在;路由另有门禁
底层查询异常处理器记录错误并返回异常状态或 500查看 maxmind handler failed 日志
某个 IP 未命中缺失记录转换为空对象后格式化不是 reader 加载故障
热重载失败记录 Failed to load MaxMind databases,保留旧 reader数据可用与数据新鲜度应分别监控
国家范围查询异常返回 null,不向调用者抛出调用者需要保留“不完整答案”语义

来源:guards.js、maxmind.js、maxmind-service.js。

性能与一致性

  • 查询路径复用模块级 reader,没有逐请求文件打开。完整查询执行两次同步 .get;国家范围接口只调用一次 getWithPrefixLength。
  • 数据加载并行执行,但成功发布以两份数据库都打开为前提。加载中的请求仍使用旧 reader;首次加载期间没有旧数据时,完整查询不可用。
  • 新 reader 发布代码没有主动清理 HTTP 缓存。因此不能把“完成热重载”理解为“所有缓存客户端立即读取新数据”。具体缓存行为仍取决于缓存中间件与部署环境。
  • reader 状态与重载合并都是模块级变量;已读实现没有跨 worker 同步机制,也没有显式的 reader 关闭调用。
  • /api/maxmind 被延迟中间件排除,但未被全局请求限流器排除。限流器仅在 SECURITY_RATE_LIMIT 非零时挂载;不能把本地数据库查询视为无全局限流。参见 backend-server.js。

扩展建议与验证边界

新增字段应首先检查 formatMaxMindResult 的统一响应契约,明确缺省值,再核对共享时区处理。扩展语言时应依据 MaxMind 数据自身的名称覆盖范围,而不是只修改 UI 语言表。需要指定替代数据库路径时,注意 openMaxMindReaders 只创建 reader;当前 reloadMaxMindDatabases 没有路径参数。

这些是基于现有边界的工程建议,不代表已经实现的插件接口。当前读取范围没有测试代码,不能宣称下列行为已被自动化测试覆盖;建议重点验证:并发重载合并、单库打开失败时保留旧数据、初次加载失败的 503、语言族回退、记录未命中以及父对象存在但字段缺失。

相关链接

其他供应商协议、通用离线数据集调度和 ASN/Radar 聚合应分别查阅对应专题;本页只定义它们与地理查询服务的边界,不替代这些专题。