# 会议出勤（Meeting Attendance）- 变更与进度记录

> **版本**: v1.0（议程能力首发上线）
> **状态**: Active（v1.0 议程能力开发中，v1.4 / v1.3 / v1.2 已上生产）
> **最后更新**: 2026-05-20

---

## 2026-05-20 · v1.0 doc-review 4-lens 修复（22 项技术 fix + 4 项产品决策）

### 用户决策（产品方向锁定）

1. **会议结束后允许改议程**：UI warning + audit `agendaModifiedAfterMeetingEnd: true` flag（撤销"会议结束后议程只读"）
2. **MIME 白名单收敛 8 项**：`application/pdf` / docx / pptx / xlsx / `image/jpeg` / `image/png` / `video/mp4` / `video/quicktime` (mov)；不含老 Office (msword / vnd.ms-*) / text/plain / text/csv
3. **重复分配任务静默跳过**：后端不抛 409，响应 `skippedExistingUserIds[]` + 前端 toast；删除 `UPLOAD_TASK_DUPLICATE` 错误码
4. **5 张新表改软删 + 标准字段对齐**：所有删除走 `deletedAt`；attachment 30 天后 cron 物理清理 + unlink；加 `createdById` / `organizationId` 与现有 meeting 模块一致

### 22 项技术 fix

- **F1/F7/F2 audit**：AuditAction 不是 prisma enum 而是 TS 常量（扩展 `MEETING_ATTENDANCE_AUDIT_ACTIONS` 常量对象，**无需 prisma migration**）；新增 4 个 AuditResource
- **F3 env 名统一**：`MEETING_ATTACHMENT_STORAGE_ROOT` / 默认值 `./var/meeting-attachments` / 路径 `<yyyy>/<mm>/<uuid>.<ext>`（不含原 filename）
- **F4 multer 配置**：`diskStorage` 必须（非 memoryStorage 否则 200MB OOM）；tmp dest 同盘避免 EXDEV；LIMIT_FILE_SIZE → 413 转换
- **F5 反向代理**：Caddy `request_body max_size 200MB` + 10min timeout / Nginx `client_max_body_size 210M` + 600s timeout
- **F6 MIME magic bytes**：`file-type` lib 读首 4KB 二次校验，与 Content-Type 不一致 → 415 `ATTACHMENT_MIME_MISMATCH`
- **F7 BigInt 序列化**：全局 `BigInt.prototype.toJSON` 或 interceptor；`size: string` 标注前端 `BigInt(resp.size)` 反序列化
- **F8 filename UTF-8 + RFC 5987**：filename 255 unicode；落盘永远 uuid（路径穿越 + ENAMETOOLONG 双防御）；Header `Content-Disposition: ...; filename*=UTF-8''...`
- **F9 reorder body 统一**：`{ ids: string[] }`（事实源 = 07-api）；09-test 5.A.5 / 5.B.4 同步
- **F10 categoryTag 修复**：09-test 5.B.1 `'REVIEW'` → `'OTHER'`；10-e2e 2.2.1 中文「审阅」→「其他」；05-ui 加 4 个 i18n key 双语
- **F11 字段长度/必填**：Section.title 1-200（与 schema 对齐）；Item.timeMinutes / presenterUserId 改可选；description 注 ≤ 2000
- **F12 API 路径数自报**：07-api 接口数算式 → "22 个对外路径"
- **F13 下载 endpoint 路径**：列两条具体路径，去掉 `:type` 形参措辞
- **F14 并发与事务**：reorder 单事务 CASE WHEN / DELETE item `SELECT FOR UPDATE` / auto-flip 检查 affectedRows / attachment + task 同事务
- **F15 tmp 文件 + orphan GC**：`req.on('close'|'aborted')` 兜底 unlink；cron 双向扫描（正向 unlink orphan 文件 / 反向 log warn DB 行无文件）
- **F16 category 改 enum**：`MeetingAttachmentCategory` `MINUTES` / `MATERIAL` / `PRESENTATION` / `OTHER`（替代 `VarChar(64)`）
- **F17 拖目录/多文件**：拖目录提示「不支持」；单次 ≤ 10 文件；并发 3；无 resume 断点续传
- **F18 assigned 路径鉴权**：request body 不带 taskId；service 内部 `ORDER BY assigned_at ASC LIMIT 1 FOR UPDATE` 自动选；无匹配且无 any 权限 → 403
- **F19 cursor 编码 + sort**：`cursor = base64({ assignedAt, id })`；query 加 `sort: 'dueAt_asc' | 'assignedAt_desc'`；响应展开 `assignedBy: UserBrief`
- **F20 status filter + CANCELLED**：05-ui 我的待办筛选条加 CANCELLED 选项；badge 保留 CANCELLED 灰色（30 天软删窗口内仍展示）
- **F21 任务卡字段命名映射**：从 API 响应映射（`agendaItem.meeting.title` 等）
- **F22 presenter onDelete**：用户禁用/删除时**不联动**议程项；查询 LEFT JOIN + 前端展示「(已离职)」标签

### 影响范围

13 份文档全部修齐（01 / 02 / 03 / 04 / 05 / 06 / 07 / 08 / 09 / 10 / 11-user-guide / 99-changelog / README 部分版本号）。

---

## 2026-05-20 · v1.0 议程管理 + 任务分配 + 资料上传（首发）

### 背景

线下/线上混合会议越来越多，运营反映：
1. 会议进行前没有结构化议程载体，议题、时长、主讲人靠口头或 IM 通知，参会人开会前临时找
2. 物料（PPT / 文档）散落在 IM 群和邮件，会后回顾找不到
3. 主讲人不主动上传 → 管理员追问，缺乏强约束的"任务分配"机制
4. 会议级与议题级资料混在一起没法区分

v1.0 在 meeting-attendance 模块引入议程能力，作为会议的"内容载体"——管理员配置段 / 主题项，给主讲人或资料提供人分配上传任务，参会人开会前后都能看到完整议程与资料。

### 改动范围

**5 个新 entity**（落 `platform_meeting_attendance` schema，均使用 CLAUDE.md §标准字段 `createdById` / `organizationId` / `deletedAt`）：

- `MeetingAgendaSection` — 议程段（一级分组，仅 title + order）
- `MeetingAgendaItem` — 主题项（挂在段下，含 title / description / code / timeMinutes / presenterUserId / categoryTag）
- `MeetingAgendaItemUploadTask` — 议程项级上传任务（管理员分配给具体用户，状态 PENDING/UPLOADED/CANCELLED）
- `MeetingAgendaItemAttachment` — 议程项级资料附件（强关联议程项）
- `MeetingAttachment` — 会议级资料附件（强关联 Meeting，议程外的会议纪要、签到表、其他材料）

**3 个新 enum**：

- `UploadTaskStatus` = `PENDING` / `UPLOADED` / `CANCELLED`
- `AgendaCategoryTag` = `FFAI_EAI_EV` / `EAI_ROBOTICS` / `MIXED` / `OTHER`
- `MeetingAttachmentCategory` = `MINUTES` / `MATERIAL` / `PRESENTATION` / `OTHER`

**6 个新权限点**（落 `platform_iam`）：

- `meeting:agenda:read` — 看议程（所有参会人）
- `meeting:agenda:update` — 改议程 CRUD（MeetingManager + creator）
- `meeting:upload-task:assign` — 分配任务（同 update 角色）
- `meeting:attachment:upload:any` — 任意上传（同 update 角色）
- `meeting:attachment:upload:assigned` — 仅被分任务时上传（Leader / Employee）
- `meeting:attachment:download` — 下载（所有参会人，含外部访客受限视图除外）

**22 个新 API 对外路径**（`/api/v1/meeting-attendance/` base 下，详见 `07-api.md`）：

