Skip to content

feat(backend): expose authenticated schedule snapshot endpoint #250

Description

@Alexander-Noah

动机 / 用户故事

TimeFlow 将客户端日程保存在 SQLite,同时将账号日程持久化到云端 PostgreSQL。

当用户重新安装应用、清除本地数据或在新设备登录时,本地业务数据库可能为空,但云端仍保留该账号的日程和周期例外。如果登录后只能进入空日历,而无法从云端重新取得完整数据,用户会误以为历史日程已经丢失。

用户需要的是一条明确、可验证的恢复数据入口:

用户完成登录后,如果客户端确认本地业务数据为空,可以使用当前登录账号的 Bearer Token 请求云端完整日程快照,再由客户端将快照恢复到 SQLite。

客户端将 HTTP 全量快照应用为 SQLite 权威状态的能力,已经由 PR #225 实现并合并。

本 Issue 负责补齐这条链路的上游服务端接口:

Bearer Token
  → GET /api/v1/schedule/snapshot
  → 账号完整 CloudScheduleSnapshot
  → PR #225:applyFullScheduleSnapshotToSqlite()
  → SQLite 日程恢复

本 Issue 只交付后端 HTTP 数据出口,不实现客户端空库判断、接口调用、快照落库或登录后页面编排。

目标用户

  • 重新安装 TimeFlow 后登录原账号的 Android 用户。
  • 清除应用数据后需要恢复历史日程的用户。
  • 在另一台设备登录并需要取得云端日程的用户。
  • 云端已有日程,但当前设备本地业务数据库为空的用户。

当前状态与依赖关系

已完成的下游客户端能力

PR #225 已实现:

  • 定义 ApplyFullScheduleSnapshotCommand
  • 通过 applyFullScheduleSnapshotToSqlite() 应用 HTTP 全量快照。
  • 将全量快照视为指定账号的完整云端投影。
  • 删除快照中不存在的本地云端日程。
  • 删除快照中不存在的本地周期例外。
  • 接受两个空数组,并将其解释为该账号没有云端日程。
  • 隔离不同账号的 SQLite 日程和周期例外。
  • 拒绝账号不匹配、字段非法或引用不完整的快照。
  • 在 SQLite 事务失败时回滚全部恢复操作。
  • 保留较新的本地 revision 以及对应周期例外。
  • 保留 next_trigger_atsnoozed_untilgeofence_armedsync_status 等设备本地运行状态。
  • 保持 WebSocket 增量快照应用行为不变。

本 Issue 与 PR #225 的关系

本 Issue 是 PR #225 的上游服务端配套能力:

虽然 PR #225 已经合并,但端到端恢复能力仍依赖本 Issue 提供后端接口。

本期范围

1. 提供受认证保护的账号快照接口

新增接口:

GET /api/v1/schedule/snapshot
Authorization: Bearer <access_token>

接口约束:

  • 请求方法固定为 GET
  • 请求不包含 body。
  • 契约不定义任何 query 参数。
  • 复用现有 Bearer Token 认证依赖。
  • 账号身份只能来自已经验证的 Token sub
  • 不允许从请求体、query 或其他业务字段取得账号身份。
  • 即使调用方附带 account_id,服务端也不得读取或使用它。
  • 缺少 Token 时返回 401 AUTH_REQUIRED
  • Token 无效、过期或声明不匹配时返回 401 AUTH_INVALID_TOKEN

例如:

GET /api/v1/schedule/snapshot?account_id=acc_attacker
Authorization: Bearer <token-for-acc_001>

服务端只能查询 acc_001,不得查询 acc_attacker

2. 返回完整账号级云端快照

成功响应顶层必须且只能包含:

{
  "schedules": [],
  "occurrence_overrides": []
}

其中:

  • schedules 包含认证账号全部 activedeleted 日程。
  • occurrence_overrides 包含这些日程的全部周期实例例外。
  • 不允许返回其他账号的日程。
  • 周期例外只能引用同一响应中的当前账号日程。
  • 两个集合必须在同一个 ScheduleUnitOfWork 中完成查询。
  • 两个集合必须一起组装并一起返回。
  • 任一查询、校验或序列化步骤失败时,不得返回部分快照。

完整响应示例:

