访问时间窗口

Access Time Windows 允许合作伙伴会员携带由外部系统提供的临时或周期性访问权限。本页面是 access_time_windows 和 access_time_windows_revision 的权威生产方指南。

Access Time Windows 是仅用于允许的附加权限。有效的时间窗口可以允许访问,但无效、已过期、已过时或未映射的时间窗口不会拒绝常规规则或永久授权已允许的访问。用户停用、显式门禁封锁、维护模式、地理围栏、速率限制和控制器可用性仍然适用。

针对特定上游系统的实现属于单独的适配器。它们必须将其源数据转换为这一与供应商无关的约定,且不得改变字段的含义。

会员快照字段

  • access_time_windows array 可选

    此会员和合作伙伴源的完整 Access Time Window 快照。一个快照可以混合包含 absolute 和 weekly 时间窗口。省略该字段将保持已存储的快照不变;发送 [] 将撤销此源拥有的所有时间窗口。

  • access_time_windows_revision integer 提供 access_time_windows 时必填

    此会员/源修订序列的一个单调递增、非负且 JSON 安全的整数。只要存在 access_time_windows,此字段即为必填,且不得超过 9007199254740991。

每个快照最多接受 50 个时间窗口。external_id、scope_key 和 label 最多接受 120 个字符。绝对时间窗口的跨度最长为 90 天。每周中的每一天最多接受八个不重叠的时段。无效数据将被拒绝,而不会被截断或推测。

时间窗口结构

绝对时间窗口标识一个确切的时间区间:

{
  "external_id": "access-4b1782",
  "kind": "absolute",
  "from": 1792053000,
  "to": 1792058400,
  "timezone": "Australia/Perth",
  "scope_key": "after_hours_entry",
  "label": "Temporary site access"
}

必填字段为 external_id、kind、from 和 to。timezone、scope_key 和 label 为可选字段。

每周时间窗口标识按本地挂钟时间周期性重复的时间区间,并可选择以 UTC 时刻作为边界:

{
  "external_id": "weekly-early-access",
  "kind": "weekly",
  "timezone": "America/New_York",
  "access_hours": {
    "sun": [],
    "mon": [["06:00", "08:30"]],
    "tue": [["06:00", "08:30"]],
    "wed": [["06:00", "08:30"]],
    "thu": [["06:00", "08:30"]],
    "fri": [["06:00", "08:30"]],
    "sat": []
  },
  "active_from": 1790812800,
  "active_until": 1796083200,
  "scope_key": "main_entry",
  "label": "Weekday early access"
}

必填字段为 external_id、kind 和 access_hours。timezone、active_from、active_until、scope_key 和 label 为可选字段。

完整的混合请求

此请求将会员之前由源拥有的快照替换为一个绝对时间窗口和一个每周时间窗口:

curl -X POST https://partner-api.performancehub.co/partner/v1/members \
  -u sk_live_YOUR_PARTNER_SECRET_KEY: \
  -H "Content-Type: application/json" \
  -d '{
    "members": [
      {
        "member_id": "member_11111",
        "facility_id": "550e8400-e29b-41d4-a716-446655440000",
        "first_name": "John",
        "last_name": "Smith",
        "access_time_windows_revision": 1842,
        "access_time_windows": [
          {
            "external_id": "access-4b1782",
            "kind": "absolute",
            "from": 1792053000,
            "to": 1792058400,
            "timezone": "Australia/Perth",
            "scope_key": "after_hours_entry",
            "label": "Temporary site access"
          },
          {
            "external_id": "weekly-early-access",
            "kind": "weekly",
            "timezone": "America/New_York",
            "access_hours": {
              "sun": [],
              "mon": [["06:00", "08:30"]],
              "tue": [["06:00", "08:30"]],
              "wed": [["06:00", "08:30"]],
              "thu": [["06:00", "08:30"]],
              "fri": [["06:00", "08:30"]],
              "sat": []
            },
            "active_from": 1790812800,
            "active_until": 1796083200,
            "scope_key": "main_entry",
            "label": "Weekday early access"
          }
        ]
      }
    ]
  }'

根据您配置的关联方式,使用 facility_id 或 linking_identifier。请勿同时发送两者。

时间和边界规则

from、to、active_from 和 active_until 是以 Unix 秒表示的 JSON 整数,用于标识 UTC 时刻:

  • 发送不带引号的 JSON 整数。数字字符串、浮点数、RFC 3339 字符串和 Unix 毫秒值都会被拒绝。
  • 每个区间都是左闭右开区间:[from, to)。访问可以恰好在 from 开始;恰好在 to 时不再有效。
  • from 必须小于 to。当两个每周边界都存在时,active_from 必须小于 active_until。
  • 不要根据不带时区的本地日期/时间推导纪元时间。请先将源时间解析为 UTC 时刻,然后将 Unix 毫秒值除以 1000,并验证结果为整数。