- 议程查看 1 + 段 CRUD + reorder 4 + 项 CRUD + reorder 4 + 上传任务 4 + 我的待办 1 + 议程项资料 CRUD 3 + 会议级资料 CRUD 3 + 下载 2

**11 个新错误码**（落 `08-error-codes.md`）：

- `AGENDA_SECTION_NOT_FOUND` (404) / `AGENDA_ITEM_NOT_FOUND` (404)
- `AGENDA_FORBIDDEN_NOT_MANAGER_OR_CREATOR` (403)
- `UPLOAD_TASK_NOT_FOUND` (404) / `UPLOAD_TASK_NOT_OWNED` (403) / `UPLOAD_TASK_ALREADY_UPLOADED` (409)
- `ATTACHMENT_NOT_FOUND` (404) / `ATTACHMENT_TOO_LARGE` (413, 单文件 > 200MB) / `ATTACHMENT_MIME_NOT_ALLOWED` (415) / `ATTACHMENT_MIME_MISMATCH` (415, magic bytes 与 Content-Type 不一致) / `ATTACHMENT_DELETE_FORBIDDEN` (403)

### 关键产品决策（v1.0 锁定）

1. **议程项软删**：删除议程项时连带 attachments + upload-tasks 在同事务里软删（`UPDATE deletedAt = now()`）；service 层显式 cascade soft-delete，不依赖 FK `onDelete: Cascade`；attachment 软删 30 天后由 cron 物理清理 + unlink 底层文件
2. **5 张新表标准字段对齐 CLAUDE.md §标准字段**：`id` / `createdAt` / `updatedAt` / `createdById` / `organizationId` / `deletedAt`，DataScope 零配置；上传任务 / 附件类表的 `createdById` 冗余 = `assignedById` / `uploadedById` 以保 DataScope 一致
3. **系列议程独立**：每场会议自己一套议程，不在 series 层做议程模板（避免"模板 vs 实例"双向同步复杂度）
4. **会议结束后允许改议程**：保留可改，UI 加 warning（i18n key `meeting.agenda.editAfterEndWarning`）+ 审计留痕（含 `agendaModifiedAfterMeetingEnd: true` flag）；满足会后补主讲人 / 补资料场景
5. **同议程项多 assignee**：各上传各自文件、互相可见可下载，**仅本人能删自己的**；manager / creator 可强删任意
6. **外部访客（MeetingExternalAttendee）受限视图**：仅看 section title + item title，**不看** description / presenter / 资料（保护内部资料）
7. **审计复用现有 MeetingAttendanceAuditLog**：扩展 `backend/src/modules/meeting-attendance/constants/audit.ts` 中 `MEETING_ATTENDANCE_AUDIT_ACTIONS` 常量对象，新增 10 个键 `AGENDA_SECTION_CREATED` / `AGENDA_SECTION_UPDATED` / `AGENDA_SECTION_DELETED` / `AGENDA_ITEM_CREATED` / `AGENDA_ITEM_UPDATED` / `AGENDA_ITEM_DELETED` / `UPLOAD_TASK_ASSIGNED` / `UPLOAD_TASK_CANCELLED` / `ATTACHMENT_UPLOADED` / `ATTACHMENT_DELETED`；`audit_logs.action` 字段类型不变（`String VarChar(100)`），**不需要 prisma migration**（AuditAction 不是 prisma enum 而是 TS 常量）；同时新增 4 个 AuditResource `AGENDA_SECTION` / `AGENDA_ITEM` / `UPLOAD_TASK` / `ATTACHMENT`
8. **文件存储 MVP 本地 disk**：`storagePath` 字段存相对 disk 路径（`<yyyy>/<mm>/<uuid>.<ext>`，不含原始 filename），二期可换 S3 / OSS 不动 schema；MIME 校验两阶段（Content-Type + magic bytes）；上传 RFC 5987 + 软删 30 天 cron 物理清理
9. **不复用 platform_master.Attachment**：cuid / uuid 类型不兼容，且 platform_master 是跨模块共用资源，议程附件强耦合 meeting-attendance 业务语义（任务 / 议程项关联），独立建表更清晰
10. **议程项 `code` 字段可选自填**：用户填类似 `S1-ECAI051826-1` 的业务编号，系统不解析、不校验、不生成；仅作展示与跨会议人工对照
11. **重复分配任务静默跳过**：同 (agendaItemId, assigneeUserId) 已有非 `CANCELLED` 任务时后端跳过该人不重复创建，响应 `skippedExistingUserIds[]` 标注；前端 toast「已有 X 人已分配，跳过」
12. **MIME 白名单 8 项**：`application/pdf` / docx / pptx / xlsx / `image/jpeg` / `image/png` / `video/mp4` / `video/quicktime` (mov)；不含老 Office (msword / vnd.ms-*) / text/plain / text/csv

### 二期承诺（不在 v1.0 范围）

- 议程模板（运营在系列层维护，复用到下属会议）
- 议程版本历史（保留多版本 + diff）
- S3 / OSS 存储切换（schema 已留 `storagePath`，仅切实现）
- 资料预览（PDF / 图片在线预览，当前仅下载）
- 议程项级评论 / 讨论区
- 资料 OCR / 全文搜索

### 兼容性

- v1.0 是议程能力首发，无现存数据需要迁移
- 现有 Meeting / MeetingRequiredAttendee / MeetingAttendanceAuditLog 表只**新增**字段或 enum 值，不改字段语义
- 现有 API 一律不动

---

## 2026-05-15 · v1.4 出勤统计卡 UI 一致性 + ON→OFF 数据联动

### 背景

PR #378 上线后用户反馈：AI-Robotics 周日例会 `enforceCheckinMode=已关闭`，但 Attendance Statistics 仍显示 `0/17 线下 + 0/8 线上`（17+8=25 ≠ 应到 28，3 个 NOT_SET 被吞）。两处一致性问题：
1. 关闭签到校验时分母仍渲染，且因 NOT_SET 不进分母，导致 `线下分母 + 线上分母 ≠ 应到`
2. 可选参会人卡仅在 `optionalTotal > 0` 时渲染，没有可选时整张卡消失，跟"恒定布局"预期不一致

### 改动

**数据层**：
- `meetings.service.updateMeetingEnforceCheckinMode`：ON → OFF 时清空本场 `MeetingRequiredAttendee.checkinMode`（meeting-level 临时覆盖）
- `series.service.updateSeriesEnforceCheckinMode`：级联清空所有下属会议的同字段
- **保留** `MeetingSeriesAttendeePreference.defaultCheckinMode`（系列级默认覆盖），重开后自动恢复来源链路
- 响应新增 `clearedAttendeeOverrides`（仅 ON→OFF 时返回）
- 审计日志 `changes` 新增 `clearedAttendeeOverrides` 字段

**UI 层**（会议详情页 + reports 页 SingleMeetingView/SeriesView）：
- 线上/线下卡：`enforceCheckinMode = true` → `X/Y`；`false` → `X/-`（横杠占位）
- 可选卡：恒定渲染；`optionalTotal > 0` → `X/Y`；`optionalTotal === 0` → 单 `-`
- Attendance Rate 卡下小标同步：`optionalTotal === 0` → `Optional: -`

**文档**：`01-prd.md` v1.4 规则、`05-ui-interaction-spec.md` 三行更新、`07-api.md` 073 + 076 endpoint 行为补充

**测试**：L1 集成 `enforce-checkin-mode-toggle.api.test.ts`（ON→OFF 清空会议级覆盖、系列级覆盖保留、OFF→ON 自动从系列级派生 `allowedMode`）

### 兼容性