{
  "schedules": [
    {
      "id": "schedule_001",
      "account_id": "acc_001",
      "schedule_type": "time",
      "schedule_kind": "recurring",
      "title": "每周例会",
      "is_all_day": false,
      "start_time": "2026-08-17T01:00:00+00:00",
      "end_time": "2026-08-17T02:00:00+00:00",
      "timezone": "Asia/Shanghai",
      "recurrence_rule": "FREQ=WEEKLY;BYDAY=MO",
      "location_name": null,
      "latitude": null,
      "longitude": null,
      "reminder_type": "before_start",
      "reminder_trigger_at": null,
      "reminder_offset_minutes": 10,
      "reminder_strength": "medium",
      "reminder_disposition_state": null,
      "status": "active",
      "revision": 3,
      "created_at": "2026-08-01T03:00:00+00:00",
      "updated_at": "2026-08-10T06:00:00+00:00",
      "deleted_at": null
    }
  ],
  "occurrence_overrides": [
    {
      "id": "override_001",
      "schedule_id": "schedule_001",
      "occurrence_start": "2026-08-24T01:00:00+00:00",
      "action": "cancel",
      "replacement_schedule_id": null,
      "created_at": "2026-08-20T04:00:00+00:00",
      "updated_at": "2026-08-20T04:00:00+00:00"
    }
  ]
}

3. 日程响应字段

每条日程必须返回以下字段:

  • id
  • account_id
  • schedule_type
  • schedule_kind
  • title
  • is_all_day
  • start_time
  • end_time
  • timezone
  • recurrence_rule
  • location_name
  • latitude
  • longitude
  • reminder_type
  • reminder_trigger_at
  • reminder_offset_minutes
  • reminder_strength
  • reminder_disposition_state
  • status
  • revision
  • created_at
  • updated_at
  • deleted_at

HTTP 传输模型必须验证:

  • id 非空。
  • account_id 非空。
  • account_id 等于认证 Token 对应账号。
  • title 非空且最长 255 个 Unicode code point。
  • revision >= 1
  • 纬度存在时范围为 -90~90
  • 经度存在时范围为 -180~180
  • created_atupdated_at 必须是带时区的 RFC 3339 时间。
  • start_timeend_timereminder_trigger_atdeleted_at 存在时必须带时区。
  • 同一响应中不得出现重复日程 ID。

HTTP 层只增加传输层、账号归属和响应完整性校验。

以下规则继续由现有日程业务层维护,不在 HTTP 层复制第二套实现:

  • 时间日程和位置日程的创建规则。
  • 全天日程规则。
  • 起止时间业务关系。
  • IANA 时区业务校验。
  • 周期规则语义。
  • 提醒字段组合规则。
  • active 和 deleted 状态转换规则。

4. 周期例外响应字段

每条周期例外必须返回:

  • id
  • schedule_id
  • occurrence_start
  • action
  • replacement_schedule_id
  • created_at
  • updated_at

HTTP 传输层必须验证:

  • 周期例外 ID 非空。
  • 同一响应中不得出现重复周期例外 ID。
  • 同一 schedule_id + occurrence_start 不得出现重复例外。
  • occurrence_start 必须是带时区的 RFC 3339 时间。
  • created_atupdated_at 必须是带时区的 RFC 3339 时间。
  • schedule_id 必须引用同一响应中的日程。
  • 被引用的父日程必须是 recurring 日程。
  • cancel 操作的 replacement_schedule_id 必须为 null
  • replace 操作的 replacement_schedule_id 必须引用同一响应中的日程。
  • 不得返回缺少父日程的周期例外。
  • 不得返回跨账号引用。
  • 不得返回错误的 replacement 引用。

5. 稳定排序

响应顺序必须稳定,不能依赖数据库未声明的默认顺序。

日程按以下顺序返回:

  1. start_time 升序;
  2. created_at 升序;
  3. id 升序;
  4. start_time = null 的日程排在最后。

周期例外按以下顺序返回:

  1. occurrence_start 升序;
  2. id 升序。

排序继续复用现有 Schedule Repository 行为,不在 HTTP 层维护第二套排序或 SQL。

6. 空账号行为

认证账号没有任何云端日程时,接口返回:

200 OK
Content-Type: application/json
{
  "schedules": [],
  "occurrence_overrides": []
}

空账号是合法业务状态:

  • 不返回 404。
  • 不增加 has_data
  • 不增加 is_new_account
  • 不通过账号 created_at 推断新用户。
  • 不新增初始化标记。
  • 不新增同步状态。
  • 不判断客户端是否应该调用该接口。

7. 空快照与 PR #225 的权威状态语义

PR #225 会将 HTTP 全量快照作为指定账号的权威云端状态应用到 SQLite。

因此,以下两种结果必须严格区分:

