DEPLOY
prerequisites
前置条件
当前部署方案为单个 Node 进程配合持久化数据目录。
- 在目标机器上安装 Node.js 20+ 和 pnpm 10+。
- 将代码部署到一个具有写权限的应用目录。
- 为 ./data/ 目录设置持久化存储。
build
构建项目
从 GitHub 上下载 Open CRH Tracker 代码并构建
如果你的网络环境不佳,请你在下载代码前先通过
git config --global http.proxy <proxy_address>
git config --global https.proxy <proxy_address>设置 Git 的网络代理,或者启用 TUN 模式。
在确保网络环境没有问题后,请执行以下代码以从 GitHub 上下载项目。
git clone https://github.com/lihugang/OpenCRHTracker.git --depth=1
cd OpenCRHTracker随后,你需要通过 pnpm 安装程序需要的依赖,执行:
pnpm install安装成功后,执行
pnpm build以生成构建。
config
config.json 配置指南
服务器启动时会读取 data/config.json 下的配置文件,并校验配置合法性。
配置文件加载规则
- 开发环境优先读取 data/config.dev.json,找不到时回退到 data/config.json。
- 生产环境优先读取 data/config.prod.json,找不到时回退到 data/config.json。
- 启动时会校验配置文件合法性,字段缺失、时间格式错误、前缀范围重叠、分页上限非法等问题都会直接阻止服务启动。
部署前先确定当前环境实际会读取哪份配置文件,再进行修改。生产环境的配置修改不会热更新,修改后需要重启 Node 进程。
完整示例:data/config.json
{
"$schema": "../assets/json/configScheme.json",
"spider": {
"userAgent": "Mozilla/5.0 (iPhone; CPU iPhone OS 14_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148 MicroMessenger/7.0.17(0x17001126) NetType/WIFI Language/zh_CN",
"params": {
"eKey": "OpenCRHTracker",
"jsonpCallback": "OpenCRHTracker",
"routeProbeCarCode": "CR400AF-C-2214"
},
"rateLimit": {
"query": {
"minIntervalMs": 1500
},
"search": {
"minIntervalMs": 8000
}
},
"scheduleProbe": {
"dailyTimeHHmm": "0000",
"retryAttempts": 3,
"maxBatchSize": 200,
"checkpointFlushEvery": 20,
"refresh": {
"batchSize": 20,
"ttlHours": 24,
"generateIntervalHours": 24
},
"probe": {
"defaultRetry": 5,
"overlapRetryDelaySeconds": 3600
},
"coupling": {
"statusResetTimeHHmm": "0000",
"detectDelaySeconds": 900,
"detectCooldownSeconds": 3600
},
"prefixRules": [
{
"prefix": "G",
"minNo": 1,
"maxNo": 9999
},
{
"prefix": "D",
"minNo": 1,
"maxNo": 9999
},
{
"prefix": "C",
"minNo": 1,
"maxNo": 9999
},
{
"prefix": "S",
"minNo": 5500,
"maxNo": 5600
}
]
}
},
"data": {
"assets": {
"EMUList": {
"file": "data/emu_list.jsonl",
"provider": "https://storage.lihugang.top/open_crh_tracker/initialized_data/emu_list.jsonl",
"refresh": {
"enabled": true,
"refreshAt": "0000"
}
},
"QRCode": {
"file": "data/qrcode.jsonl",
"provider": "https://storage.lihugang.top/open_crh_tracker/initialized_data/qrcode.jsonl",
"refresh": {
"enabled": true,
"refreshAt": "0000"
}
},
"schedule": {
"file": "data/schedule.json",
"provider": "https://storage.lihugang.top/open_crh_tracker/initialized_data/schedule.json"
}
},
"databases": {
"task": "data/task.db",
"EMUTracked": "data/emu.db",
"users": "data/users.db",
"feedback": "data/feedback.db"
},
"runtime": {
"adminTraffic": {
"file": "data/runtime/admin-traffic.json",
"flushIntervalMinutes": 30
},
"adminServerMetrics": {
"file": "data/runtime/admin-server-metrics.json",
"flushIntervalMinutes": 10,
"sampleIntervalSeconds": 60
},
"requestMetrics12306": {
"file": "data/runtime/12306-request-metrics.json",
"retentionDays": 3,
"flushIntervalMinutes": 10
}
}
},
"user": {
"saltLength": 16,
"apiKeyPrefixes": {
"webapp": "ocrh_webapp_",
"api": "ocrh_api_"
},
"apiKeyBytes": 24,
"apiKeyTtlSeconds": 2592000,
"apiKeyMaxLifetimeSeconds": 157680000,
"apiKeyNameLength": {
"minLength": 1,
"maxLength": 64
},
"adminUserIds": [
"admin-user-id"
],
"favorites": {
"maxEntries": 10
},
"pushSubscriptions": {
"maxDevices": 5,
"maxEventSubscriptions": 50,
"syncTimeoutSeconds": 30
},
"push": {
"vapidPublicKey": "replace-with-base64url-vapid-public-key",
"vapidPrivateKey": "replace-with-base64url-vapid-private-key",
"vapidEmail": "[email protected]"
},
"signKey": "replace-with-strong-random-secret",
"scrypt": {
"keyLength": 64,
"cost": 16384,
"blockSize": 8,
"parallelization": 1
}
},
"api": {
"versionPrefix": "/api/v1",
"apiKeyHeader": "authorization",
"authCookieName": "token",
"clientIpHeaders": [
"cf-connecting-ip",
"x-forwarded-for",
"x-real-ip"
],
"authRateLimit": {
"login": {
"maxRequests": 10,
"windowSeconds": 1800
},
"register": {
"maxRequests": 3,
"windowSeconds": 86400
}
},
"authCache": {
"userRecord": {
"maxEntries": 1024,
"defaultTtlSeconds": 1800
},
"apiKeyRecord": {
"maxEntries": 4096,
"defaultTtlSeconds": 21600
},
"userProfile": {
"maxEntries": 256,
"defaultTtlSeconds": 21600
}
},
"payload": {
"maxStringLength": 16384
},
"feedback": {
"validation": {
"createBody": {
"minLength": 2,
"maxLength": 12000
},
"replyBody": {
"minLength": 2,
"maxLength": 2000
},
"title": {
"minLength": 4,
"maxLength": 80
}
}
},
"headers": {
"remain": "x-api-remain",
"cost": "x-api-cost",
"retryAfter": "Retry-After"
},
"cache": {
"currentDayMaxAgeSeconds": 300,
"historicalMaxAgeSeconds": 31536000,
"searchIndexMaxAgeSeconds": 1800,
"sitemapMaxAgeSeconds": 86400,
"timetableMaxAgeSeconds": 21600
},
"pagination": {
"defaultLimit": 20,
"maxLimit": 200
},
"timestampUnit": "seconds",
"debug": {
"enableEchoError": true
},
"permissions": {
"anonymousScopes": [
"api.config.read",
"api.search.read",
"api.records.daily.read",
"api.history.train.read",
"api.history.emu.read",
"api.timetable.train.read",
"api.timetable.station.read",
"api.exports.daily.read",
"api.feedback.read",
"api.feedback.create"
],
"issuedKeyDefaultScopes": [
"api.config.read",
"api.search.read",
"api.records.daily.read",
"api.history.train.read",
"api.history.emu.read",
"api.timetable.train.read",
"api.timetable.station.read",
"api.exports.daily.read",
"api.feedback.read",
"api.feedback.create",
"api.feedback.reply",
"api.auth.me.read",
"api.auth.logout",
"api.auth.password.update",
"api.auth.api-keys.read",
"api.auth.api-keys.create",
"api.auth.api-keys.revoke",
"api.auth.favorites.read",
"api.auth.favorites.write",
"api.auth.subscriptions.read",
"api.auth.subscriptions.write"
],
"creatableKeyMaxScopes": [
"api.auth.me.read",
"api.records.daily.read",
"api.history.train.read",
"api.history.emu.read",
"api.timetable.train.read",
"api.timetable.station.read",
"api.exports.daily.read"
]
}
},
"task": {
"startup": {
"disabledExecutors": []
},
"apiKeyCleanup": {
"retentionDays": 7,
"dailyTimeHHmm": "0000"
},
"dailyExport": {
"dailyTimeHHmm": "0000"
},
"referenceModel": {
"windowDays": 14,
"batchSize": 1000,
"threshold": 0.3,
"dailyTimesHHmm": ["0300", "0900", "1500", "2100"]
},
"scheduler": {
"pollIntervalMs": 180000,
"maxTasksPerQuery": 65535,
"idle": {
"maxTasksPerTick": 256,
"emaAlpha": 0.3
}
}
},
"logging": {
"retentionDays": 5
},
"quota": {
"anonymousMaxTokens": 25,
"userMaxTokens": 1000,
"refillAmount": 5,
"refillIntervalSeconds": 300,
"resetToMaxOnRestart": true,
"consumeTokens": true
},
"cost": {
"fixed": {
"health": 0,
"authMe": 1,
"authLogout": 1,
"authChangePassword": 5,
"debugEchoError": 0,
"authIssueApiKey": 5,
"authListApiKeys": 1,
"authRevokeApiKey": 1,
"searchIndex": 1,
"timetableTrain": 1,
"exportDailyIndex": 2,
"exportDaily": 50
},
"perRecord": {
"historyEmu": {
"unitCost": 0.05,
"rounding": "ceil"
},
"historyTrain": {
"unitCost": 0.05,
"rounding": "ceil"
},
"recordsDaily": {
"unitCost": 0.05,
"rounding": "ceil"
},
"timetableStation": {
"unitCost": 0.05,
"rounding": "ceil"
}
}
}
}spider:抓取 12306 数据
这一组决定 12306 抓取行为和探测频率。
spider.userAgentstring 必填 抓取请求使用的 User-Agent。
- 建议保持一个稳定、能被上游接受的移动端 UA。
spider.paramsobject 必填 抓取接口固定参数,包含 eKey、jsonpCallback 和 routeProbeCarCode。
spider.rateLimit.query.minIntervalMsinteger 必填 查询车次、车组号和畅行码接口的最小调用间隔,单位毫秒。
spider.rateLimit.search.minIntervalMsinteger 必填 通过 12306 搜索接口检索启用车次号的最小调用间隔,单位毫秒。
spider.scheduleProbe.dailyTimeHHmmstring(HHmm) 必填 每天生成车次探测任务的时间点,北京时间。
spider.scheduleProbe.retryAttempts / maxBatchSize / checkpointFlushEveryinteger 必填 控制车次探测任务的失败重试次数、单批处理上限和检查点落盘频率。
spider.scheduleProbe.refreshobject 必填 控制时刻表刷新任务的批次、TTL 和生成间隔。
spider.scheduleProbe.probeobject 必填 探测重试次数与 12306 接口数据异常的重试策略。
spider.scheduleProbe.couplingobject 必填 担当关系探测的状态重置、延迟和冷却参数。
- statusResetTimeHHmm 也必须是 HHmm 字符串。
spider.scheduleProbe.prefixRulesarray<object> 必填 允许探测的车次前缀与号段范围。
- prefix 必须是大写字母。
- 同一 prefix 的号段不能重叠。
- 数组不能为空。
data:数据库与静态资产文件
这一组决定数据库文件、schedule 文件和初始化资产的实际落盘位置。
data.assets.EMUListobject 必填 EMU 列表文件路径、下载地址与刷新时间。
- refresh.enabled=true 时 provider 必填。
- refresh.refreshAt 必须是 HHmm 字符串。
data.assets.QRCodeobject 必填 铁路畅行码资产文件路径、下载地址与刷新时间。
- refresh.enabled=true 时 provider 必填。
- refresh.refreshAt 必须是 HHmm 字符串。
data.assets.scheduleobject 必填 时刻表文件路径与来源地址。
- file 路径必须可读写;provider 建议保持可用。
- schedule.json 会在旧版格式下自动升级,无需手工删除旧文件。
- 当前格式会持久化完整经停站、当前站车次与检票口信息。
data.databases.taskstring 必填 任务调度数据库路径。
data.databases.EMUTrackedstring 必填 担当历史与日记录数据库路径。
data.databases.usersstring 必填 用户、登录态和 API Key 数据库路径。
data.databases.feedbackstring 必填 反馈与回复数据数据库路径。
- 所有数据库路径都建议指向持久化磁盘。
data.runtime:运行时统计文件
这一组决定管理员流量统计、服务器监控与 12306 请求计数的落盘位置、保留天数和定时写盘间隔。
data.runtime.adminTraffic.filestring 必填 管理员流量统计文件路径。
- 服务启动时会优先尝试从该文件恢复统计窗口。
- 文件父目录必须可写,建议放在持久化磁盘。
data.runtime.adminTraffic.flushIntervalMinutesinteger 必填 管理员流量统计的定时落盘间隔,单位分钟,默认值为 30。
- 仅在内存状态有变化时写盘。
data.runtime.adminServerMetrics.filestring 必填 服务器监控统计文件路径。
- 管理员服务器监控页的 CPU、内存、系统负载、SSR/API 时长窗口与 Top 5 慢路径聚合会从这里恢复。
- 文件父目录必须可写,建议放在持久化磁盘。
data.runtime.adminServerMetrics.flushIntervalMinutesinteger 必填 服务器监控统计的定时落盘间隔,单位分钟,默认值为 10。
- 仅在内存状态有变化时写盘。
data.runtime.adminServerMetrics.sampleIntervalSecondsinteger 必填 服务器监控后台采样间隔,单位秒,默认值为 60。
- CPU、内存和系统负载会按这个周期采样。
- SSR/API 时长与路径级延迟聚合仍按请求完成时实时入桶。
data.runtime.requestMetrics12306.filestring 必填 12306 请求计数文件路径。
- 管理员被动告警页的请求曲线直接读取该文件恢复的数据。
- 12306 请求计数不再通过日志回放恢复。
data.runtime.requestMetrics12306.retentionDaysinteger 必填 12306 请求计数保留天数,默认值为 3。
- 系统只保留最近 N 天的半小时桶。
- 超出保留窗口后,管理员页不再提供对应日期的请求曲线。
data.runtime.requestMetrics12306.flushIntervalMinutesinteger 必填 12306 请求计数的定时落盘间隔,单位分钟,默认值为 10。
- 间隔越短,异常崩溃时潜在丢失的数据窗口越小。
user:用户与 API Key 安全参数
这一组决定密码派生策略、API Key 生命周期和登录签名密钥。
user.saltLengthinteger 必填 密码盐长度。
user.apiKeyPrefixesobject 必填 分别定义 webapp 和 API Key 的前缀。
- 新部署请使用 webapp/api 两个前缀。
- 两个前缀不能重复。
user.apiKeyBytesinteger 必填 生成 API Key 时的随机字节数。
user.apiKeyTtlSecondsinteger 必填 默认签发的 API Key 生命周期,单位秒。
user.apiKeyMaxLifetimeSecondsinteger允许签发的最大 API Key 生命周期上限。
- 省略时默认 157680000 秒。
- 必须不小于 apiKeyTtlSeconds。
user.apiKeyNameLengthobject 必填 API Key 名称长度限制,包含 minLength 和 maxLength。
- 前端签发表单和服务端接口会共用这组限制。
- maxLength 必须大于等于 minLength。
user.adminUserIdsarray<string>管理员用户 ID 列表,命中的 webapp 登录用户会额外获得 api.admin 权限。
- 可以留空;留空表示不通过配置文件授予管理员。
- 生产环境建议通过 OCRH_ADMIN_USERS 覆盖,格式为逗号分隔的用户 ID 列表。
- 当 OCRH_ADMIN_USERS 非空时,会覆盖 user.adminUserIds。
user.favorites.maxEntriesinteger 必填 单个用户允许同步保存的最大收藏数。
- 当前前端收藏能力依赖这个上限,超限时接口会直接报错。
- 本次默认值为 10。
user.pushSubscriptions.maxDevices / user.pushSubscriptions.maxEventSubscriptions / user.pushSubscriptions.syncTimeoutSecondsinteger 必填 已存储 PushSubscription 端点的设备数量上限、预留的事件订阅规则数量上限,以及当前设备同步超时时间(秒)。
- maxDevices 按每个用户已存储的 PushSubscription 端点数量计数。
- 同一台物理设备在使用不同浏览器、不同用户配置或不同 PWA 安装时,可能会占用多条记录。
- maxEventSubscriptions 为后续事件订阅规则预留,当前代码尚未实际强制这一上限。
- syncTimeoutSeconds 控制控制台等待权限弹窗、Service Worker 就绪和浏览器订阅调用的最长时间;默认值为 30 秒。
user.push.vapidPublicKey / user.push.vapidPrivateKey / user.push.vapidEmailstring 必填 Web Push 所需的 VAPID 公钥、私钥和联系邮箱。
- 生产环境建议通过 OCRH_VAPID_PUBLIC_KEY、OCRH_VAPID_PRIVATE_KEY 和 OCRH_VAPID_EMAIL 覆盖,而不是把真实值直接写进配置文件。
- vapidEmail 只填写纯邮箱地址,例如 [email protected];程序会自动补上 mailto: 前缀。
- Apple 的 Safari / iOS Web Push 对 VAPID subject 更严格,邮箱缺失、非法或使用本地占位值时都可能导致推送被拒绝。
user.signKeystring 必填 登录签名密钥。
- 生产环境建议通过 OCRH_SIGN_KEY 覆盖,而不是把真实密钥直接写进配置文件。
- 修改后现有相关登录态可能失效。
user.scryptobject 必填 密码派生算法参数,包含 keyLength、cost、blockSize、parallelization。
api:接口基础行为、鉴权与权限范围
这一组决定 API 路径、请求头、Cookie、缓存、分页以及公开权限边界。
api.versionPrefixstring 必填 API 统一前缀,例如 /api/v1。
api.apiKeyHeaderstring 必填 API Key 所使用的请求头名称。
- 如果使用 authorization,文档和调试器会按 Bearer 形式发送。
api.authCookieNamestring 必填 浏览器登录态使用的 Cookie 名称。
api.clientIpHeadersarray<string>按顺序决定服务端从哪些请求头读取客户端 IP。
- 未配置时默认依次读取 cf-connecting-ip、x-forwarded-for、x-real-ip。
- 当命中 x-forwarded-for 时,只会取逗号分隔后的第一个地址。
- 所有配置头都取不到值时,会回退到 socket.remoteAddress。
- 使用 Cloudflare、Nginx 或其他反向代理时,应确保真实客户端 IP 会被透传到这些头之一。
api.authRateLimitobject 必填 登录与注册接口的限流配置。
api.authCacheobject 必填 用户记录、用户资料和 API Key 记录缓存的容量与 TTL。
api.authCache.userProfileobject 必填 user profile 数据 JSON 的服务端缓存容量与 TTL。
- 收藏接口读写成功后会直接回写这一层缓存,而不是简单删除缓存。
- 本次推荐值为 maxEntries=256,defaultTtlSeconds=21600。
api.payload.maxStringLengthinteger 必填 请求体允许的最大字符串长度。
api.feedback.validationobject 必填 反馈接口后端校验使用的长度限制配置,分别控制创建反馈正文、回复正文和管理员修改标题时的最小/最大长度。
- createBody 对应 POST /api/v1/feedback/topics 的 body 长度范围。
- replyBody 对应 POST /api/v1/feedback/topics/[id]/messages 的 body 长度范围。
- title 对应 PATCH /api/v1/feedback/topics/[id] 的 title 长度范围。
api.headersobject 必填 额度剩余、成本和重试时间的响应头名称。
api.cacheobject 必填 当前日、历史、搜索索引、sitemap 和 timetable 接口成功响应的缓存时长。
- sitemapMaxAgeSeconds 用于控制 /sitemap.xml 响应的 Cache-Control max-age。
- timetableMaxAgeSeconds 用于控制 /api/v1/timetable/train/* 和 /api/v1/timetable/station/* 成功响应的 Cache-Control max-age。
api.paginationobject 必填 分页默认大小和最大上限。
- maxLimit 必须不小于 defaultLimit。
api.timestampUnitstring 必填 时间戳单位。
- 当前代码只支持 seconds。
api.debug.enableEchoErrorboolean 必填 是否允许调试接口回显错误。
api.permissions.anonymousScopesarray<string> 必填 匿名访问允许拥有的 scopes。
- 这组配置直接决定公开可匿名调用的接口范围。
- 如果要公开车次详情页时刻表弹窗,需要包含 api.timetable.train.read。
- 如果要公开车站页时刻表接口,需要包含 api.timetable.station.read。
api.permissions.issuedKeyDefaultScopesarray<string> 必填 新签发 API Key 的默认权限集合。
- 如需默认允许当前车次时刻表接口,请包含 api.timetable.train.read。
- 如需默认允许车站时刻表接口,请包含 api.timetable.station.read。
- 如需让 Web 端收藏功能开箱即用,请包含 api.auth.favorites.read 和 api.auth.favorites.write。
api.permissions.creatableKeyMaxScopesarray<string> 必填 前端可签发 API Key 时允许选择的最大权限范围。
- 若希望外部调用方可勾选当前车次时刻表接口,这里也要包含 api.timetable.train.read。
- 若希望外部调用方可勾选车站时刻表接口,这里也要包含 api.timetable.station.read。
task:后台任务与调度器
这一组决定启动时禁用的执行器、定时任务时间以及轮询调度行为。
task.startup.disabledExecutorsarray<string>启动时跳过的执行器列表。
- 只能使用 build_today_schedule、generate_route_refresh_tasks、dispatch_daily_probe_tasks、clear_daily_probe_status、cleanup_revoked_api_keys、export_daily_records、rebuild_reference_model_index。
- 为空数组表示全部启用。
task.apiKeyCleanup.retentionDaysinteger 必填 吊销 API Key 的保留天数。
task.apiKeyCleanup.dailyTimeHHmmstring(HHmm) 必填 每日执行 API Key 清理任务的时间。
task.dailyExport.dailyTimeHHmmstring(HHmm) 必填 每日导出任务的执行时间。
task.referenceModelobject 必填 参考车型索引重建任务的配置项。
- windowDays 用于设置历史窗口天数,当前默认值为 14。
- batchSize 用于设置扫描 daily_emu_routes 时的分页批大小,当前默认值为 1000。
- threshold 是 weightedShare 的阈值,必须大于 0 且小于等于 1。
- dailyTimesHHmm 用于手动填写多个 HHmm 时刻,控制 rebuild_reference_model_index 在一天内多次重建。
task.scheduler.pollIntervalMsinteger 必填 调度器轮询间隔,单位毫秒。
task.scheduler.maxTasksPerQueryinteger 必填 单次查询最多取回的任务数。
task.scheduler.idle.maxTasksPerTickinteger 必填 空闲模式下每个 tick 最多拉起的任务数。
task.scheduler.idle.emaAlphanumber 必填 空闲调度使用的 EMA 系数。
- 必须大于 0 且小于等于 1。
logging:日志文件
这一组决定 logs/ 目录下按日滚动日志的保留时间;12306 请求计数已独立落盘,不再写入日志。
logging.retentionDaysinteger 必填 应用日志的总保留天数,默认值为 5。
- 保留天数包含当前当天正在写入的日志文件。
- 日志文件位于 logs/ 目录,可按运维策略另行备份。
- 12306 请求计数不会写入日志,日志中仅继续保留 warning、error 等异常信息。
quota:额度与补充策略
这一组决定匿名用户和登录用户的额度桶大小,以及额度恢复方式。
quota.anonymousMaxTokensinteger 必填 匿名访问的额度桶上限。
quota.userMaxTokensinteger 必填 登录用户的额度桶上限。
quota.refillAmountinteger 必填 每个补充周期恢复的额度数量。
quota.refillIntervalSecondsinteger 必填 额度补充周期,单位秒。
quota.resetToMaxOnRestartboolean 必填 服务重启后是否把额度恢复到上限。
quota.consumeTokensboolean 必填 是否实际扣减额度。
- 关闭后可以保留成本计算,但不会真正消耗额度。
cost:接口耗额策略
这一组决定每个接口、以及按记录数量计费接口的额度成本。
cost.fixed.health / authMe / authLogout / debugEchoErrorinteger 必填 健康检查、身份相关和调试接口的固定成本。
cost.fixed.authChangePassword / authIssueApiKey / authListApiKeys / authRevokeApiKeyinteger 必填 API Key 管理接口的固定成本。
cost.fixed.searchIndex / timetableTrain / exportDailyIndex / exportDailyinteger 必填 搜索、当前时刻表与导出接口的固定成本。
cost.perRecord.historyEmuobject 必填 按车组历史查询的按记录计费规则。
- 当前 rounding 只支持 ceil。
cost.perRecord.historyTrainobject 必填 按车次历史查询的按记录计费规则。
- 当前 rounding 只支持 ceil。
cost.perRecord.recordsDailyobject 必填 每日记录查询的按记录计费规则。
- 当前 rounding 只支持 ceil。
cost.perRecord.timetableStationobject 必填 车站时刻表分页查询的按记录计费规则。
- 当前 rounding 只支持 ceil。
修改配置后的建议
- 修改 data.databases 或 data.assets 路径前先备份旧文件,再验证新路径具备正确的读写权限。
- 修改 user.signKey、api.apiKeyHeader、api.authCookieName 等身份相关字段后,应该视为一次完整重启变更并安排验证。
- 修改 user.adminUserIds 或 OCRH_ADMIN_USERS 后需要重启 Node 进程,新的登录会话才会按最新名单授予管理员权限。
- 修改 user.push 或 OCRH_VAPID_PUBLIC_KEY / OCRH_VAPID_PRIVATE_KEY / OCRH_VAPID_EMAIL 后需要重启 Node 进程,并建议立即用一台 Apple Safari PWA 设备验证推送是否正常。
- 提高 api.permissions.anonymousScopes、quota 或 cost 前,先确认你准备公开暴露的接口范围和限额策略。
- 修改 spider.scheduleProbe.prefixRules、task.referenceModel.dailyTimesHHmm 或 task.scheduler 参数后,重启后应观察首轮任务执行是否符合预期。
run
启动服务
构建完成和确认配置文件无误后,可启动服务。
请先设置登录签名秘钥,该秘钥将被用于签名用户的 token,切勿泄露。
export OCRH_SIGN_KEY=<xxxxxxxx>如果需要启用 Web Push,请同时设置 VAPID 公私钥和联系邮箱;vapidEmail 只填写纯邮箱地址,不要带 mailto: 前缀。
export OCRH_VAPID_PUBLIC_KEY=<base64url-public-key>
export OCRH_VAPID_PRIVATE_KEY=<base64url-private-key>
export [email protected]如果需要授予管理员权限,请设置管理员用户 ID 列表;多个用户使用英文逗号分隔。
export OCRH_ADMIN_USERS=<user-id-1>,<user-id-2>然后设置服务器监听端口,默认为 3000
export NITRO_PORT=<port>启动服务
nohup node .output/server/index.mjs>log.log 2>&1 &应用会在 logs/ 目录下按天滚动写入日志,同时按 data.runtime 配置把管理员流量统计、服务器监控统计和 12306 请求计数分别写入独立文件。12306 请求计数不再进入日志。
operations
运维建议
把 data 目录视为需要持久化和备份的运行时数据目录。
更新代码前建议先停服务、备份 data 目录和当前生效的配置文件,再替换代码、重新构建并启动。
请确保 data.runtime.adminTraffic.file、data.runtime.adminServerMetrics.file 与 data.runtime.requestMetrics12306.file 的父目录可写;管理员流量统计按 30 分钟默认周期落盘,服务器监控按 10 分钟默认周期落盘并每 60 秒采样一次,同时持久化 SSR/API 时长与路径级延迟聚合,12306 请求计数按 10 分钟默认周期落盘并按 retentionDays 自动裁剪。
当前运行时统计文件按单实例设计;如果部署多实例,请避免多个进程同时写同一份 runtime 文件。