- API 响应字段**只增不减**（`clearedAttendeeOverrides` 可选）；前端不读也兼容
- 数据库无 schema 变更（仅 `UPDATE meeting_required_attendees SET checkin_mode = NULL`）
- 历史关闭状态的会议本次**不做数据回填**——UI 层已通过 `enforceCheckinMode` 决定是否显示分母，呈现一致；下次有人 toggle 才会清理 stale 数据

---

## 2026-05-15 · v1.3 可选参会人统计口径分离（PR #378）

### 背景

OPTIONAL_ATTENDEE 之前混算进 totalRequired / totalAttended / attendanceRate / statusDistribution / departmentStats。混算会稀释出勤率指标——可选参会人是否到场对会议结论无影响，管理员主要关心常规参会人到场情况。

### 改动

- 后端 `reports.service.ts`（series + single-meeting 两段循环）按 role 拆分：
  - `totalRequired` / `totalAttended` / `attendanceRate`：**仅统计 REGULAR_ATTENDEE**
  - `statusDistribution`：optional 不进
  - `departmentStats`：optional 不进
  - `roleStats`：仍按角色完整分桶（含 OPTIONAL_ATTENDEE 行）
- `overallStats` 新增字段：`optionalTotal` / `optionalAttended`，分子分母独立展示
- 前端：
  - 会议详情页 `Attendance Rate` 卡下方加文字小标 `Optional: X/Y`
  - 会议详情页 `Attendance Statistics` 栏在 Online 卡后加紫色 Optional 状态卡，可点击 toggle `statusFilter='OPTIONAL_ONLY'`（参会人列表筛到仅 OPTIONAL_ATTENDEE）
  - 其他 status 卡（ON_SITE/ONLINE/LATE 等）点击后的列表筛选**也排除 optional**，跟卡数字一致
  - 报表页（单会议视图 + 系列视图）应到卡下加文字小标 `Optional: X/Y`
- CSV 导出（单会议 / 系列）汇总段加 `可选 / Optional` 行
- i18n（zh/en）加 `meetingDetail.optionalLabel` + `reports.optionalLabel`
- L1 集成测试 `optional-attendee-stats.api.test.ts` 锁口径（单会议 / 部门统计 / 系列三场景）

### 兼容性

- API 字段口径变窄：`totalRequired` / `totalAttended` 不再包含 optional。确认过仅本系统内部消费，无外部接入方。
- 历史会议数字会变小，是预期的口径修正。

---

## 2026-05-12 · v1.3 系列参会人鉴权修复 + 排除清单（hotfix PR #344 + feature/series-attendee-exclusion）

### 背景

生产 FFAI 反馈：(1) 系列页改某个参会人的参会类型一律 403；(2) 系列页删除参会人 toast 成功，单次会议仍可见，且过一会儿那人又回到系列。

### Bug 1：4 处硬编码角色鉴权不一致（PR #344）

`updateSeriesAttendeeRole` 写成 `[ADMIN, MeetingManager, LEADER, MANAGER]`，跟同模块其他 3 处的 `[Administrator, MeetingManager, Leader]` 大小写都不同 → 任何 role=`Administrator` 的用户都被该接口 403。

修复：抽 `canMutateSeries(actor)` helper，优先 `meeting_attendance:manage` 权限点（含通配 `:*`），fallback 三元组角色保留 Leader 现有能力（Leader 在 seed 里没 `:manage`）。`requireMeetingUser` 透传 `permissions`，4 处统一调用。

### Bug 2：系列删除不持久化、cascade 漏 hasCustomAttendees 的会议（feature/series-attendee-exclusion）

根因：`MeetingSeries` 没有 attendee 关联字段，"系列参会人"是聚合视图（取下一场未来会议的 `requiredAttendees`）。系列层删除实现成 fan-out `DELETE MeetingRequiredAttendee`，没任何持久化标记。Outlook 同步把单场 attendees 列表当真理来源全量重写，删除决策在下一轮被回填。`!hasCustomAttendees` 过滤又跳过被单独自定义过的单次会议，造成"系列删了、单次仍在"。

修复：
- 新增 `MeetingSeriesAttendeeException(seriesId, userId, excludedBy, excludedAt, reason)` 表 + migration
- `deleteSeriesAttendee` 改成事务：upsert exception + cascade 删除所有未来 / 进行中会议（**不再用 `!hasCustomAttendees` 过滤**）
- `outlook-sync.service.ts syncAttendees`：入口查 exceptions，循环中命中即 continue（不 upsert、不进 reservedUserIds），事务末 `deleteMany(notIn reservedUserIds)` 自动清掉残留
- `getSeriesAttendees`：聚合视图同样过滤 exceptions，防止前端短暂时序差看到"幽灵参会人"
- `hasCustomAttendees` 字段保留，语义改为"仅控制聚合视图的默认场次选择"，不再控制 cascade 边界

详见 [06-data-model.md §系列层参会人排除清单（v1.3）](./06-data-model.md)。

---

## 2026-04-27 · v1.3 Outlook 同步参会人自动入组上线 + ai-tools 修复联合发布

### 上线

- 同事直接走 staging → production PR #156 合 prod（HEAD `894077c8`）；FF + AIxC 两侧 prod deploy 自动 backfill migration 跑（`12:43:54.792` 一致毫秒级时间戳给 4 个 RobotManager-* 角色补 24 grants 含 m365_mail）
- 联合发布内容：v1.3 自动入组（PR #150）+ ai-tools 角色自动种入修复（PR #145，原"运营 UI 新建 RobotManager-* 角色没自动 seed 工具"的隐性 bug，导致 chentao.jia / Fiona Xu / Sangaran Nagendran 在 OpenClaw 端只剩 LOCKED_SET 4 个工具看不到 m365_mail）+ ADP 同步增强（PR #147 + #153）+ IAM bug 修复（PR #149）+ 文档（PR #148）

### prod 端实测验证（FF prod 18:50 reconcile）

v1.3 hook 真触发，audit_logs 6 条记录覆盖 3 类决策路径：
- `fan.zhang@ff.com` → 真新增（graphResponseStatus=204）+ 后续 dedup race 触发 alreadyMember=true（400，业务无害）
- `corporate.operations@ff.com` → 同样模式
- `!conf_ec_conference1@ff.com` → `skipped_user_not_found`（资源邮箱 Graph 查不到，预期）

### #145 修复在 prod 实测

OpenClaw FF prod 26 个 msteams agent，**24 个 healthy（tools.allow=24 含 m365_mail）**，含 chentao.jia / Fiona Xu / Sangaran Nagendran 3 人 — m365_mail 正常出现在他们的 `tools.allow`，邮件 bot 可用。剩 2 个 `tools.allow=0` 的 stale agent 是 ws users 表里没记录的孤儿，sync 按设计跳过（非本次发版引入）。

### 一处行为细节（运维需知）

v1.3 dedup TTL = 10 分钟，Entra 整点 cron 周期 = 60 分钟。同一邮箱第一次成功 add（204）后，dedup 过期窗口（10-60 min 内）会被 outlook delta poll 反复触发再次 add，Graph 返回 400 "already exist" → audit 标 `alreadyMember=true status=200`，**Entra 审计页同时显示一条"失败"**。这是预期行为不是 bug，单邮箱最差产生 ~5 条假失败（直到下个整点 cron 拉进 users 表）。后续如果运维觉得吵可以单独 patch 把 dedup TTL 拉长到 60 分钟。

---

## 2026-04-27 · v1.3 Outlook 同步参会人自动入组（开发中）

### 背景

会议出勤模块从 Outlook 同步参会人时，常遇邮箱不在 workspace `users` 表的情况。workspace 用户由 Entra 安全组按小时 cron 同步，FF 侧源组为 `List_FFAI_Workspace`（由 `AZURE_ENTRA_SYNC_GROUP_ID` 配置）。当前行为：邮箱不在 users 表的参会人**只落 `meeting_external_attendee`**，无法成为 required attendee 也无法签到。运营需手工把这些真人邮箱加入源组，下个 Entra cron 拉进系统才能正常使用。