200 + 两个空数组
= 服务端已经确认该账号在云端没有日程
= 客户端可以将空快照作为权威状态应用
500 SCHEDULE_SNAPSHOT_INTERNAL_ERROR
= 服务端无法确认账号的完整云端状态
= 客户端不得将其解释为空快照

服务端不得在以下情况返回 200 空数组:

  • 数据库连接失败。
  • 日程查询失败。
  • 周期例外查询失败。
  • 账号归属校验失败。
  • 引用完整性校验失败。
  • 时间字段校验失败。
  • 响应序列化失败。
  • 只取得部分查询结果。

如果服务端错误地把查询异常降级成空快照,PR #225 可能按照权威状态语义清理该账号的本地云端投影。因此这是本接口必须防止的数据安全问题。

8. 统一错误处理

查询、校验或序列化失败时统一返回:

500 Internal Server Error
Content-Type: application/json
{
  "error": {
    "code": "SCHEDULE_SNAPSHOT_INTERNAL_ERROR",
    "message": "Schedule snapshot unavailable"
  }
}

错误响应和日志不得泄露:

  • Bearer Token 或 JWT。
  • JWT 密钥、签名或声明细节。
  • 数据库连接串。
  • SQL 语句。
  • 数据库异常原文。
  • 损坏字段的原始值。
  • 其他账号 ID。
  • 其他账号的日程内容。

内部日志只记录安全事件标识、异常类型和经过脱敏的栈位置。

9. 业务查询边界

业务层新增账号快照查询结果:

@dataclass(frozen=True, slots=True)
class AccountScheduleSnapshot:
    schedules: tuple[ScheduleSnapshot, ...]
    occurrence_overrides: tuple[ScheduleOccurrenceOverrideSnapshot, ...]

新增只读端口:

class ScheduleSnapshotReader(Protocol):
    def get_account_snapshot(
        self,
        *,
        account_id: str,
    ) -> AccountScheduleSnapshot: ...

新增查询服务:

class ScheduleSnapshotQueryService:
    def __init__(
        self,
        unit_of_work_factory: ScheduleUnitOfWorkFactory,
    ) -> None: ...

    def get_account_snapshot(
        self,
        *,
        account_id: str,
    ) -> AccountScheduleSnapshot: ...

查询服务必须:

  • 拒绝空白 account_id
  • 复用现有 ScheduleUnitOfWorkFactory
  • 调用 list_schedules(account_id=..., include_deleted=True)
  • 调用 list_occurrence_overrides(account_id=...)
  • 在同一个 UoW 中读取两个集合。
  • 保持只读,不执行 commit()
  • 不依赖 FastAPI。
  • 不依赖 Pydantic。
  • 不依赖 SQLAlchemy。
  • 不编写第二套 SQL。

10. HTTP 网关边界

新增路由工厂:

def create_schedule_snapshot_router(
    reader: ScheduleSnapshotReader,
    authenticated_account: AuthenticatedAccountDependency,
) -> APIRouter: ...

HTTP 网关负责:

  • 使用现有认证依赖取得认证账号。
  • 使用认证账号调用 ScheduleSnapshotReader
  • 定义严格响应模型。
  • 校验账号归属。
  • 校验带时区的时间字段。
  • 校验重复 ID。
  • 校验周期例外父日程和 replacement 引用。
  • 映射统一的 500 错误。
  • 在 OpenAPI 中记录 401 和 500 响应。
  • 复用现有 AuthErrorEnvelope
  • 不解析第二次 JWT。
  • 不直接导入 timeflow.data
  • 不读取 query 中的 account_id

11. 组合根装配

create_app() 增加可替换的快照读取端口:

def create_app(
    *,
    audio_sink: AudioSink | None = None,
    auth_access: AuthAccess | None = None,
    access_token_service: AccessTokenService | None = None,
    engine: Engine | None = None,
    auth_rate_limiter: AuthRateLimiter | None = None,
    schedule_snapshot_reader: ScheduleSnapshotReader | None = None,
) -> FastAPI: ...

生产装配必须:

  • 复用现有数据库 Session Factory。
  • 使用 SqlAlchemyScheduleUnitOfWork 构建真实查询服务。
  • 复用认证接口使用的同一个 Token 服务。
  • 复用 create_authenticated_account_dependency()
  • 注册 /api/v1/schedule/snapshot
  • 允许测试注入 ScheduleSnapshotReader 替身。
  • 只在组合根中连接业务端口与数据适配器。

预计文件范围

新增或修改:

