地理位置、时区与 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。
架构与职责
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 | 原始调用参数 | 原样返回 |
city | city.city.names | 本地化名称、英语或 N/A |
region | city.subdivisions[0].names | 仅第一层 subdivision;否则 N/A |
country、country_code | city.country.iso_code | country 对象不存在时为 N/A |
country_name | city.country.names | 本地化名称、英语或 N/A |
latitude、longitude | city.location 内同名字段 | location 不存在时为 N/A |
asn | 真值编号加 AS 前缀 | 假值时为 N/A |
org | autonomous_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 加载与热替换
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。
源码使用示例
以下片段均摘自真实实现,展示接入方式和关键行为,不是虚构的调用脚本。
路由接入
app.get('/api/maxmind', requirePublicIP(), needsMaxMind, withTimeZone(), cacheable(ONE_DAY_CACHE), maxmindHandler);Source: backend-server.js
该顺序确保 IP 校验与数据就绪门禁位于地理响应处理之前;一天策略使用 24 * 60 * 60 秒。
处理器调用与异常映射
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。
语言归一化
1export const normalizeLang = (tag) => {
2 if (typeof tag !== 'string') return 'en';
3 return matchLocale(tag.trim(), SUPPORTED_LANGS) ?? 'en';
4};Source: maxmind-service.js
语言选择属于数据服务契约,不需要每个处理器重复实现。
双数据库并行打开
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。
国家范围查询
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.mmdb | City 文件名 |
MAXMIND_ASN_DB | 字符串常量 | GeoLite2-ASN.mmdb | ASN 文件名 |
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
| 参数 | 位置 | 必填 | 行为 |
|---|---|---|---|
ip | Query | 是 | 必须通过格式及公网可用性校验 |
lang | Query | 否 | 原样传入服务,再匹配数据库语言集合 |
成功路径由处理器发送地理与 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? | 检查全局守卫,而非数据库 |
| 缺少 IP | 400,No IP address provided | 检查 Query 参数 |
| IP 格式无效 | 400,Invalid IP address | 检查格式校验 |
| 非公网 IP | 400,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、语言族回退、记录未命中以及父对象存在但字段缺失。
相关链接
- HTTP 接入与共享时区挂载:backend-server.js。
- 数据查询契约与语言集合:maxmind-service.js。
- 热重载和国家范围查询:maxmind-service.js。
其他供应商协议、通用离线数据集调度和 ASN/Radar 聚合应分别查阅对应专题;本页只定义它们与地理查询服务的边界,不替代这些专题。