### 目标

Outlook 同步发现新邮箱时，按"是否真人员工"自动判断：符合条件的真人通过 Graph API 自动加入源组，下次 Entra 同步即拉进 users 表，免去运营手工操作。会议室 / 资源邮箱 / 服务账号 / Guest / 已离职等非真人账号一律拒绝入组。

### 关键决策

- **零 schema 改动 / 零新表 / 零新 API**：仅在 `outlook-sync.service.ts:syncAttendees` 的 "邮箱不在 users 表" 分支挂 hook
- **复用 `audit_logs`**：actor 用 `username='itadmin'` 系统管理员，`source='OUTLOOK_AUTO_SYNC'` 区分人工 vs 自动；新增 action `OUTLOOK_ATTENDEE_AUTO_ADD`
- **乐观放行**：模糊命中（`admin` / `test` / `temp` 等单词）一律放行，靠 audit 监控事后补规则；只严格拒明确黑名单命中（资源邮箱前缀 / 服务账号关键词 / `!` 前缀 / Guest / 无邮箱 / accountEnabled=false）
- **Graph 权限**：`GroupMember.ReadWrite.All`（FF + AIxC 两租户已 admin consent）
- **AIxC smoke only**：AIxC 无 meeting-attendance 业务流，代码两侧上线但 UAT 验收只跑 FF

### 文档变更

- `01-prd.md`: 新增"自动入组真人参会人（v1.3 新增）"章节
- `09-test-scenarios.md`: 新增 v1.3 测试用例（识别规则 4 类拒绝路径 + 1 类通过路径 + Graph 调用失败不阻塞 + audit 写入）
- 不改：`06-data-model.md` / `07-api.md` / `08-error-codes.md`（无 schema / API / 错误码变更）

### 探针结果（2026-04-27 实测，FF 租户）

`backend/scripts/probes/graph-naming-probe.ts` 跑出全量分布：4677 条用户 → Member 3675 / Guest 1002，accountEnabled=true 仅 1830（这一关就拒掉 60%）。识别规则黑名单依据 FF 实际命名规律敲定：
- 会议室 77 条命中：`Conf_*` / `Room_*` / `!Conf_*` / `!Room_*`
- 服务账号 46 条命中：`svc-*` / `svc.*` / `_svc` / displayName `Service Account` / `(ServiceAccount)`
- 共享凭据 6 条：displayName `Shared Credential` / `Shared Login` / `MFP Mailbox`
- 无邮箱 active Member 83 条：mail 字段空一律拒
- z_archive_ 历史归档：拒

---

## 2026-04-22 · v1.2 上线 + 迁移收尾联合发布

### 上线

- PR #121 合 staging（`62073c6c`）触发 FF UAT + AIxC UAT 部署，MCP 验收 7/7 通过
- PR #124 合 production（`441cb6158b`）—— v1.2 + 迁移收尾 + 旁路 robot-manager / dingtalk 批次一起推
- 生产 pg_dump 兜底：`srvadmin@43.130.6.44:/tmp/legacy_backup_20260422_024737.sql`（795 MB，留 7 天）
- 迁移收尾 cleanup 脚本 `backend/scripts/cleanup-legacy-ffai-air.ts`：prod 删除 FFAI 41 + AIR 44 meeting（dry-run + execute 双模式 + 行数守卫）；旧系列 end_date 更新（FFAI=2026-03-17 / AIR=2026-02-20）
- 生产 MCP smoke：v1.2 字段 / 旧系列 cutoff UI 反映 / 报表合并视图 banner 三点全过

### 上线策略偏差（⚠️ 关键）

运营选择**跳过 workCity 数据收集**，直接走 series-level preference 全员覆盖：

- 对两个 EC 系列的每位 required_attendee 通过 `backend/scripts/bulk-set-ec-preferences.ts` 批量 upsert preference（FFAI 56 条 / AIR 62 条，共 118 条）
- `users.work_city` 全员空；三层 fallback 只有 layer 2 生效，layer 3（城市派生）实际不触发
- `meetings.city` / `meeting_series.city` 填占位 `El Segundo`（UI guard 要求非空）

**⚠️ 后续维护注意**：
- **短期不要删 series-level preference 条目** —— 删了 fallback 到 city 派生会撞 `MEETING_ATTENDANCE_034`
- **新增参会人必须同步补 preference**，否则扫码 034

### 2026-04-22 发现并修复的 bug（hotfix）

**管理默认参会人弹窗分页漏 preference**（PR #127 / #128 / #129，prod HEAD `3c96bad3f9`）

- **根因**：`frontend/src/app/(modules)/meetingattendance/series/page.tsx:434` 调 `listSeriesAttendeePreferences(seriesId)` 不传 pageSize，后端 `series.service.ts:1274` default=50。FFAI 新系列 58 人 required_attendees 超过 50，切掉 8 条 preference → `prefMap` 缺失 → 下拉 value=null → 渲染"跟随工作地"
- **修复**：显式传 `{ pageSize: 200 }`（后端 Math.min cap）
- **影响**：UI-only 显示错位；签到后端直查 DB 行为不变
- **相关经验沉淀**：`~/.claude/projects/-home-ubuntu-Code/memory/feedback_list_api_pagesize_gotcha.md`

### 运营使用指南

- 文档：`docs/user-guides/checkin-mode-validation.md` + `.docx`（271 KB）
- 内容：运营视角说人话 + 入口红圈标注截图 + 聚焦图（非长 fullPage）+ 错误码只列用户会看到的文案
- 生成工具链：MCP browser overlay 标注截图 + pandoc md→docx
- 相关经验沉淀：`feedback_mcp_screenshot_annotation.md` / `feedback_pandoc_markdown_to_docx.md`

---

## 2026-04-21 · v1.2 签到方式校验（城市派生 + 系列级覆盖）

### 背景
FFAI EC / AI-ROBOTICS EC 两个系列会议需要严格区分线上/线下参会人，扫错码无法签到。运营沟通后方案几经演进：
- v1.1 草案：用户级”默认签到方式”Excel 打标 + 会议/系列白名单开关 + 会议级临时调整
- v1.2 定稿：**推翻”用户级标签”为”工作地 vs 会议地点派生”**，理由是标签方案缺乏前瞻性——未来会议可能在任何城市开，用户该到场与否取决于他工作地和会议地点是否一致。同时新增**系列级参会人覆盖**，支持”某用户在某系列因时间冲突长期线上”的场景。

### 相对 v1.0 的功能增量
- **用户工作地**：`User.workCity`（`platform_iam.users`）字符串字段，运营在组织架构模块维护（单改 + Excel 导入）
- **会议 / 系列地点 + 校验开关**：`Meeting.city` / `MeetingSeries.city` + `enforceCheckinMode`；系列级字段级联刷新下属 meeting
- **签到方式校验三层 fallback**：会议级 `MeetingRequiredAttendee.checkinMode` > 系列级 `MeetingSeriesAttendeePreference.defaultCheckinMode` > 城市派生
- **Excel 导入预览**：用户工作地 Excel 导入按”已存在/相似警告/全新”分类展示，管理员逐项确认后落库；相似度基于 Levenshtein 编辑距离（≤2）+ 长度差限制（≤2），排除缩写场景
- **城市自动补全**：无受控表；前端下拉从 `users.work_city + meetings.city + meeting_series.city` 聚合去重
- **签到 guard**：开关开启前后端校验 meeting/series.city 非空；否则拒绝打开