文件 操作 职责
backend/src/timeflow/business/calendar/snapshot.py 新增 定义账号快照结果、读取端口和查询服务
backend/src/timeflow/business/calendar/__init__.py 修改 导出快照查询公共接口
backend/src/timeflow/gateway/http/schedule_snapshot.py 新增 定义响应模型、校验、路由和错误映射
backend/src/timeflow/gateway/http/__init__.py 修改 导出快照 HTTP 接口
backend/src/timeflow/main.py 修改 装配查询服务、认证依赖和路由
backend/tests/business/calendar/test_schedule_snapshot.py 新增 查询服务测试
backend/tests/gateway/http/test_schedule_snapshot.py 新增 HTTP 契约和安全测试
backend/tests/test_app_wiring.py 修改 组合根和共享 Token 服务测试
backend/tests/test_auth_integration.py 修改 真实 PostgreSQL 集成测试

明确不修改:

  • frontend/
  • backend/alembic/
  • backend/src/timeflow/data/models.py
  • backend/src/timeflow/data/repositories/schedule.py

明确不做

  • 不修改 PR fix(schedule): apply HTTP full snapshot as authoritative SQLite state #225 已实现的 SQLite 全量快照应用逻辑。
  • 不在本 Issue 中实现客户端接口调用。
  • 不判断客户端 SQLite 是否为空。
  • 不把 HTTP 响应写入客户端 SQLite。
  • 不实现登录成功后的恢复编排。
  • 不实现日历刷新。
  • 不增加手动恢复按钮。
  • 不修改 AppRoot 页面门禁。
  • 不实现增量同步。
  • 不实现分页快照。
  • 不实现游标同步。
  • 不实现冲突合并。
  • 不新增 has_data
  • 不新增 is_new_account
  • 不新增初始化标记。
  • 不新增同步状态。
  • 不新增或修改 PostgreSQL 模型。
  • 不新增或修改 Alembic 迁移。
  • 不修改 Schedule 字段。
  • 不修改现有日程业务规则。
  • 不新增第二套 SQL 或 Repository。
  • 不读取客户端本地数据。
  • 不让 query 中的 account_id 覆盖 Token 身份。
  • 不返回其他账号的任何数据。
  • 不在单条数据损坏时跳过该记录并返回剩余数据。
  • 不返回部分快照。

关键决策与依据

决策一:提供账号级全量快照,不实现增量恢复

  • 备选方案:提供分页、游标或基于 revision 的增量接口。
  • 选择方案:返回认证账号当前完整的日程和周期例外。
  • 选择理由:本接口服务于本地业务数据为空时的基线恢复。客户端缺少完整基线时不能安全应用增量数据。

决策二:账号身份只来自 Token sub

  • 备选方案:从 query 或请求体读取 account_id
  • 选择方案:只使用认证依赖验证后的 Token sub
  • 选择理由:调用方可以伪造请求参数,不能将其作为可信账号身份来源。

决策三:复用现有 Schedule Repository 和 UoW

  • 备选方案:为快照接口编写专用 SQL。
  • 选择方案:复用 list_schedules(include_deleted=True)list_occurrence_overrides()
  • 选择理由:现有 Repository 已维护账号隔离、软删除范围和稳定排序;复制查询会形成第二套数据语义。

决策四:两个集合在同一个 UoW 中读取

  • 备选方案:分别读取日程和周期例外,并在部分失败时返回成功部分。
  • 选择方案:在同一个 ScheduleUnitOfWork 中读取并组装两个集合。
  • 选择理由:客户端恢复需要内部一致的完整快照,不能接受两个集合来自不同读取边界或只返回其中一部分。

决策五:空账号使用两个空数组表达

  • 备选方案:返回 404、特殊新用户响应或额外状态字段。
  • 选择方案:返回 200 和两个空数组。
  • 选择理由:没有云端日程是合法业务状态,不需要增加账号分类或同步状态。

决策六:完整快照失败时不得降级为空结果

  • 备选方案:数据库异常时返回空数组,或者跳过损坏数据后返回剩余记录。
  • 选择方案:只有确认账号确实没有云端记录时才返回空数组;查询、校验或序列化失败统一返回 500。
  • 选择理由:PR fix(schedule): apply HTTP full snapshot as authoritative SQLite state #225 将快照作为权威状态应用,空数组和缺失记录具有清理本地数据的语义,不能作为错误降级结果。