纪元时间转换

2026-10-15T08:30:00Z 转换为 1792053000:

UTC instant                 Unix milliseconds   Unix seconds sent
2026-10-15T08:30:00Z        1792053000000        1792053000

与语言无关的转换方式:

instant = parse_source_timestamp_with_explicit_zone(source_timestamp)
milliseconds = instant.to_unix_milliseconds()
assert milliseconds modulo 1000 equals 0
seconds = milliseconds / 1000
assert seconds is a JSON-safe nonnegative integer

如果源数据具有亚秒级精度,请在集成测试之前制定明确的开始/结束舍入策略。切勿传递毫秒值并依赖 API 推断单位。

时区和夏令时

timezone 必须是有效的 IANA 时区,例如 UTC、Australia/Perth 或 America/New_York。省略时默认为 UTC。

  • 绝对时间窗口:from 和 to 已经标识确切的 UTC 时刻。timezone 仅为显示和审计元数据;更改它绝不会使任一时刻发生偏移。
  • 每周时间窗口:timezone 定义用于评估 access_hours 的本地挂钟时间和夏令时规则。请勿预先将每周时段转换为 UTC。
  • 在夏令时开始(时钟拨快)时,该时区跳过的本地时间永远不会出现。
  • 在夏令时结束(时钟拨回)时,带有该重复本地时间的两个真实时刻都与每周时段匹配。
  • 即使每周时间窗口使用非 UTC 时区,active_from 和 active_until 仍然是 UTC 时刻。

access_hours 使用小写的 sun 到 sat。每个值是由 [start, end] 对组成的数组,采用补零的 24 小时制 HH:mm 格式。时段必须非空、有序且互不重叠。end 不包含在内。时段不能跨越午夜;请将跨夜的源区间拆分为第一天的较晚时段和第二天的较早时段。

稳定 ID 和范围映射

external_id 必须在逻辑时间窗口的整个生命周期内保持稳定,并在会员快照中保持唯一。当时间或标签发生变化时,请保持其稳定,以便运营人员和审计记录能够关联各个修订版本。请勿将其重复用于不相关的时间窗口。

scope_key 是您稳定的逻辑访问目标,默认为 default。设施管理员会将每个允许的键映射到 Performance Hub 中的整个设施、门或访问范围目标:

Partner scope_key           Performance Hub mapping
default                     Facility's configured default target
main_entry                  Access Scope "Main entrances"
after_hours_entry           Doors approved for after-hours entry

合作伙伴不会在时间窗口中发送 Performance Hub 设施、门或访问范围 ID。未知、已停用或未映射的 scope_key 会使该时间窗口按拒绝处理(fail closed),而不会扩大访问权限。请在上线前商定并测试映射。

完整快照和修订

将这两个字段视为会员更新插入中单独进行版本控制的分区:

输入结果
省略 access_time_windows现有快照保持不变;无需修订号
存在且修订号更新替换此会员/源已存储的整个快照
相同修订号且规范化负载相同幂等重放;快照不变
相同修订号但规范化负载不同冲突;对账后分配新的修订号
较旧的修订号跳过过时快照;不能覆盖较新的状态
access_time_windows: [] 且修订号更新显式撤销源拥有的所有时间窗口

快照是完整的,而不是补丁。如果修订版本 1842 包含时间窗口 A 和 B,那么仅包含 B 的修订版本 1843 将移除 A。

在 webhook 工作进程、轮询、重放和完整对账之间,为每个合作伙伴/会员维护一个协调一致的修订序列。建议使用持久的数据库序列。只有在每个写入方都进行协调并保证严格的顺序和唯一性时,才能将 Unix 毫秒值用作修订号;它是修订值,而不是纪元时间字段。

在获知 API 响应之前,请持久保存确切的请求和修订号。发生超时或暂时性错误后,请使用相同的修订号重试相同的规范化快照。不要仅因传输投递结果不确定就分配新的修订号。

取消、过期和上游故障

  • **取消一个时间窗口:**成功获取权威源状态,移除该时间窗口,然后使用更新的修订号发送剩余的完整快照。
  • **取消所有时间窗口:**使用更新的修订号发送 access_time_windows: []。
  • **让绝对时间窗口过期:**它在 to 时停止允许访问;之后发送一次不含已过期条目的对账,以保持快照整洁。
  • **结束每周时间窗口:**在更新的快照中将其移除;如果源具有已知的结束时刻,也可以使用 active_until。
  • **源获取失败或不完整:**不要发送 [],也不要发布部分快照。发出警报并重试获取。将上游故障转化为空快照会导致意外撤销访问权限。
  • **Partner API 请求未通过验证:**修正数据;仅当规范化快照发生变化时,才发送更新的修订号。对于暂时性故障,请使用原修订号重试。

源数据新鲜度根据每个集成承诺的投递和对账频率按集成进行配置。在激活之前,请商定最大源数据时效、超出时效时的处理方式、警报阈值,以及运营人员是否可以确认仅依赖每个时间窗口自身的边界。过时的源时间窗口永远不会成为拒绝规则;常规规则和永久授权仍是后备途径。

生产方算法

每次更新会员时,请使用以下与语言无关的工作流程:

function publish_member(member_key):
    source_result = fetch_authoritative_access_state(member_key)

    if source_result.failed or source_result.incomplete:
        record_failure(member_key)
        schedule_retry(member_key)
        return without sending access_time_windows

    windows = []
    for source_window in source_result.current_windows:
        if source_window.is_absolute:
            windows.append({
                external_id: stable_window_id(source_window),
                kind: "absolute",
                from: to_integer_unix_seconds(source_window.start_instant),
                to: to_integer_unix_seconds(source_window.end_instant),
                timezone: source_window.display_timezone or "UTC",
                scope_key: stable_scope_key(source_window) or "default",
                label: safe_label(source_window)
            })
        else:
            windows.append({
                external_id: stable_window_id(source_window),
                kind: "weekly",
                timezone: valid_iana_zone(source_window.timezone) or "UTC",
                access_hours: split_validate_and_sort_weekly_blocks(source_window),
                active_from: optional_integer_unix_seconds(source_window.active_start),
                active_until: optional_integer_unix_seconds(source_window.active_end),
                scope_key: stable_scope_key(source_window) or "default",
                label: safe_label(source_window)
            })

    normalized_snapshot = validate_sort_and_deduplicate(windows)
    revision = next_durable_member_revision(member_key)
    request = member_upsert_with(normalized_snapshot, revision)
    persist_for_retry(member_key, request)

    response = post_request(request)
    if response.is_transient_or_unknown:
        retry_exact_request_with_same_revision(request)
    else if response.is_revision_conflict:
        reconcile_with_latest_committed_source_state(member_key)
    else if response.is_stale_skip:
        stop_replaying_this_revision_and_reconcile(member_key)

认证检查清单

使用最终的 scope_key 映射,在暂存设施中完成以下检查:

  • 省略时区的绝对时间窗口被接受,并按 UTC 进行评估。
  • 使用自定义 IANA 时区的绝对时间窗口保留相同的 from/to 时刻。
  • 使用自定义 IANA 时区的每周时间窗口在夏令时开始和结束切换时都遵循本地挂钟时间。
  • 混合绝对/每周时间窗口的快照完全替换之前的快照。
  • 访问恰好在 from 开始、恰好在 to 停止;每周时段的结束时间同样不包含在内。
  • 在同一 external_id 下使用更新的修订号更新时间后生效。
  • 省略 access_time_windows 时,会更改其他会员字段而不更改时间窗口。
  • 重放相同的修订号/负载是幂等的。
  • 相同修订号/不同负载返回冲突,且不更改快照。
  • 乱序到达的较旧修订号被报告为过时,且不能回滚状态。
  • 更新的 [] 会撤销源拥有的时间窗口;上游获取失败时不发送任何内容。
  • 未知或未映射的 scope_key 按拒绝处理(fail closed),并生成运营人员可见的警告。
  • 源数据时效警报和已配置的过时源策略在商定的阈值下正常工作。
  • webhook 路径和完整对账路径收敛到相同的快照和修订顺序。
  • 序列投递在商定的目标时间内到达 Door & Gate Access。
  • 有效的时间窗口可以允许访问已映射的实体门,但无法绕过用户停用、blockAllAccess、维护或地理围栏。
  • 在时间窗口之前、之外和之后,常规规则和永久授权仍会允许其他方面符合条件的用户访问。

最低测试向量

[
  {
    "name": "absolute-half-open",
    "window": {
      "external_id": "tv-absolute",
      "kind": "absolute",
      "from": 1792053000,
      "to": 1792058400
    },
    "expect": {
      "1792052999": "inactive",
      "1792053000": "active",
      "1792058399": "active",
      "1792058400": "inactive"
    }
  },
  {
    "name": "weekly-dst",
    "window": {
      "external_id": "tv-weekly-dst",
      "kind": "weekly",
      "timezone": "America/New_York",
      "access_hours": {
        "sun": [["01:00", "03:30"]],
        "mon": [],
        "tue": [],
        "wed": [],
        "thu": [],
        "fri": [],
        "sat": []
      }
    },
    "expect": "Evaluate local wall time: skipped times do not occur; both repeated 01:30 instants qualify."
  }
]

还请提交以下负面测试向量:Unix 毫秒值、数字字符串、浮点数、无效的 IANA 时区、from >= to、重复的 external_id、重叠的时段、跨夜时段、一天超过八个时段、超过 50 个时间窗口,以及未映射的范围键。