### 文档变更
- `01-prd.md`: 新增”签到方式校验（v1.2）”章节
- `05-ui-interaction-spec.md`: 新增元素（校验开关 / 地点/工作地输入 / Excel 导入预览弹窗 / 允许方式 tag 三色来源 / 系列默认参会人弹窗下拉）；交互清单新增 12 条 v1.2 项；人类阅读区新增”签到方式校验交互细则”
- `06-data-model.md`: 新增 `MeetingSeriesAttendeePreference` 实体；`User.workCity` / `Meeting.city` / `MeetingSeries.city`；`Meeting.enforceCheckinMode` / `MeetingSeries.enforceCheckinMode`；`MeetingRequiredAttendee.checkinMode`；新增关系约束与”签到方式校验”备注章节
- `07-api.md`: 新增签到方式校验域 8 个 endpoint（073/074/076/080/081/082/083 + 组织架构侧 import-work-city）；DTO 扩充；新增”签到校验新行为（v1.2）”与”报表字段新增（v1.2）”段落
- `08-error-codes.md`: 新增 `MEETING_ATTENDANCE_033/034/036`（撤销 v1.1 草案的 035）
- `09-test-scenarios.md`: 新增 23 条 v1.2 测试用例（覆盖三层 fallback、Excel 预览分类、相似度边界、级联刷新、Outlook 同步继承、报表字段、guard 拦截等）

### 数据模型
- 新 enum `AttendanceMode { ON_SITE, ONLINE }`（放 platform_meeting_attendance）
- `platform_iam.users.work_city VARCHAR`（nullable，trim 后原样，区分大小写比较）
- `meetings.city VARCHAR`（nullable）+ `meetings.enforce_checkin_mode BOOLEAN DEFAULT false`
- `meeting_series.city VARCHAR`（nullable）+ `meeting_series.enforce_checkin_mode BOOLEAN DEFAULT false`
- `meeting_required_attendees.checkin_mode “AttendanceMode”`（nullable）
- 新表 `meeting_series_attendee_preferences`（复合 PK `seriesId + userId`，`defaultCheckinMode` 必填）

### 不做数据迁移
- 所有 `enforce_checkin_mode` 默认 false，存量会议零影响
- 运营按需在目标系列手动配 city + 打开开关
- **撤销** v1.1 草案的”2 系列 + 65 场回填”迁移脚本

### 明确不做
- 违规扫码次数统计
- 签到页线上参会理由输入
- 物料 PDF 打印样式修改
- 受控城市表 / 城市管理页（字符串 + 自动补全已覆盖）
- 会议级 Excel 导入 / 用户级签到方式 Excel 导入（撤销 v1.1 草案的两个导入路径）
- 二维码页”仅限线下/线上”文字提示（撤销 v1.1 草案）
- 用户 AD `defaultRegion` 字段复用（保持独立 `workCity` 字段）

---

## 2026-03-03

### 当前进度

- ✅ 统一时间模型：入库 `UTC instant + IANA 业务时区`，展示默认按会议时区，用户时区作为补充信息。
- ✅ 明确 Graph 时区解析优先级并固化租户默认时区为 `America/Los_Angeles`（仅兜底）。
- ✅ 修复候选会议时间来源口径：候选接口返回可直接用于展示的 UTC 时间点，避免浏览器对无时区字符串误解析。
- ✅ Outlook 候选会议“系统纳管”判定修正：系列实例仅在 `startTime >= seriesBinding.syncFrom` 时标记 `managedBySeries=true`。
- ✅ 修复历史实例展示偏差：`syncFrom` 之前实例不再在候选列表显示为“系统纳管”。
- ✅ API 文档补充 `managedBySeries` 判定边界说明，避免与实现歧义。
- ✅ 候选会议历史过滤基准升级：实例会议采用 effectiveStart（有绑定取 `syncFrom`，无绑定取 `now`）过滤。
- ✅ 系列分组数量口径统一：页面徽标仅显示“下属会议数（children）”，不再包含系列主会议。

## 2026-03-07

### 当前进度

- ✅ Outlook 对账补齐策略增强：`Run Reconcile` 在 Delta 后按批次补齐候选窗口内缺少子快照的系列，减少系列主会议需先手工展开后才出现的问题。
- ✅ Outlook 候选列表过滤已结束系列：`includePast=false` 时不再展示已结束 `seriesMaster`。
- ✅ Outlook 候选子项对齐系列结束状态：已结束系列即使存在残留未来快照，也不再展示候选子会议。
- ✅ Outlook 纳管校验补充：已结束 `seriesMaster` 返回明确提示并拒绝纳管。

## 2026-03-09

### 当前进度

- ✅ Outlook 官方源历史追溯方案文档化：新增“官方源版本链 + 结构化差异摘要”设计，解决仅有覆盖式快照时无法回答“什么时候变了、改了什么”的问题。
- ✅ 明确同步历史展示口径：同步时间线需直接展示 Outlook 官方更新时间、获取时间、变化字段与人数摘要，避免管理员人工比对 raw payload。
- ✅ 明确三类参会人数口径必须同时留痕：`graph attendees count`、`internal matched attendees count`、`meeting required attendees count`。
- ✅ 明确阶段性范围：第一阶段仅对已纳管会议保存历史版本链与 diff；候选会议继续以当前态快照为主。

### 后续动作

- 基于文档补充 Prisma 模型与迁移设计，新增官方源版本表与差异表。
- 在 Outlook 同步主链路中落版本、判重、生成 diff，并增强绑定历史接口。
- 待办：实现 Outlook 官方源历史追溯能力，范围包括：
  - 新增 `outlook_event_source_versions`、`outlook_event_sync_diffs` 及对应 Prisma 迁移；
  - 在 `manage + delta/reconcile` 主链路中落官方源版本、标准化 payload hash 与结构化 diff；
  - 增强 `[063] /history` 返回差异摘要；
  - 实现 `[073] /source-versions`、`[074] /source-versions/:versionId`；
  - 为版本判重、人数口径摘要、版本详情查询补单元/集成测试；
  - 补充实现后的测试报告与最小复现步骤。

### 已执行验证

- 代码检查：`backend/src/modules/meeting-attendance/services/outlook-sync.service.ts` 中 `managedBySeries` 判定已绑定 `isEventInSyncWindow(..., syncFrom)`。
- 文档检查：`07-api.md` 已补充 `managedBySeries` 与 `syncFrom` 的关系说明。

---

## 2026-01-25

### 当前进度

- ✅ 确认旧系统数据仅出现 `ADMIN`/`EMPLOYEE` 角色（dev.db）。
- ✅ 角色迁移脚本补充 `ADMIN -> Administrator`、`EMPLOYEE -> Employee` 映射。
- ✅ 组织模块管理员/默认员工角色兼容新旧 code（Administrator/Employee 与 ADMIN/EMPLOYEE）。
- ✅ 审计日志分页文案对齐旧平台（`Page {page} of {totalPages}`）。
- ✅ 确认参会名单仅允许系统用户，访客签到必须在名单内。

### 当前状态

- 会议出勤角色对齐方案保持不变：Administrator/MeetingManager/Leader/Employee（旧角色映射）。
- 会议出勤用户与角色将迁移到平台组织架构（meeting_attendance.users 不再使用）。
- 角色种子无需新增角色，仅需确保权限分配与初始化执行。
- 审计日志分页文案已更新，待权限验证页面显示一致。

### 已知问题

- 本地账号无审计日志权限，无法在 `/meetingattendance/audit-logs` 验证分页文案。

### 阻断/待决策

- 组织模块文档已更新为 Administrator/Employee，角色 code 与系统实现对齐。

### 已执行验证