决策七:HTTP 层只补充传输和跨记录约束

  • 备选方案:在 HTTP 层复制全部日程业务规则。
  • 选择方案:HTTP 层只验证账号归属、时间格式、字段边界、重复 ID 和周期例外引用。
  • 选择理由:日程创建、修改、周期和提醒组合规则已经由现有业务层维护,复制规则会造成语义漂移。

决策八:认证依赖继续复用现有实现

  • 备选方案:快照路由自行解析 JWT。
  • 选择方案:复用 create_authenticated_account_dependency() 和现有认证错误处理器。
  • 选择理由:避免重复认证实现,并确保 /auth/access 与快照接口使用同一 Token 语义。

基本概念

  • 云端全量快照:认证账号当前全部日程和周期例外组成的完整只读集合。
  • 认证账号:Bearer Token 验证成功后,从 Token sub 取得的账号身份。
  • 快照读取器:业务层暴露的账号级只读端口。
  • 完整结果:日程和周期例外在同一 UoW 中查询、一起校验并一起返回。
  • 部分快照:只返回其中一个集合,或者跳过损坏记录后返回剩余数据;本接口禁止这种行为。
  • 空账号:云端确认不存在该账号日程和周期例外的合法状态。
  • 权威状态:PR fix(schedule): apply HTTP full snapshot as authoritative SQLite state #225 将 HTTP 全量快照解释为该账号当前完整云端投影。
  • 设备本地运行状态next_trigger_atsnoozed_untilgeofence_armedsync_status 等不属于云端快照的数据。

与其他 Proposal、Issue 和 PR 的关系

  • 共享字段、路径和错误语义以 TimeFlow《共享接口契约 v1》为唯一依据。
  • Bearer Token 的签发和验证继续复用现有认证实现。
  • Schedule 字段、PostgreSQL 模型、迁移和 Repository 继续复用现有云端日程实现。
  • PR #225 已实现 HTTP 全量快照到 SQLite 的权威状态应用。
  • 本 Issue 为 PR fix(schedule): apply HTTP full snapshot as authoritative SQLite state #225 提供上游 HTTP 数据源。
  • 客户端 SQLite 空库检测、调用时机和登录后恢复编排不在本 Issue 中实现。
  • 本 Issue 完成只代表后端数据出口可用,不代表客户端已经完成自动调用和页面刷新。

待后续客户端 Issue 决策

以下内容不阻塞本 Issue:

  • 客户端在登录流程的哪个阶段检查本地业务数据库。
  • 如何定义“本地业务数据为空”。
  • 由哪个应用服务发起 HTTP 快照请求。
  • 快照落库失败后的重试策略。
  • 快照恢复失败时如何向用户提示。
  • 本地已有业务数据时是否允许手动恢复。
  • 完成恢复后如何刷新日历页面。
  • 恢复后如何重新调度本地提醒。

服务端不得根据这些客户端决策改变本期请求或响应结构。

接口示例

已有云端数据

请求:

GET /api/v1/schedule/snapshot
Authorization: Bearer <valid-token>

响应:

200 OK
Content-Type: application/json
{
  "schedules": [
    {
      "id": "schedule_001",
      "account_id": "acc_001",
      "schedule_type": "time",
      "schedule_kind": "once",
      "title": "Cloud schedule",
      "is_all_day": false,
      "start_time": "2026-08-17T01:00:00+00:00",
      "end_time": null,
      "timezone": "Asia/Shanghai",
      "recurrence_rule": null,
      "location_name": null,
      "latitude": null,
      "longitude": null,
      "reminder_type": null,
      "reminder_trigger_at": null,
      "reminder_offset_minutes": null,
      "reminder_strength": null,
      "reminder_disposition_state": null,
      "status": "active",
      "revision": 1,
      "created_at": "2026-08-14T03:00:00+00:00",
      "updated_at": "2026-08-14T03:00:00+00:00",
      "deleted_at": null
    }
  ],
  "occurrence_overrides": []
}

空账号

200 OK
Content-Type: application/json
{
  "schedules": [],
  "occurrence_overrides": []
}

缺少 Token

401 Unauthorized
Content-Type: application/json
{
  "error": {
    "code": "AUTH_REQUIRED",
    "message": "Authentication required"
  }
}

Token 无效

401 Unauthorized
Content-Type: application/json
{
  "error": {
    "code": "AUTH_INVALID_TOKEN",
    "message": "Invalid access token"
  }
}

查询、校验或序列化失败

500 Internal Server Error
Content-Type: application/json
{
  "error": {
    "code": "SCHEDULE_SNAPSHOT_INTERNAL_ERROR",
    "message": "Schedule snapshot unavailable"
  }
}

