# Core Server Calendar Service 设计

## 1. 背景

Core Server 需要通过稳定的配置名称提供日历事件查询能力。日历来源由用户上传的静态 ICS/iCalendar 文件提供，Core Server 不访问外部 Calendar API，也不保存展开后的事件。

Calendar Service 支持：

- 独立上传并校验 ICS 文件；
- 使用内容 SHA-256 对文件寻址和去重；
- 管理配置名称、时区和文件 hash；
- 查询时间窗口内的全日、定时和重复事件；
- 判断某个绝对时间是否落在一个或多个事件期间；
- 周期性清理不再被配置引用的文件。

## 2. 核心语义

### 2.1 配置与文件

- `name` 是精确匹配、区分大小写且创建后不可修改的唯一 key。
- `name` 可包含空格，但不能为空、不能超过 128 个字符，也不能包含 `/`。
- `time_zone` 必须是有效的 IANA 时区，用于浮动时间和全日事件。
- `file_hash` 是上传接口返回的 64 位小写 SHA-256。
- 不同配置可以引用同一文件 hash。
- 删除配置或换绑文件时不立即删除旧文件，由清理任务统一回收。

### 2.2 时间窗口

`ListCalendarEvents` 使用 `[start_time, end_time)` 半开区间。开始时间必须早于结束时间，最大跨度由 `maxQueryDays` 控制，默认 366 天。

`HasCalendarEvent` 使用相同的半开区间语义：事件开始时刻包含，结束时刻不包含。只有 S3 读取和 ICS 解析均成功后，服务才能返回 `has_event=false`。

返回结果按有效开始时间、summary、UID 升序排列。

## 3. API 设计

| RPC / HTTP | 用途 |
| --- | --- |
| `UploadCalendarFile` | gRPC 文件上传，接收 `bytes file` 并返回 `file_hash` |
| `POST /api/v1/core/calendar/files` | Gateway 手工注册的 multipart 上传接口，字段名为 `file` |
| `ListCalendarEvents` / `GET /api/v1/core/calendar/events` | 查询窗口内事件 |
| `HasCalendarEvent` / `GET /api/v1/core/calendar/events/check` | 判断某时刻是否存在事件 |
| `GetCalendarConfig` / `GET /api/v1/core/calendar/configs/{name}` | 查询配置 |
| `ListCalendarConfigs` / `GET /api/v1/core/calendar/configs` | 分页查询配置 |
| `CreateCalendarConfig` / `POST /api/v1/core/calendar/configs` | 使用已上传的 `file_hash` 创建配置 |
| `UpdateCalendarConfig` / `PUT /api/v1/core/calendar/configs/{name}` | 修改时区或 `file_hash` |
| `DeleteCalendarConfig` / `DELETE /api/v1/core/calendar/configs/{name}` | 删除配置 |

上传 RPC 不声明 `google.api.http`。Gateway 使用 `runtime.ServeMux.HandlePath` 注册 multipart 路由，解析文件后通过生成的 gRPC Client 调用 RPC，以保留 gRPC middleware、metadata、trace 和统一错误响应。

multipart 文件最大为 3 MiB。HTTP Authorization 等请求 metadata 必须随 RPC 转发。

核心实体：

```proto
message CalendarConfig {
  uint64 id = 1;
  int64 created_at = 2;
  optional int64 updated_at = 3;
  string name = 4;
  string time_zone = 5;
  string file_hash = 6;
}

message CalendarEvent {
  string summary = 1;
  CalendarEventTime start = 2;
  CalendarEventTime end = 3;
  string source_calendar_name = 4;
  string uid = 5;
}
```

Create 的 `file_hash` 必填；Update 使用 optional 字段，省略表示保持当前文件引用。

## 4. 文件存储

Calendar Repo 同时负责配置数据库访问和 Calendar Asset 操作。Calendar Service 将 Core Server 注入的 Asset 限定为 `calendar_service/` 后传给 Repo，Repo 只负责追加 `calendar_files/` 子目录。因此文件最终存放在 `calendar_service/calendar_files/` 下。

上传流程：

1. Service 校验文件非空、大小不超过 3 MiB，并严格解析全部 VEVENT；
2. Repo 对未经规范化的原始字节计算 SHA-256；
3. S3 对象 key 直接使用 64 位 hash，不增加扩展名；
4. 已存在相同 key 时直接返回，实现幂等上传；
5. 新文件使用 `Content-Type: text/calendar` 写入；
6. 返回 `file_hash`，由调用方创建或更新配置。

数据库表：

```text
id         bigint unsigned primary key
created_at datetime(3) not null
updated_at datetime(3) null
name       varchar(128) not null
time_zone  varchar(64) not null
file_hash  varchar(64) not null
```

索引为 `uk_calendar_config_name(name)` 和 `idx_calendar_config_file_hash(file_hash)`。

Create/Update 必须验证 hash 格式及对象存在性。文件内容不可变；内容变化必须重新上传并产生新的 hash。

## 5. ICS 解析

每次 List/Has 查询均按配置 hash 从 S3 读取原始文件并解析，不增加进程缓存。

支持：

- `VALUE=DATE` 全日事件；
- UTC、带 IANA `TZID` 和浮动的 DATE-TIME；
- `DTEND` 或正数 `DURATION`；
- `RRULE`、`RDATE` 和 `EXDATE`；
- 单次 `RECURRENCE-ID` 移动、修改和取消；
- `STATUS:CANCELLED`；
- 跨日、跨时区和夏令时。

浮动时间使用配置的 `time_zone`。全日事件保留本地日期表达，结束日期仍为开区间。定时事件输出绝对 Timestamp。

不支持已废弃的 `EXRULE` 和 `RECURRENCE-ID;RANGE=THISANDFUTURE`。文件中任意 VEVENT 缺少 UID、时间范围无效、时区或重复规则无法解析、使用不支持语义时，整份上传返回 InvalidArgument，不写入 S3。

重复实例只在请求窗口附近展开，并在转换后再次执行 `[start, end)` 相交过滤。空 summary 使用 CalendarConfig Name；UID 直接来自 ICS。

## 6. 无引用文件清理

Calendar Service 使用 JobService 注册 `CleanupCalendarFiles`：

```yaml
services:
  calendar:
    maxQueryDays: 366
    fileCleanup:
      cronDisable: false
      cronSpec: "0 3 * * *"
      minAge: "24h"
```

任务默认每日 03:00 执行：

1. 查询数据库中所有 distinct `file_hash`；
2. 列举 `calendar_service/calendar_files/` 下的对象；
3. 忽略非 64 位小写 SHA-256 key；
4. 保留仍被引用或最后修改时间未超过 24 小时的文件；
5. 删除其余对象。

24 小时宽限保证上传和绑定配置可以分两步完成。数据库查询或 S3 列举失败时本轮不删除；单个删除失败只记录错误并继续。

## 7. 错误处理与发布

| 场景 | gRPC code |
| --- | --- |
| 文件缺失、超过 3 MiB、ICS/事件/重复规则非法 | `InvalidArgument` |
| Name、Timestamp、范围、时区或 hash 格式非法 | `InvalidArgument` |
| 配置引用的 hash 在 S3 中不存在 | `FailedPrecondition` |
| S3 读写或检查失败 | `Unavailable` |
| 已保存文件损坏或未分类内部错误 | `Internal` |
| 配置不存在 | `NotFound` |

Calendar 功能尚未进入主分支，因此直接修正初始 migration，不迁移旧 Calendar ID 数据。

发布前确认：

1. Core Server 配置了 Asset bucket；
2. Runtime IAM 对 bucket 的 `calendar_service/calendar_files/` 前缀具备 list、read、write、delete 权限；
3. multipart 上传、配置创建、全日/定时/重复事件查询和清理任务在目标环境验证通过。