- 旧系统角色分布检查：`sqlite3 /home/chentao/Downloads/dev.db "select role, count(*) from users group by role;"`。
- 权限初始化脚本：`npm run init:permissions`（backend）。
- MCP 权限页验证：`/organization/roles/permissions` 搜索 `meeting_attendance`。
- MCP 访问：`/meetingattendance/audit-logs` 显示 `Access Denied`（需管理员权限）。
- 报告：`testing/reports/meeting-attendance-2026-01-25-e2e-report.md`。

---

## 2026-01-28

### 当前进度

- ✅ 去除迁移脚本对 `chonghan.lee` 的过滤逻辑，保证缺失用户检测完整。
- ✅ 全量重生成会议二维码并回写（共 226 条）。
- ✅ 会议出勤模块 E2E 全量冒烟完成并记录报告。
- ✅ 历史会议二维码缺失问题未复现（完成列表页、二维码页、数据库、API 核查）。
- ✅ Teams 会议列表页点击二维码按钮问题已修复并复测通过。
- ✅ 审计日志写入对齐旧系统并复测通过（会议/模板/参会人/出勤更新）。

### 当前状态

- 会议列表“二维码”按钮显示条件依赖 `qrCodeOnline/qrCodeOffline` 字段。
- 数据库与接口返回均显示 `qrCodeOnline/qrCodeOffline` 非空，列表页与二维码页验证正常，暂未复现缺失按钮问题。
- Teams 会议页二维码按钮已可正常打开二维码页。

### 已知问题

- 历史会议二维码缺失问题未复现，待用户侧复核是否仍出现。
- 审计日志筛选资源类型 + 日期区间问题已修复并复测通过（见 2026-01-28 E2E 报告新增用例）。

### 阻断/待决策

- 暂无新增决策。

### 已执行验证

- 二维码重生成（全量回写）：`scripts/backend/data/regenerate-meeting-attendance-qrcodes.ts`。
- 数据库核查：会议二维码字段 `qrCodeOnline/qrCodeOffline` 均非空。
- API 核查：`GET /meeting-attendance/meetings?status=all` 返回 `qrCodeOnline/qrCodeOffline` 非空。
- MCP 页面验证：`/meetingattendance/meetings`（Completed 列表）二维码按钮可见；`/meetingattendance/meetings/{id}/qr` 可见二维码。
- E2E 报告：`testing/reports/meeting-attendance-2026-01-28-e2e-report.md`。

---

## 2026-01-23

### 当前进度

- 角色对齐：会议出勤角色映射到系统角色（Administrator/MeetingManager/Leader/Employee），前后端兼容旧角色值。
- 角色种子：MeetingManager/Leader 的 code 已统一为驼峰写法。
- 权限种子：新增 meeting_attendance 权限（read/manage/checkin/checkin:manage/report/user:manage/audit）。
- 权限初始化：已执行 IAM seed 并在权限页确认 meeting_attendance 权限可见。
- 权限解析修复：修复 permission code 含多段 action 时的解析（如 checkin:manage/user:manage）。
- MCP 验证：已通过系统角色列表页确认 MeetingManager/Leader 的 code。
- MCP 验证：已确认 MeetingManager/Leader/Employee 的 meeting_attendance 权限分配。

### 当前状态

- 系统角色存在并可见。
- 会议出勤模块权限已出现在权限页面可分配列表。
- 会议出勤权限已按角色分配完成。

### 已知问题

- 暂无新增阻断问题。

### 阻断/待决策

- 暂无新增决策。

### 已执行验证

- 权限初始化脚本（本地）: `scripts/backend/init/init-permissions.ts`
- MCP 页面验证：`/organization/roles/system-roles`
- 截图：`testing/reports/meeting-attendance-2026-01-23-e2e/E2E-ROLE-01-system-roles-success.png`
- 报告：`testing/reports/meeting-attendance-2026-01-23-e2e-report.md`
- 权限页验证：`/organization/roles/permissions`（搜索 meeting_attendance）
- 截图：`testing/reports/meeting-attendance-2026-01-23-e2e/E2E-PERM-01-meeting-attendance-permissions.png`
- 角色权限验证：MeetingManager/Leader/Employee 的 meeting_attendance 权限
- 截图：`testing/reports/meeting-attendance-2026-01-23-e2e/E2E-PERM-04-meeting-manager-meeting-attendance-fixed.png`
- 截图：`testing/reports/meeting-attendance-2026-01-23-e2e/E2E-PERM-05-leader-meeting-attendance.png`
- 截图：`testing/reports/meeting-attendance-2026-01-23-e2e/E2E-PERM-06-employee-meeting-attendance.png`
- 报告：`testing/reports/meeting-attendance-2026-01-28-e2e-report.md`
- 截图目录：`testing/reports/meeting-attendance-2026-01-28-e2e/`

---

## 2026-02-28

### 当前进度

- ✅ 确认 Outlook 同步方案：Webhook + Delta + 定时对账兜底。
- ✅ 明确源邮箱配置改为后台管理（共享邮箱 + 个人邮箱），不在 `.env` 配置业务邮箱列表。
- ✅ 明确纳管策略为全手动勾选，未纳管会议不写入出勤业务实体。
- ✅ 明确主来源规则：系统级主来源 + 会议级覆盖同时支持。
- ✅ 明确历史接管边界：`2025-11-01` 及之后会议。
- ✅ 明确同步方向：仅 Outlook -> 本系统，Outlook 优先。
- ✅ 明确删除/取消映射：统一映射本地 `CANCELLED`，并记录内部取消来源。

### 当前状态

- `01-09` 文档均已新增 Outlook 同步相关正式定义。
- 已补充同步域 API、错误码、测试场景与数据模型草案。
- 已新增运行所需最小环境变量：`GRAPH_NOTIFICATION_CLIENT_STATE`。

### 阻断/待决策

- 暂无新增阻断。

### 后续动作

- 基于本次文档草案进入后端实现（Controller/Service/Repository 分层）。
- 完成 Prisma 模型与迁移设计，并补充集成测试。

### 实现进展（同日追加）

- ✅ 新增 Outlook 同步基础数据模型与 Prisma 迁移（mailboxes/subscriptions/bindings/cursors/settings）。
- ✅ 新增 Outlook 集成后端接口（源邮箱管理、候选会议、纳管、会议级主来源覆盖、同步设置）。
- ✅ 新增 Graph Webhook 回调端点（通知与生命周期）及 clientState 校验。
- ✅ 实现 Delta 增量同步主链路：通知触发 -> mailbox 游标推进 -> 会议/参会人更新。
- ✅ 实现删除/取消映射：Outlook 删除与取消统一映射本地 `CANCELLED`，并记录取消来源。
- ✅ 实现定时续订与定时对账任务框架（支持后台配置 cron 重载）。

---

## 2026-03-01（持续迭代）

### 当前进度

- ✅ 候选会议默认过滤增强：默认隐藏历史/已取消项，支持关键词与多条件筛选。
- ✅ 候选会议批量纳管：支持全选未纳管、批量提交与结果提示。
- ✅ 同步观测增强：源邮箱列表新增健康状态、最近同步、最近对账、最近错误。
- ✅ 邮箱级重试入口：支持行内“立即对账”并刷新观测时间。
- ✅ 已纳管会议维护区：新增已纳管列表搜索与分页能力（按邮箱维度）。
- ✅ 已纳管会议详情弹窗：支持查看绑定详情并在详情内保存会议级来源覆盖。
- ✅ 同步历史时间线：新增绑定维度同步事件日志与详情弹窗时间线展示。
- ✅ 同步失败可观测：增量处理异常写入 `SYNC_ERROR`，详情支持“仅失败事件”过滤。
- ✅ 失败日志结构化：`SYNC_ERROR` payload 新增 `stage/errorCode/statusCode/errorMessage`，前端时间线支持错误码与阶段展示。
- ✅ 时间线运维增强：支持按 eventType/stage 过滤并导出当前过滤结果 CSV。

### 已执行验证

- 前端构建通过：`cd frontend && npm run build`
- 后端构建通过：`cd backend && npm run build`
- MCP 回归：Outlook Sync 页面已验证筛选、批量纳管、立即对账、已纳管列表查询与分页。
- MCP 回归：已验证“Details”详情弹窗、绑定字段展示、详情内来源覆盖保存（无 console error/warning）。
- MCP 回归：已验证绑定历史接口与详情时间线渲染（事件倒序，字段可读）。

---

## 2026-03-02（持续迭代）

### 当前进度

- ✅ 候选会议补充拉取 `seriesMaster`：候选列表在 `calendarView` 基础上追加 `events(type=seriesMaster)` 并按 eventId 去重。
- ✅ 候选排序优化：`seriesMaster` 优先展示（避免被大量 occurrence 挤到后页）。
- ✅ 候选会议类型筛选：支持按 `seriesMaster/occurrence/exception/...` 快速过滤。
- ✅ 历史接口升级为服务端分页：`GET /integrations/outlook/bindings/:id/history` 新增 `page/pageSize`。
- ✅ 历史接口新增服务端筛选：支持 `startDate/endDate/eventType/stage/onlyError`。
- ✅ 历史导出改为后端导出：新增 `GET /integrations/outlook/bindings/:id/history/export.csv`，导出内容与筛选条件一致。
- ✅ 详情弹窗历史区改为服务端查询：支持时间范围筛选、分页浏览与服务端导出。
- ✅ 系列纳管策略升级：纳管 `occurrence/exception` 时自动归一到 `seriesMaster`，并在增量/对账中自动覆盖系列实例。
- ✅ 系列单次排除能力：新增 `POST /integrations/outlook/bindings/:id/exclusions` 与 `POST /integrations/outlook/exclusions/:id/remove`。
- ✅ 新增数据模型 `outlook_series_occurrence_exclusions`，用于稳定维护“系列纳管 + 单次排除”规则。

### 已执行验证

- 后端构建通过：`cd backend && npm run build`
- 前端构建通过：`cd frontend && npm run build`

## 2026-03-02（管理员绑定接管方案）

### 方案调整

- ✅ 候选事件默认改为“当前管理员邮箱相关事件”视角。
- ✅ 绑定约束改为 `graphEventId` 全局唯一（同一 Outlook 日历事件仅允许一个绑定）。
- ✅ 增加“强制接管绑定”语义：有页面权限的管理员可接管，接管后绑定人和同步来源邮箱同时切换。
- ✅ 增加 `syncFrom` 边界：绑定/接管后仅同步该时刻之后实例，历史不自动回补。
- ✅ 历史系列处理改为“手工执行”：系统提供检查清单与操作清单页面，不自动改 Outlook 数据。

### 页面整洁化（零技术债）

- ✅ Outlook 同步页面移除“新增源邮箱/启停邮箱/主来源设置”等运维项，避免业务管理员误操作。
- ✅ 页面移除“同步策略参数编辑（cron/批次/重试）”入口，防止策略层与业务层耦合。
- ✅ 页面仅保留纳管核心闭环：候选筛选、纳管/接管、已纳管查看、同步历史、上线清单。

### 管理员总览页（新增）

- ✅ 新增“已绑定会议”独立页面：`/meetingattendance/integrations/outlook/bindings-all`。
- ✅ 页面仅 `Administrator` 可见（导航与接口双重权限控制）。
- ✅ 新增后端接口：`GET /integrations/outlook/bindings/all`（跨邮箱分页查询已绑定会议）。

## 2026-03-02（候选会议可见性修复）

### 问题修复

- ✅ 修复“会议管理员已有 Outlook 事件但候选列表为空”问题：当未传 `mailboxId` 且当前管理员邮箱未登记为源邮箱时，后端自动创建并启用该个人邮箱，再拉取候选会议。
- ✅ 自动纳管邮箱流程增强：自动尝试创建订阅，订阅失败仅记录告警不阻塞候选会议展示。
- ✅ 修复导航高亮冲突：`/integrations/outlook` 与 `/integrations/outlook/bindings-all` 改为“最长路径优先”，避免双高亮。

### 验证

- ✅ `hongwei.zhang` 账号登录后可在 Outlook 同步页看到候选会议与邮箱标识。
- ✅ 会议管理员访问“已绑定会议”页面保持无权限（符合仅 Administrator 可见设计）。

## 2026-03-02（旧入口清理）

- ✅ 移除手动源邮箱管理对外接口：`GET/POST/PATCH /integrations/outlook/mailboxes*`。
- ✅ 移除前端 API 中对应旧方法，避免误调用旧方案。
- ✅ 统一文档契约：改为“候选接口自动纳管当前管理员邮箱”，不再保留手动新增邮箱流程描述。

## 2026-03-02（新方案最终收口）

- ✅ 下线旧多来源覆盖入口：移除 `PATCH /integrations/outlook/bindings/:id/source` 对外接口与前端 API 调用。
- ✅ 同步执行策略收口为“单绑定单来源邮箱”：运行时不再使用 `overridePrimaryMailboxId` 参与来源决策。
- ✅ 文档全面对齐：移除“会议级主来源覆盖”相关 API/测试场景/错误码描述，统一为“接管即切换来源”。
- ✅ 上线清单更新：移除手工源邮箱管理步骤，改为“管理员首次进入页面自动纳管邮箱”检查项。

## 2026-03-02（零技术债收尾）

- ✅ 数据模型移除 `outlook_sync_bindings.override_primary_mailbox_id`，彻底删除旧多来源覆盖遗留字段。
- ✅ 新增 Prisma 迁移：`20260302213000_meeting_attendance_remove_outlook_override_mailbox`。
- ✅ 测试报告命令更新：不再引用旧 `/integrations/outlook/mailboxes` 接口，统一改为候选接口与对账接口验证。

## 2026-03-03（同步可靠性与配置收口）

- ✅ 对账自愈增强：系列主会议（seriesMaster）在对账更新时会再次执行系列补齐，支持“本地手动删除后自动回补系列与实例”。
- ✅ 系列实例排序统一：系列相关查询改为按 `startTime asc`（次级 `instanceNumber asc`），前端列表展示稳定按时间线。
- ✅ 对账交互增强：Outlook 同步页面“触发对账”增加进行中态、禁重复点击和成功提示。
- ✅ 同步时间解析增强：Graph `dateTime` 不含偏移量时按 `timeZone` 转 UTC 入库，降低跨时区偏移风险。
- ✅ 参数零技术债清理：移除未使用的 `maxRetry/retryBackoffMs`（DTO/API/文档/DB）。
- ✅ 同步参数配置入口恢复：Outlook 同步页新增可编辑配置（`reconcileCron/deltaBatchSize/lookaheadDays/lookbackDays/renewBeforeMinutes`）。
- ✅ 同步参数默认值更新：
  - `reconcileCron`: `*/10 * * * *`
  - `deltaBatchSize`: `100`
  - `lookaheadDays`: `180`
  - `lookbackDays`: `7`
  - `renewBeforeMinutes`: `1440`
- ✅ 新增 Prisma 迁移：`20260303194000_meeting_attendance_outlook_sync_settings_cleanup`，会将本地已保存设置重置为上述新默认值。

## 2026-03-03（事件类型与时区一致性修复）

- ✅ Graph 事件类型标准化：将 `singleInstance + seriesMasterId` 统一识别为 `occurrence`，避免系列实例误归类为单次会议。
- ✅ 系列纳管链路统一使用标准化类型：
  - 纳管/接管绑定写入 `graphEventType` 改为 `single/seriesMaster/occurrence/exception`。
  - 对账与增量更新中的系列实例识别/补齐逻辑统一基于标准化类型。
- ✅ 时区同步标准化：
  - 会议时区优先使用 `start.timeZone`，兜底 `end.timeZone/originalStartTimeZone/originalEndTimeZone`。
  - 新增 Windows -> IANA 时区映射（含 `Pacific Standard Time -> America/Los_Angeles`），入库统一保存 IANA 时区标识。
  - Graph `dateTime` 无偏移时使用标准化时区转换 UTC，降低跨时区偏差。
- ✅ 已纳管列表排序优化：改为按会议 `startTime` 升序（次级 `updatedAt`），减少时间线“跳动”感。

## 2026-03-03（系列可见性与候选聚合优化）

- ✅ 修复“系列会议页面看不到同步系列”：
  - 前端系列页不再二次过滤 `isActive=true`。
  - 后端 `listSeries` 去除 `isActive=true` 过滤，返回全部系列。
- ✅ 同步链路新增系列激活兜底：
  - 当 Outlook 同步更新命中某系列（`meetingSeriesId` 存在）时，自动将 `meeting_series.is_active=true`，并同步系列时区/地点。
  - 避免历史系列被标记 inactive 后再次被“隐藏”。
- ✅ 候选会议默认按系列聚合展示：
  - 同一 `seriesMaster + occurrence/exception` 归为一行（显示总数）。
  - 支持展开查看系列下属实例，实例按开始时间升序展示。
  - 批量纳管的“全选未纳管”改为按聚合根（系列主会议/单次会议）选择，减少误操作与噪音。

## 2026-03-03（交互与契约收口）

- ✅ 移除已废弃上线检查清单接口：`GET /integrations/outlook/checklists/cutover`，避免旧方案残留接口继续暴露。
- ✅ 前端 API 同步清理：删除 `getOutlookCutoverChecklist` 调用壳，避免误用死链路。
- ✅ 文档契约同步更新：UI/API/测试场景中去除“上线检查清单”页面与接口描述，统一为迁移文档操作。
- ✅ 系列页仅展示 `isActive=true` 系列，修复“删除后仍可见”的回归问题。
- ✅ 同步页操作反馈统一为 toast，并为纳管/接管/取消纳管/排除操作增加进行中状态。

## 2026-03-03（候选/已纳管/已绑定分页与展开性能优化）

- ✅ 候选列表改为服务端分页驱动（默认 `pageSize=20`），避免首屏一次拉取全量候选导致慢加载。
- ✅ 候选接口新增筛选参数：`eventType/includeCancelled/includePast/onlyUnmanaged`，筛选逻辑下沉到后端。
- ✅ 候选并发请求稳定性修复：前端改为“请求序列”防覆盖，避免快速翻页出现空页/旧数据覆盖。
- ✅ 新增候选系列子项接口：`GET /integrations/outlook/candidates/:seriesMasterId/children`，展开时按需加载实例，分页不再影响系列子项完整性。
- ✅ 新增已纳管系列子项接口：`GET /integrations/outlook/bindings/series/:seriesMasterId/children`，用于同步页与管理员已绑定页的系列展开。
- ✅ 已纳管/已绑定列表支持服务端 `eventType/status` 筛选参数，分页与筛选口径一致。
- ✅ 后端候选查询新增短 TTL 缓存（按 mailbox+actor+时间窗口），降低翻页重复调用 Graph 的压力与 502 风险。
- ✅ `bindings-all`（Administrator）修复跨邮箱展开：未指定 `mailboxId` 时不再错误按当前管理员邮箱过滤子项。
- ✅ `bindings-all` 系列展开 UI 对齐 Outlook 同步页（同款 `▶/▼` 交互与数量展示样式）。

## 2026-03-04（Outlook 同步性能与数据一致性收口）

- ✅ 同邮箱同步并发解耦：新增邮箱级串行队列 + 去重合并，webhook/delta 与 reconcile 不再并发打架。
- ✅ 新增 3 分钟 delta 轮询兜底任务（低优先级），减少 webhook 丢失造成的候选滞后。
- ✅ 候选数据快照化：新增 `outlook_event_snapshots`，delta/候选回源时写入快照，候选列表优先读本地快照。
- ✅ 候选取消识别增强：除 `isCancelled` 外，补充 `Canceled:/Cancelled:/已取消/取消:` 标题前缀识别。
- ✅ 候选过滤收口：默认不含已取消；默认隐藏“无下属实例”的系列主会议（历史诊断场景除外）。
- ✅ 系列纳管体验修复：系列主会议纳管进行中时，下属实例“纳管”按钮禁用并显示进行中状态。
- ✅ 系列纳管异步化：`POST /integrations/outlook/bindings` 对系列返回 `bootstrapQueued=true`，实例补齐后台执行。
- ✅ 参会人双轨落库：新增 `meeting_external_attendees`，保留内部参会关系并补齐外部参会人 + organizer 快照。
- ✅ Prisma 迁移新增：
  - `20260304090000_meeting_attendance_outlook_snapshots_external_attendees`
  - 新表：`meeting_external_attendees`、`outlook_event_snapshots`
- ✅ 查询参数布尔解析修复：`includeCancelled/includePast/onlyUnmanaged/onlyError` 改为显式字符串布尔转换，修复 `"false"` 被错误识别为 `true`。
- ✅ 候选接口移除请求期 Graph 全量回退：快照为空时改为快速返回并异步入队同步，避免首屏 30~50s 阻塞。
- ✅ 候选查询下推数据库分页：`count + skip/take` 服务端分页，避免“全量查出后内存切页”。
- ✅ 候选系列数量前置返回：新增 `seriesChildCount`，无需展开即可显示系列下属实例数量。
- ✅ 候选子项筛选语义统一：`/candidates/:seriesMasterId/children` 新增 `includeCancelled/includePast` 入参并默认过滤已取消。
- ✅ 同步页重复请求收口：初始化后首轮自动重载跳过，减少同参数重复请求。
- ✅ 候选顶层展示收口：默认仅返回 `single + seriesMaster`，`occurrence/exception` 改为系列展开按需查看（仅在类型筛选显式选择时顶层返回），降低首屏噪音并减轻查询压力。
- ✅ 系列补齐状态落库：`outlook_sync_bindings` 新增 `bootstrapStatus/bootstrapError/bootstrapUpdatedAt`，前端可直接感知系列纳管后台进度与失败原因。
- ✅ 同步策略新增 organizer 开关：`outlook_sync_settings.includeOrganizerAsAttendee`（默认 false），支持按租户策略决定是否将组织者并入参会人名单。
- ✅ 候选/已纳管请求可中止：前端快速翻页和切筛选时取消旧请求，避免旧响应覆盖新状态导致“回第一页空数据/跳页”回归。
- ✅ 候选系列可见性修复：当本地快照同步游标超过 10 分钟未更新时，候选查询不再启用严格“未来实例”过滤，避免 `includePast=false` 下误隐藏仍有未来实例的 `seriesMaster`。
- ✅ 首次进入快照初始化：当邮箱尚无同步游标时，首次调用候选接口会异步初始化候选快照（含系列实例时间窗），并入队邮箱同步，不阻塞当前请求。
- ✅ 首次进入初始化可观测：候选接口新增 `snapshotInitializing` 字段，前端在初始化完成前显示“数据初始化中”提示。
- ✅ 候选历史过滤语义收口：`includePast=false` 时改为严格仅展示未来会议（`startTime >= now`），不再展示过去单次会议。
- ✅ 系列展开回源优化：`[071]` 子项查询改为优先读本地快照，仅在本地无子项时回源 Outlook；游标陈旧触发加入邮箱级节流，避免频繁重复入队。
- ✅ 同步链路冗余清理：移除未使用的候选缓存代码路径，减少无效 `clear` 与维护复杂度。