测试要求

业务查询测试

必须验证:

  • 在同一个 UoW 中读取两个集合。
  • 查询使用认证账号 ID。
  • list_schedules() 使用 include_deleted=True
  • list_occurrence_overrides() 读取账号全部周期例外。
  • 查询过程不执行 commit()
  • 空账号返回两个空 tuple。
  • 空白账号 ID 被拒绝。
  • 业务层不依赖 FastAPI、Pydantic 或 SQLAlchemy。

HTTP 路由测试

必须验证:

  • 有效 Token 返回完整响应结构。
  • query 中伪造的 account_id 不改变查询账号。
  • 缺少 Token 时不调用读取器。
  • 无效 Token 时不调用读取器。
  • active 和 deleted 日程同时返回。
  • 空账号返回 200 和两个空数组。
  • 跨账号日程触发脱敏 500。
  • 重复日程 ID 触发脱敏 500。
  • 重复周期例外 ID 触发脱敏 500。
  • 重复 schedule_id + occurrence_start 触发脱敏 500。
  • 缺失父日程触发脱敏 500。
  • 父日程不是 recurring 时触发脱敏 500。
  • 错误 replacement 引用触发脱敏 500。
  • revision < 1 触发脱敏 500。
  • 无时区 created_at 触发脱敏 500。
  • 无时区 updated_at 触发脱敏 500。
  • 无时区 occurrence_start 触发脱敏 500。
  • 数据库异常内容不出现在响应中。
  • 数据库连接串不出现在日志中。
  • 查询失败不会返回空快照或部分快照。

组合根测试

必须验证:

  • create_app() 允许注入 schedule_snapshot_reader
  • 快照路由复用应用中的 Token 服务。
  • 测试注入读取器时不会意外创建数据库连接。
  • OpenAPI 中存在 /api/v1/schedule/snapshot
  • OpenAPI 记录成功、401 和 500 响应。

PostgreSQL 集成测试

必须验证:

  • 通过认证接口取得真实 Bearer Token。
  • 使用真实 PostgreSQL 创建账号日程。
  • 有效 Token 可以取得该账号的日程。
  • 空账号返回两个空数组。
  • 响应中不存在其他账号数据。

质量门禁

backend/ 执行:

bash scripts/check.sh

需要保证以下检查全部通过:

uv run ruff check .
uv run ruff format --check .
uv run mypy
uv run pytest
uv run alembic heads

要求:

  • Ruff 无错误。
  • 格式检查无差异。
  • mypy 无错误。
  • pytest 全部通过。
  • 分支覆盖率不低于仓库要求。
  • 架构检查通过。
  • Alembic 只有一个 head。

范围检查:

git diff --check upstream/main...HEAD
git diff --name-only upstream/main...HEAD

不得出现:

  • frontend/
  • backend/alembic/
  • backend/src/timeflow/data/models.py
  • backend/src/timeflow/data/repositories/schedule.py
  • 新的客户端 SQLite 检测
  • 新的恢复按钮
  • 新的 AppRoot 恢复编排

后续验收方向

  • GET /api/v1/schedule/snapshot 已注册并受 Bearer Token 保护
  • 请求不需要 body,也不依赖 query 参数
  • 账号身份只能来自 Token sub
  • 伪造 account_id 不会改变查询账号
  • 响应顶层只有 schedulesoccurrence_overrides
  • active 和 deleted 日程会同时返回
  • 周期例外只引用同一响应中的当前账号日程
  • 日程和周期例外在同一个 UoW 中读取
  • 日程和周期例外按共享契约稳定排序
  • 空账号返回 200 和两个空数组
  • 数据库异常不会返回 200 空快照
  • 任一集合查询失败时不会返回部分快照
  • 无时区时间和损坏引用会触发统一 500
  • 错误响应和日志不会泄露 Token、SQL、连接串或异常原文
  • 响应结构与 PR fix(schedule): apply HTTP full snapshot as authoritative SQLite state #225 使用的 CloudScheduleSnapshot 一致
  • 本 Issue 不重复修改 PR fix(schedule): apply HTTP full snapshot as authoritative SQLite state #225 的 SQLite 应用逻辑
  • 本 Issue 不实现客户端调用、空库判断或登录后编排
  • 不修改 PostgreSQL 模型、Repository 或 Alembic 迁移
  • Ruff、格式检查、mypy、pytest、架构检查和 Alembic 单头检查全部通过

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions