邮箱、日历与会议联动开发说明
本文面向产品、前端、服务端与邮件集成开发,整理自「日历邮箱联动设计」对话及补充的联动时序说明。目标是说明 teleMail 多邮箱账号、统一日历与独立会议模块应该如何协作,而不是复刻某一家邮箱产品的界面。
本文的数据模型和服务接口是开发设计建议,不代表已经上线或已经批准建设后端。当前仓库仍为静态、无持久化的原型;实际已实现范围见第九节。未确定的产品规则集中列在第十一节,开发不应自行补齐这些决策。
一、先回答:两个邮箱收到同一邀请,日历显示几条?
建议采用「统一日历视图 + 多邮箱身份 + 邀请关联去重」:同一用户的工作邮箱 A、个人邮箱 B 收到同一个日程邀请,统一日历显示一个日程块,详情保留两份来源和两个参与身份的状态。
查看流程图源码与文本描述
flowchart LR mailA["工作邮箱 A 的邀请"] -->|"同一可信 UID"| event["统一事件 E1001"] mailB["个人邮箱 B 的邀请"] -->|"同一可信 UID"| event event --> entry["用户日历:一个日程块"] entry --> identityA["身份 A:已接受"] entry --> identityB["身份 B:未回应"]
图示为两个不同参与地址都属于当前用户的例子;如果只是同一个别名的两份投递,则仍只有一个参与身份。
- A 接受、B 未回应:显示一个日程;A 为已接受,B 为未回应。
- A 接受、B 拒绝:仍显示一个日程;拒绝的是 B 这个参与身份,不是取消整个活动。
- A 和 B 都接受:仍显示一个日程,不自动视为组织者邀请了同一个参与地址。
- 两封邮件标题、时间相同,但邀请标识不同:显示两个日程,不能按标题和时间强行合并。
- 同一邀请的修改邮件到达:更新已有日程,并保留旧邮件作为历史,不增加一个新日程。
关键区别:邮件是传输记录,事件是日程事实,参与者是回应身份,用户日历是展示关系。四者不能合成一张「邮件事件表」。
二、领域边界:事件、会议、邮件分别负责什么?
查看流程图源码与文本描述
flowchart LR message["邮件:邀请和变更记录"] -->|"解析与来源验证"| event["日历事件:时间与参与关系"] event -->|"供用户查看"| view["统一日历视图"] event -.->|"可选 meetingId"| meeting["内部会议能力"] event -.->|"可选外部链接"| external["外部会议能力"]
实线表示信息处理/展示,虚线表示可选关联。这是逻辑职责图,不代表已经建设了这些生产服务。
日历事件:时间安排的主体
事件负责标题、开始/结束时间、时区、组织者、参与者、重复规则、修订版本和取消状态。普通提醒、线下活动、会议都可以对应事件;一个事件不一定有会议能力。
会议:可选的业务能力
会议模块负责会议室、加入会议、内部聊天、会议纪要等能力,通过 meetingId 与事件关联。事件是安排,会议是能力,两者不要互相复制时间与参与状态作为双主数据。
外部邀请中出现 Teams 等加入链接,不意味着必须创建 teleMail 内部会议。可保存外部会议提供方及加入链接;内部会议 ID 仍为空。外链需要协议和安全校验,不能执行邀请中的脚本或任意 URI。
邮件:邀请与回应的传输通道
邮件模块负责多个邮箱账号授权、收件、解析日历 MIME 内容、发出邀请或回应。日历调度层识别新邀请、更新、回应和取消,再把结果提供给统一日历。普通 .ics 附件可能仅供导入,不能仅凭文件扩展名就认定它是要求回复的会议邀请。
内部创建的活动先生成事件,再通过邮件邀请外部参与者;外部发来的邀请直接进入收件处理流程,不要求先经过内部会议模块。
三、身份与去重规则
四种 ID,不要混用
userId:企业协同用户,一个用户可以绑定多个邮箱。mailAccountId:邮箱连接及授权账号,用于同步和发送权限,不是事件 ID。eventId:teleMail 内部事件 ID,供日历、会议和邮件之间引用。icalUid:外部 iCalendar 的 UID,用于识别事件或重复系列;不能拿某封邮件的 Message-ID 代替。
重复系列由 UID 识别,单次实例还需要 RECURRENCE-ID。某次会议从周五改到周四时,实例标识仍指向原定周五,而不是改后的开始时间。SEQUENCE 属于版本,不属于恒定的事件身份。协议依据:RFC 5545。
先验证来源,再建立关联
查看流程图源码与文本描述
flowchart TD
received["收到日历邮件"] --> parse["解析方法、UID、实例与版本"]
parse --> trusted{"来源可信且有访问权限?"}
trusted -->|"否或无法确认"| conflict["隔离记录,不覆盖已有事件"]
trusted -->|"是"| matched{"隔离范围内匹配同一事件?"}
matched -->|"组织者冲突"| conflict
matched -->|"没有"| create["新建规范事件"]
matched -->|"已有可信关联"| revision["按方法检查实例与修订"]
create --> link["幂等保存邮件、账号、参与身份关系"]
revision --> link
link --> entry["唯一用户视图关系,按实例展示"]建议在隔离范围内寻找同 UID 的事件,并校验组织者与可信来源的连续性。已可信关联的重复系列,再按实例标识定位具体一次事件;不能简单把「租户 + UID + 组织者」字符串拼接后就认为安全。
不同用户或租户的私人邮箱数据不能因 UID 相同而直接全局合并。推荐先在用户可访问的数据范围内建立规范事件,跨用户共享必须通过明确的访问关系。对于同 UID 但组织者冲突、来源不可信的邮件,隔离为冲突记录,不覆盖原事件。组织者和参与者伪造是邮件日历协议需要处理的安全风险。协议依据:RFC 6047。
收件账号不一定是被邀请身份
A 邮箱可能收到发给别名的邀请,B 邮箱也可能收到转发副本。因此需要区分:
- 哪个账号收到邮件:来源关系。
ATTENDEE中实际被邀请的地址:参与身份。- 当前用户可验证控制哪些地址:回应权限。
转发副本不自动授予代他人接受的权限。两个账号收到的都是同一个别名时,也不应生成两个不同参与者。地址规范化需要保留原值,不擅自去掉 +tag、点号或推断两个地址属于同一身份。
四、建议数据结构与唯一约束
以下为逻辑模型,可由真实业务服务落地为关系表或等价存储。隔离字段必须参与实际查询与唯一约束;不能只写在文档里。
核心关系图:事件、用户视图与参与回应
查看流程图源码与文本描述
erDiagram
direction LR
User ||..o{ UserCalendarEntry : 拥有视图
CalendarEvent ||..o{ UserCalendarEntry : 被用户查看
CalendarEvent ||..o{ EventOccurrence : 展开实例
CalendarEvent ||..o{ EventAttendee : 邀请身份
EventAttendee ||..o{ AttendeeResponse : 按范围回应
Meeting o|..o{ CalendarEvent : 可选关联
User { string id PK }
CalendarEvent { string id PK
string scopeId
string icalUid
string meetingId FK
}
UserCalendarEntry { string userId FK
string eventId FK
string visibility
}
EventOccurrence { string id PK
string eventId FK
string recurrenceKey
}
EventAttendee { string id PK
string eventId FK
string identityKey
}
AttendeeResponse { string attendeeId FK
string responseScope
string status
}
Meeting { string id PK }读图:圆圈表示可选,单线表示一,分叉表示多;图中虚线为非标识关联,子记录有自己的标识。用户与事件通过 UserCalendarEntry 建立多对多视图关系;参与者及其回应另存。会议只表示可选关联,不额外规定一个会议能关联几个事件的产品限制。
来源关系图:多账号、多邮件与外部映射
查看流程图源码与文本描述
erDiagram
direction LR
User ||..o{ MailAccount : 绑定账号
User ||..o{ VerifiedIdentity : 验证身份
CalendarEvent ||..o{ EventAccountLink : 关联来源
MailAccount ||..o{ EventAccountLink : 收到邀请
CalendarEvent ||..o{ EventMessageLink : 保留历史
MailAccount ||..o{ EventMessageLink : 提供邮件
CalendarEvent ||..o{ ExternalEventBinding : 关联外部事件
MailAccount ||..o{ ExternalEventBinding : 提供同步上下文
CalendarEvent ||..o{ ResponseOperation : 记录回应操作
User { string id PK }
MailAccount { string id PK
string userId FK
}
VerifiedIdentity { string id PK
string userId FK
string address
}
CalendarEvent { string id PK }
EventAccountLink { string eventId FK
string mailAccountId FK
}
EventMessageLink { string eventId FK
string mailAccountId FK
string providerMessageId
}
ExternalEventBinding { string eventId FK
string mailAccountId FK
string externalEventId
}
ResponseOperation { string id PK
string eventId FK
string deliveryStatus
}为避免图过密,此图仅列关键关系;账号与已验证身份之间的授权映射、实例外键及回应操作的目标身份仍按下文约束实现。PK 是主键,FK 是外键;复合唯一约束见各实体说明,不把 icalUid 单独标为唯一。
1. MailAccount 与 VerifiedIdentity
MailAccount 保存账号 ID、所属用户、隔离范围、提供方、授权状态和连接状态。凭据由安全的账号服务管理,不下发给日历前端。
VerifiedIdentity 保存用户拥有且已验证的邮箱地址或内部协同身份。邮箱别名与账号之间为显式授权映射,不凭地址相似性自动认领。
2. CalendarEvent:事件/系列主记录
interface CalendarEvent {
id: string;
scopeId: string;
icalUid: string;
source: "internal" | "ics" | "provider";
organizerIdentity: string;
title: string;
description: string;
start: CalendarTime;
end: CalendarTime;
location?: string;
recurrenceRule?: string;
sequence: number;
dtstamp: string;
revision: string; // 服务端并发控制版本,不等同于 SEQUENCE
status: "active" | "cancelled";
meetingId?: string;
externalMeeting?: { provider: string; joinUrl: string };
}
type CalendarTime =
| { kind: "date"; value: string } // 全天日期,不强制换算为 UTC 午夜
| { kind: "instant"; value: string; timeZone: string }
| { kind: "floating"; value: string }; // 无时区时间,需明确解释策略
记录经过验证的来源绑定。icalUid 不应设置为跨租户、跨私人邮箱范围的裸全局唯一键。单次普通事件和重复系列都使用内部 ID。
3. EventOccurrence:重复事件实例与例外
保存 id、eventId、原始 recurrenceId、实例版本、覆写的时间/标题以及实例取消状态。保留实例标识的值类型、时区语义与原始值。
建议唯一约束:(eventId, normalizedRecurrenceKey)。普通非重复事件使用显式的单次键;不要依赖数据库对空值唯一约束的不同处理。重复系列可按查询时间窗口展开,不必无限预生成实例。仅取消某一次、取消整个系列和修改后续实例必须分开处理。
4. UserCalendarEntry:用户视图关系
保存 userId、eventId、个人可见性、个人提醒设置和展示偏好。唯一约束为隔离范围内的 (userId, eventId)。系列在某一时间窗口内按实例展开,所以「去重」不是把整个重复系列压成一个时间块。
同一用户只建立一条事件视图关系,是多账号不重复展示的关键。这个关系不是参与者状态:隐藏日程不等于拒绝邀请,删除个人视图不等于组织者取消活动。
5. EventAttendee 与 AttendeeResponse:身份及回应
EventAttendee 保存 eventId、参与身份类型、邀请原始地址、规范身份键、可选 userId。唯一约束为 (eventId, attendeeIdentityKey),不能用 (eventId, userId),否则一个用户的两个邮箱身份会被错误合并。
AttendeeResponse 保存参与者、回应范围(整个系列/指定实例)、NEEDS_ACTION、ACCEPTED、TENTATIVE 或 DECLINED,并保存回应所针对的事件版本、回应时间、传输结果。每个参与身份、每个回应范围的当前状态唯一,历史另存。
事件版本与参与者回应版本要分开。组织者修改重要内容后,是否要求重新回应,应由明确规则驱动;不要把过去的接受标记无条件当作接受了新时间。
6. EventAccountLink:账号来源关联
保存 eventId、mailAccountId、可选的参与身份关联以及来源连接状态。一个事件可以关联多个收件账号;其中某个账号断开,不影响另一个账号的关联,也不抹掉邀请历史。
账号与参与身份可能多对多,必要时使用独立的映射表,不假设收件账号就是回应地址。账号筛选应按来源或明确映射匹配,合并视图仍只显示一份事件。
7. EventMessageLink:邮件历史关联
保存 eventId、可选实例 ID、账号 ID、提供方邮件 ID、方法、SEQUENCE、DTSTAMP、内容摘要、解析和处理结果。邀请、更新、取消、回应均保留原邮件关系。
收件幂等键建议为 (mailAccountId, providerMessageId);只有 MIME Message-ID 时,还需连接器给出稳定的本地邮件标识,不能假定 Message-ID 永远唯一可信。重复投递的相同有效载荷也需要业务去重。取消是状态变化,不是硬删除这些关联。
8. Meeting、ExternalEventBinding 与 ResponseOperation
Meeting:内部会议能力,通过meetingId关联,不存一套独立且不同步的日程主数据。ExternalEventBinding:账号、提供方、外部日历 ID、外部事件 ID、版本标记与内部事件 ID 的映射。唯一键限定在提供方和账号上下文中。ResponseOperation:回应操作 ID、幂等键、操作者、目标身份、目标事件版本、通知选项、发送/失败/确认状态和重试记录。
这三个实体用于生产集成。本次仅编写设计说明,不在静态原型中添加数据库、凭据、发信或运行时 API。
五、完整业务流程
1. 创建与发送邀请
查看流程图源码与文本描述
sequenceDiagram participant 组织者 participant 日历调度 participant 邮件连接器 participant 受邀邮箱 组织者->>日历调度: 创建事件并选择授权发件身份 日历调度->>日历调度: 保存事件、UID、参与者与可选会议关联 日历调度->>邮件连接器: 投递 METHOD REQUEST 邮件连接器->>受邀邮箱: 发送邀请 邮件连接器-->>日历调度: 提交或失败结果 日历调度-->>组织者: 展示真实投递状态
需要会议时再关联会议能力。失败不能显示成已成功邀请。
2. 两个邮箱收到同一邀请
查看流程图源码与文本描述
sequenceDiagram participant 邮箱A participant 邮箱B participant 日历调度 participant 统一读模型 邮箱A->>日历调度: 同步 M001 日历调度->>日历调度: 解析并验证来源和参与身份 日历调度->>统一读模型: 创建或匹配 E1001,保存 A 的关系 邮箱B->>日历调度: 同步 M002,同一可信 UID 日历调度->>日历调度: 匹配 E1001 并检查修订 日历调度->>统一读模型: 补充 B 的来源与身份关系 统一读模型-->>日历调度: 一个日程,两份来源,独立回应
事务或等价原子机制应保证 A、B 同时到达也不会创建两个展示关系。无法可靠判定是否同一邀请时保留冲突待处理,不能用模糊匹配静默合并。
3. 接受、待定与拒绝
查看流程图源码与文本描述
sequenceDiagram participant 用户 participant 邮件或日历界面 participant 回应服务 participant 单一回应通道 用户->>邮件或日历界面: 选择身份 A 与接受、待定或拒绝 邮件或日历界面->>回应服务: 提交目标身份、版本与幂等键 回应服务->>回应服务: 校验身份、权限、取消状态与版本 回应服务-->>邮件或日历界面: 返回操作 ID,显示处理中 回应服务->>单一回应通道: 需要通知时提交回应 单一回应通道-->>回应服务: 提交或失败结果 回应服务->>回应服务: 保存 A 的选择与发送结果,B 不变 回应服务-->>邮件或日历界面: 更新邮件卡片和日历读模型
邮件入口默认定位可验证的受邀身份;日历入口多身份时明确选择。单一回应通道指提供方 API 或授权邮箱 iTIP 回应,不能两种都发送。无需通知时跳过通道,保存本地选择;失败以同一操作幂等重试。缓存按受影响用户、事件和身份失效。
发送成功仅证明连接器接受了操作,不保证组织者已经阅读或更新日历。界面应区分「本地已选择」「正在发送」「发送失败」「已提交回应」;如提供方没有回执,不显示未经证实的「组织者已确认」。
「不向组织者发送邮件」也不是一个协议层面的通用能力。若产品允许仅更新本地选择,需要明确提示组织者不会得知,并独立保存本地状态;提供方 API 是否支持静默回应由连接器定义。
4. 修改与取消
同一 UID/实例的事件修订按 SEQUENCE 处理;同版本需要结合 DTSTAMP 判断顺序。回应携带所回应的修订版本,不应作为事件内容更新。实现时按方法分别处理,不能把收到的所有 ICS 都当作最新事件覆盖。协议依据:RFC 5546。
- 修改:可信组织者发来的新 REQUEST 更新原事件,保留旧邮件与修订历史,向界面标出时间等重要变更。
- 取消:可信 CANCEL 更新指定实例或系列的取消状态,保留取消版本及历史。旧邀请晚到时不能复活已取消事件。
- 参与者拒绝:只更新该身份的回应,不取消组织者的事件,也不移除其他身份的接受记录。
- 普通邮件删除:不直接删除日程;日程取消也不删除收件箱邮件。
- 更高版本再次邀请:按协议、权限与产品规则处理,不仅凭本地取消标记永久拒绝所有后续版本。
若两个来源的同版本内容冲突,记录冲突并按可信来源策略解决;收件时间不是事件修订时间,也不能作为唯一裁决依据。
查看流程图源码与文本描述
flowchart TD
validated["已验证来源的日历消息"] --> method{"消息方法?"}
method -->|"REQUEST 或 CANCEL"| revision{"相同 UID 与实例的修订检查"}
revision -->|"旧修订或重复"| history["保留历史,不覆盖当前状态"]
revision -->|"同版本内容冲突"| conflict["记录冲突,按可信策略解决"]
revision -->|"可应用的修订"| action{"REQUEST 还是 CANCEL?"}
action -->|"REQUEST"| update["更新原事件或实例,标出重要变化"]
action -->|"CANCEL"| cancel["取消指定范围,保留取消版本"]
method -->|"REPLY"| response["校验身份与回应版本,只更新该参与者"]
method -->|"其他方法"| unsupported["交给专用处理或标记不支持"]修订检查结合 SEQUENCE、DTSTAMP、实例范围与内容;旧邀请不能让已取消事件复活。REPLY 不覆盖事件内容。
六、日历与邮件的展示契约
统一日历的查询结果应该包含事件/实例 ID、标题、时间、来源账号列表、当前用户各参与身份的状态,以及用于展示的派生状态。派生状态不能替代原始参与状态。
建议的显示优先级为:取消状态优先;未取消时,任一身份接受则显示已接受,否则任一待定则显示待定,否则任一未回应则显示未回应,全部拒绝则显示已拒绝。此优先级是产品建议,仍需确认,且应同时标明「存在其他身份未回应」。
查看流程图源码与文本描述
flowchart TD
event{"事件或当前实例已取消?"} -->|"是"| cancelled["显示已取消"]
event -->|"否"| accepted{"任一身份已接受?"}
accepted -->|"是"| showAccepted["显示已接受"]
accepted -->|"否"| tentative{"任一身份待定?"}
tentative -->|"是"| showTentative["显示待定"]
tentative -->|"否"| pending{"任一身份未回应?"}
pending -->|"是"| showPending["显示未回应"]
pending -->|"否且全部拒绝"| showDeclined["显示已拒绝"]此图仅描述「至少一个本人受邀身份」时的建议聚合规则,不决定日程是否隐藏;无本人受邀身份的导入事件不套用该规则。所有身份的原始状态和发送结果始终保留。
日程详情中,每个身份显示地址、来源账号、回应和发送结果。切换身份后,接受/待定/拒绝作用于所选身份。隐藏某一账号的来源筛选时,如果另一账号仍命中该事件,日程继续显示。
邮件邀请卡片显示当前邀请身份的状态,并链接到统一事件。日历反向查看邮件时,可列出多个账号的邀请、修改、取消历史;导航使用内部事件/实例 ID,不能靠标题搜索或仅传一个日期。
七、服务接口建议
以下是未来业务服务的契约草案,不是当前 Next.js 原型的 API Routes,也不要求前端自己发送 ICS。
interface ReplyInvitationCommand {
eventId: string;
occurrenceId?: string;
responseScope: "series" | "occurrence";
attendeeIdentityId: string;
mailAccountId?: string;
expectedRevision: string;
action: "ACCEPTED" | "TENTATIVE" | "DECLINED";
notifyOrganizer: boolean;
note?: string;
idempotencyKey: string;
}
interface ReplyInvitationResult {
operationId: string;
localStatus: "ACCEPTED" | "TENTATIVE" | "DECLINED";
deliveryStatus: "not-requested" | "pending" | "submitted" | "failed";
eventRevision: string;
}
操作者与隔离范围从认证上下文获得,不信任客户端提交的 userId。回应身份、账号、事件之间的权限关系由服务校验。版本冲突时返回新版本供用户确认,不静默接受已经改期的日程。幂等键限定到操作者与操作范围,重试返回原操作结果。
查询能力至少包括:按时间窗口/账号筛选日程;获取日程及本人身份状态;获取来源邮件;查询回应操作结果。组织者创建、修改和取消属于独立的有权限命令,不复用参与者拒绝接口。
八、邮件 ICS 与外部日历同步如何选择?
路径 A:邮件驱动
收件连接器解析邀请、修改、取消,回应通过邮件传输。优点是与多邮箱收件统一;限制是只有收到新消息才知道外部变化,不能承诺实时反映用户在外部日历的操作。
路径 B:提供方日历同步
通过提供方日历 API 同步事件与状态,保存外部事件映射、同步游标和授权范围。若同一事件也从邮件到达,应进入同一规范化事件,而不是再显示一份。
一个事件可有多个来源,但需要明确各字段的来源权威、同步冲突策略和回应通道。不能开启双向同步后又把自己发送的更新当作新的邀请无限循环。内部来源 ID、幂等标记和外部映射应参与循环检测。
查看流程图源码与文本描述
flowchart LR mail["路径 A:邮件 ICS"] --> mailNormalize["解析、来源验证与邮件幂等"] provider["路径 B:提供方日历 API"] --> providerNormalize["授权、外部映射与同步版本"] mailNormalize --> match["匹配同一规范事件"] providerNormalize --> match match --> readModel["统一事件与参与状态读模型"] readModel --> calendar["日历展示一次"] readModel --> message["邮件邀请卡片"]
两条路径是方案对照,不表示一期同时启用;多来源汇入同一事件,也不代表对外回应要发两次。
建议先明确一期选择哪条路径,再设计连接器;数据模型保留扩展点。不能把「支持登录邮箱」等同于「已经有读取和修改提供方日历的授权」。
九、当前原型已实现与尚未实现的边界
当前已有 Web 邮件 ICS 邀请卡片、详情、接受/待定/拒绝,以及周/月日历联动。接受和待定按 UID 写入浏览器内存日历,拒绝移除;勾选通知后生成本地模拟回应邮件。刷新页面会重置,不向外部组织者发信。
当前模型只有单一模拟参与身份。src/features/calendar/repository.ts 中的回应和日历条目以 UID 为键;这可以演示单身份流程,不能直接扩展成本文的多账号生产模型。
尚未实现:多邮箱授权与身份映射、真实收发、完整 iCalendar 解析、重复事件、版本排序、取消消息处理、外部日历连接器、服务端权限、事务、持久化及失败重试。
后续扩展原型时,先把回应键拆为事件/实例/参与身份,再通过 repository 暴露统一读模型。邮件与日历通过各功能域的 index.ts 公开入口协作,不由 UI 直接读取 fixtures 或操作另一域内部存储。新页面仍走生成器,交互状态由 TanStack Query 管理。
十、开发拆解与验收用例
推荐实施顺序
查看流程图源码与文本描述
flowchart TD decisions["1 产品规则与一期范围"] --> model["2 隔离、身份、事件模型与唯一约束"] model --> inbound["3 来源验证、收件去重与修订排序"] inbound --> reply["4 回应权限、幂等、发送结果与重试"] reply --> ui["5 邮件与日历共用契约,关联会议能力"] ui -.-> future["6 按批准范围扩展重复事件与外部同步"]
必须覆盖的用例
- A、B 同时收到同 UID 的有效邀请:一个日程、两份来源;事务重试不重复。
- A 接受、B 未回应或拒绝:A 状态不被覆盖,详情可解释聚合结果。
- 同标题同时间但不同 UID:不误合并;同 UID 组织者冲突:不覆盖可信事件。
- 别名邀请、转发副本、未验证身份:正确识别来源,未授权用户不能代他人回复。
- 重复收件、重复点击、网络超时后重试:不重复创建、不重复发送回应。
- 修改邮件乱序到达、取消先到邀请后到:不降级、不复活旧日程。
- 单次实例改期/拒绝/取消:不影响整个重复系列及其他实例。
- 回应时事件已经改期:要求确认最新内容,历史回应仍可追溯。
- 账号断开、解绑、重新授权:不能继续冒用账号发送,也不删除其他来源的日程。
- 全天、TZID、夏令时与跨时区邀请:保留原始语义;界面按 Asia/Singapore 展示,不以 UTC 午夜替代全天日期。
- 外部 API 和邮件都同步到同一事件:只有一份日程,不形成更新回环。
- 发送失败、只保存本地选择、事件取消后查看邮件:状态提示准确且历史仍可访问。
- 跨用户/跨租户猜测事件 ID:不能读取私人事件、邮件或账号信息。
本仓库继续使用既定 npm run check 和 root//telemail 静态构建验证;以上生产服务用例是未来开发验收要求,不代表本次新增测试框架或后端。
十一、上线前需逐项确认的产品决策
- 未回应邀请是否直接出现在日历?当前原型是接受/待定才进入日历。
- 多身份全部拒绝后是否隐藏?当前单身份原型会移除,但生产系统应保留历史。
- 是否采用第六节的聚合状态优先级?如何提示其他身份仍未回应?
- 是否允许只保存本地回应而不通知组织者?各提供方是否具备该能力?
- 日历入口多身份时是否每次选择,或允许显式记忆默认身份?不能自动代所有身份回复。
- 一期仅处理单次事件,还是支持重复系列与单次例外?不支持时如何提示?
- 一期选择邮件驱动还是提供方日历同步?授权和同步失败的用户提示是什么?
- 解绑最后一个来源后,保留、隐藏还是移除个人视图?历史与隐私删除如何处理?
- 邀请改期等重要修改是否要求重新回应?旧接受状态如何展示?
这些决策需逐项确认,不应从参考截图或对话建议中推断已批准。无论最终展示策略如何,底层始终保持:同一邀请可关联多封邮件、同一用户可有多个参与身份、每个身份独立回应、取消与删除历史分离。