# 会议出勤（Meeting Attendance）- 产品需求文档

> **版本**: v1.0（议程能力首发）
> **状态**: Active
> **创建日期**: 2026-01-22
> **最后更新**: 2026-05-20
> **产品经理**: 待定

---

## ✅ 机器读取区（必填）

### 通用字段

| 字段 | 内容 |
|------|------|
| 模块 | meeting-attendance |
| 文档类型 | PRD |
| 目标 | 保留现有线上会议出勤系统全部功能，并接入 FFOA 统一登录与权限体系 |
| 范围 | In Scope: 会议/系列/模板/签到/报表/用户管理/审计 / Out of Scope: 需求未明确的新功能 |
| 核心规则 | 1) 访客扫码免登录保留（仅限参会名单内用户） 2) 会议开始前15分钟开放签到 3) 会议开始后8分钟判定迟到 4) 会议完成后未签到人员可批量标记缺席 5) 同设备每场会议只允许一次签到（同一用户可更新） 6) 角色沿用线上语义，映射到系统角色（Administrator/MeetingManager/Leader/Employee） 7) 参会名单仅允许系统用户 8) 会议/系列可开启"签到方式校验"——开启后按"工作地 vs 会议地点"自动派生允许的签到方式，扫错码或未配工作地/会议地点均被拒签 |
| 验收标准 | 1) 原系统全部页面与核心流程可用 2) 新旧数据可迁移且统计一致 3) 统一登录替换完成且权限正确 4) QR 码重新生成后可正常签到 |
| 关联文档 | `05-ui-interaction-spec.md` / `07-api.md` |

### 功能清单（最小）

| 功能 | 优先级 | 说明 |
|------|--------|------|
| 会议管理 | P0 | 创建/编辑/取消会议，生成双二维码 |
| 系列会议 | P0 | 创建/维护系列会议，批量改期 |
| 签到（系统用户） | P0 | 登录后扫码/手动签到 |
| 签到（访客免登录） | P0 | 访客扫码填写信息签到（需在参会名单） |
| 参会名单 | P0 | 增删/导入参会人员、默认参会人（仅系统用户） |
| 出勤统计报表 | P1 | 单场/系列报表与状态分布 |
| 用户管理 | P1 | 用户由组织架构同步管理，本模块仅查看并跳转组织架构管理 |
| 审计日志 | P1 | 关键操作审计日志查询 |
| Teams 集成页面 | P2 | 保留现有 Teams 相关入口与页面 |
| Outlook 自动同步 | P0 | 从 Outlook 自动同步会议，管理员手动纳管后生成/更新签到项 |
| 签到方式校验（线上/线下分流） | P0 | 会议/系列维度开关；用户工作地 + 会议地点派生；支持会议级临时调整与系列级参会人默认覆盖；扫错码/未配地点均拒签；报表增加允许方式与调整次数列 |
| 议程管理 + 任务分配 + 资料上传 | P0 | 会议级议程段 / 主题项 CRUD；管理员可分配上传任务给指定参会人；议程项级 + 会议级资料独立挂载；外部访客仅看议程标题不看资料 |

---

## 🧭 人类阅读区（可选）

### 背景

现有会议出勤系统已上线，具备完整业务流程与历史数据。目标是在不改变既有业务行为的前提下，迁移到 FFOA 体系内，并完成统一登录与权限控制。

### 目标用户

| 角色 | 描述 | 使用场景 |
|------|------|---------|
| 会议管理员 | 负责会议与参会人管理 | 创建会议、导入参会名单、查看报表 |
| 会议组织者 | 负责组织具体会议 | 生成二维码、查看签到情况 |
| 领导 | 关注出勤统计 | 查看报表与排名 |
| 普通员工 | 参会并签到 | 扫码签到、查看个人会议 |
| 访客 | 免登录签到 | 扫码填写信息签到 |

### 关键约束

- 访客免登录签到必须保留，但必须在参会名单中。
- 页面视觉允许保留原系统（非 Lark 风格）以降低改动量。
- QR 码需重新生成并替换旧系统地址（旧系统下线后不可用）。
- 数据迁移以 SQLite 文件为准，最终切换时再同步一次最新 db。
- 角色对齐系统角色：Administrator/MeetingManager/Leader/Employee（旧角色 ADMIN/MANAGER/LEADER/EMPLOYEE 映射）。
- 参会名单仅允许系统用户，禁止自由填写姓名/邮箱。

### Outlook 同步新增需求

- 同步方向仅支持单向：Outlook -> 本系统。
- 同步策略采用混合模式：Webhook 订阅 + Delta 增量 + 定时对账兜底。
- 源邮箱由系统自动纳管：按当前管理员邮箱无感创建/启用个人邮箱配置，不提供手动新增入口。
- 纳管策略为全手动：管理员从候选会议中勾选需要纳管的会议。
- 系列会议纳管策略：纳管 `seriesMaster` 或其任一 `occurrence/exception` 时，统一按 `seriesMaster` 级纳管并自动覆盖系列实例。
- 系列排除策略：支持在系列纳管后排除单次 `occurrence/exception`，用于临时不纳入签到的特例实例。
- 主来源策略：每个绑定仅保留单一有效来源邮箱（接管时同步切换），避免多来源仲裁抖动。
- 历史接管边界：仅接管 `2025-11-01` 及之后的会议。
- 参会人映射：Outlook `required` -> 普通参会人，`optional` -> 可选参会人。
- 参会人口径分离（v1.3）：可选参会人 **不计入应到/实到/出勤率/状态分布/部门统计**，单独以分子/分母小标展示（如 `可选: 1/3`）。理由：可选参会人是否到场对会议结论无影响，混算会稀释出勤率指标，且管理员主要关心常规参会人到场情况。`roleStats` 仍按角色完整分桶（含可选），便于在角色筛选器里单独看可选人群的明细。
- 出勤统计卡 UI 一致性（v1.4）：
  - **线上/线下卡分母**：仅当 `meeting.enforceCheckinMode = true` 时显示 `X/Y` 形式（Y = `allowedMode === ON_SITE/ONLINE` 的常规参会人数）；关闭时显示 `X/-`（横杠占位表示"此场未启用签到方式校验，分母不适用"）。
  - **可选参会人卡**：恒定渲染（不再隐藏）；`optionalTotal > 0` 时显示 `optionalAttended/optionalTotal`，`optionalTotal === 0` 时显示单 `-`（表示"本会议无可选参会人"）。
  - 同步规则：`enforceCheckinMode` 从 ON → OFF 时，**清空本场所有参会人的 `MeetingRequiredAttendee.checkinMode`**（meeting-level 临时覆盖）；**保留** `MeetingSeriesAttendeePreference.defaultCheckinMode`（系列级默认覆盖），便于重新开启后从系列级 / 城市派生快速恢复。系列开关 OFF 时级联清空所有下属会议的 meeting-level 覆盖。
  - 理由：原来分母永远显示，关闭签到校验后 `X/0` 或 `25 ≠ 应到 28` 的呈现误导用户；横杠占位 + 数据层联动确保 UI 与开关状态语义一致。
- Outlook 取消与删除统一映射为本地 `CANCELLED`，并记录同步原因（内部元数据，不改变对外状态枚举）。
- 管理页候选来源：管理员进入页面后，仅展示“与当前管理员邮箱相关”的 Outlook 日历事件。
- 自动邮箱纳管：当当前管理员邮箱尚未在系统源邮箱表中登记时，系统在首次加载候选会议时自动创建并启用该个人邮箱配置（无感知）。
- 管理员总览：新增“已绑定会议总览页”，仅 `Administrator` 可见，用于跨邮箱查看所有已绑定会议。
- 唯一绑定约束：同一 Outlook `graphEventId` 在系统内仅允许一个有效绑定（全局唯一）。
- 绑定接管机制：具备页面权限（Administrator/MeetingManager）的用户可强制接管，接管后当前绑定人和同步来源邮箱同时切换，并写入审计事件。
- 绑定同步边界：绑定创建或接管后仅同步 `syncFrom`（绑定时刻）之后的未进行会议实例，历史实例不自动回补。
- 历史系列处理方式：不自动改 Outlook 数据；上线后由管理员按迁移操作文档手工将“本系统历史系列”结束时间截断到当天之前，再由纳管 Outlook 会议生成后续实例。
- 对账补齐策略：手动/定时 `reconcile` 在完成 Delta 同步后，需按批次补齐当前邮箱候选窗口内“缺少子实例快照”的 `seriesMaster`，减少候选页因本地无子会议快照而暂时隐藏系列主会议的情况；单次对账不要求覆盖全部系列。
- 已结束系列候选规则：未勾选“包含历史会议”时，候选页不得展示已结束的 Outlook `seriesMaster`；若系列已纳管，则仍可在“已纳管会议/系列页”查看，但不作为候选再次纳管。
- 已结束系列纳管规则：管理员尝试纳管已结束 `seriesMaster` 时，系统必须返回明确提示，说明该系列已结束且不会生成后续实例。
- 候选子项展示规则：候选页展开系列子项时，仅展示该 `seriesMaster` 当前仍有效的未来实例；若系列已结束，即使本地快照残留未来实例，也不得继续展示。
- Outlook 源版本留痕：对已纳管会议，每次从 Outlook 官方源获取到事件数据时，系统必须保留该次官方源版本记录，至少包含 `graphEventId`、`graphEventType`、官方 `lastModifiedDateTime`、获取时间、来源入口与原始 payload。
- Outlook 差异审计：当同一 Outlook 事件内容发生变化时，系统必须生成结构化差异摘要，明确本次同步相对上一个官方源版本改了什么；至少覆盖 `title`、`start/end`、`location`、`isCancelled`、`organizer`、`attendees`。
- 参会人口径留痕：Outlook 同步差异必须同时记录三类数量口径，避免排查歧义：`graph attendees count`、`internal matched attendees count`、`meeting required attendees count`。
- 官方源追溯边界：当前态快照继续用于候选/同步缓存；历史排查依赖独立的官方源版本链与差异记录，不允许仅靠覆盖式快照承载历史追溯。
- 展示目标：管理员查看单个绑定的同步历史时，应能直接看到该次同步对应的 Outlook 官方更新时间、获取时间、是否有内容变化、变化字段及关键差异摘要；无需人工比对原始 JSON 才能定位人数变化。
- 本地维护优先：单次会议一旦被人工修改标题、时间、地点、状态或参会名单，即视为“转为本地维护”，后续 Outlook 同步不得再覆盖该会议。
- 本地维护提示：会议详情页必须明确提示“该会议已进行本地修改，已停止从 Outlook 自动同步”，并展示锁定时间、操作人与原因。
- 删除参会人约束：仅允许删除尚未产生真实出勤记录的参会人；若该用户已有签到时间、非 `NOT_CHECKED_IN` 状态或人工出勤调整痕迹，必须阻止删除并给出明确提示。

### 签到方式校验（v1.2 新增）

**背景**：FFAI EC / AI-ROBOTICS EC 两个系列需要严格区分线上/线下参会人，扫错码无法签到。系统按"用户工作地 vs 会议地点"自动派生允许方式，支持会议级临时调整与系列级默认覆盖。未来类似会议（无论在任何城市召开）可直接复用。

**模型**：
- **用户工作地**：`User.workCity`（`String?`，存放在 `platform_iam.users`）。字符串存储（trim 后原样写入），不建受控表。组织架构模块提供编辑入口 + Excel 批量导入（见下"Excel 导入预览"）。
- **会议/系列地点**：`Meeting.city` + `MeetingSeries.city`（均 `String?`）。创建/编辑时输入，前端自动补全从系统已有城市（`SELECT DISTINCT` 聚合）。
- **校验开关**：`Meeting.enforceCheckinMode` + `MeetingSeries.enforceCheckinMode`（均 `Boolean`，Schema `@default(false)`）。**应用层 guard**：切换开关 ON 时必须 `city != null`，否则后端拒绝并提示"请先配置会议地点"。
- **系列级参会人默认覆盖**：新表 `meeting_series_attendee_preferences`（复合 PK `seriesId + userId`，字段 `defaultCheckinMode AttendanceMode?`）。在系列"管理默认参会人"弹窗中维护。
- **会议级临时调整**：`MeetingRequiredAttendee.checkinMode`（`AttendanceMode?`）。在会议详情页参会人列表行内编辑。
- 新 enum `AttendanceMode { ON_SITE, ONLINE }`，与 `AttendanceStatus`（出勤结果 7 分类）独立，专用于"允许签到方式"语义。

**签到校验逻辑**（`POST /meetings/:id/checkin` + `POST /meetings/:id/guest-checkin`）：
1. 若 `meeting.enforceCheckinMode = false`：放行，行为与现在一致。
2. 若 `meeting.enforceCheckinMode = true`，按三层 fallback 求 `allowed`：
   - **第 1 层**（最高优先）：`MeetingRequiredAttendee.checkinMode`（本场临时调整）
   - **第 2 层**：`MeetingSeriesAttendeePreference.defaultCheckinMode`（系列级参会人默认）
   - **第 3 层**：按城市派生
     - `meeting.city == null` → 拒签 `MEETING_ATTENDANCE_036`"会议未配置地点"（正常不会发生，因为开关 guard）
     - `user.workCity == null` → 拒签 `MEETING_ATTENDANCE_034`"请联系管理员配置您的工作地"
     - `trim(user.workCity) == trim(meeting.city)`（区分大小写精确比较）→ `ON_SITE`
     - 两者都非空且不等 → `ONLINE`
3. `allowed != qrType`（qrType 来自二维码 URL 的 `type=online|on_site` 参数）→ 拒签 `MEETING_ATTENDANCE_033`"请使用线下/线上专属二维码"
4. 通过后进入现有迟到/设备/重复签到判断；`attendance.status` 按 `qrType` 派生，忽略 Body 传入的 `attendanceStatus`（防绕过）。

**继承规则**：
- 管理员修改 `MeetingSeries.enforceCheckinMode` / `city` 时，**级联刷新**该系列下所有 `Meeting` 的对应字段。
- Outlook 同步新建 meeting（含 recurrence occurrence/exception）时，若有 `seriesId` 则从 series 读 `enforceCheckinMode` + `city` 继承；否则沿用 Schema 默认（`false` + `null`）。

**不影响 Outlook 同步**：开关、地点、会议级调整、系列级覆盖、用户工作地五类修改均走独立 endpoint，**不触发 `lockOutlookBindingForMeeting`**，绑定的 `syncMode` 保持 `AUTO`。

**用户工作地 Excel 导入预览**（唯一 Excel 导入入口，位于组织架构/用户管理）：
- 上传格式：两列 `Email / 工作地`（城市名字符串）
- 预览流程：
  1. 系统读全部行，按城市聚合
  2. 每个城市根据"系统已有城市集合"（`SELECT DISTINCT workCity FROM users UNION SELECT DISTINCT city FROM meetings UNION SELECT DISTINCT city FROM meeting_series`）分类：
     - 🟢 **已存在**（精确匹配）：直接采纳
     - 🟡 **相似警告**（编辑距离算法）：长度差 ≤ 2 且 Levenshtein 距离 ≤ 2 → 提示"是否与已有 XXX 为同一城市？"，管理员可选"用已有 / 保留本次填写"
     - 🔵 **全新**：提示"确认新增 XXX？"
  3. 管理员在预览页逐项确认后点"确定导入"，系统按确认结果写入 `User.workCity`
- **缩写不提示**（如 `LA` vs `Los Angeles`）：长度差太大，自动跳过相似度检测
- 预览仅在前端/后端临时保存，不入库（批准后再写库）

**城市数据管理**：
- 字符串存储（trim 后原样，区分大小写）
- 前端在用户工作地 / 会议地点 / 系列地点三个输入框都提供自动补全下拉（数据源同上）
- 不建立受控城市表，不提供城市管理页

**上线范围**：
- 运营在两个目标系列上手动配置 `city` + 打开 `enforceCheckinMode` 开关（保存时级联刷新下属 meeting）
- **不做数据迁移脚本**：`Meeting.enforceCheckinMode` 默认 `false`，存量会议完全不受影响；运营按需开启

**明确不做**：
- 违规扫码次数统计
- 签到页线上参会理由输入
- 物料 PDF 打印样式修改
- 受控城市表 / 城市管理页（字符串 + 自动补全已覆盖）
- 会议级 Excel 导入（页面手动改更清晰）
- 用户级签到方式 Excel 导入（改为用户工作地 Excel 导入，签到方式由城市派生）

### 自动入组真人参会人（v1.3 新增）

**背景**：Outlook 同步参会人时常遇邮箱不在 workspace `users` 表 —— 因为 workspace 用户由 Entra 安全组（FF 侧 `List_FFAI_Workspace`，由 `AZURE_ENTRA_SYNC_GROUP_ID` 配置）按小时 cron 同步，未加入源组的真人无法进系统。当前行为是这类邮箱只落 `meeting_external_attendee`，不会成为 required attendee。运营需手动维护源组成员，效率低且易遗漏。v1.3 让系统自动判定真人后调 Graph API 加入源组，下个 Entra cron 自动拉进 users 表。

**真人识别规则**（任一 fail 即不入组，不影响 sync 主流程）：

1. **硬过滤**（4 条全部通过才进入下一步）：
   - `accountEnabled === true`（拒掉 disabled / leaver）
   - `userType === 'Member'`（拒掉 B2B Guest 访客）
   - `mail !== null && mail.trim().length > 0`（拒掉无邮箱的服务账号）
   - `mail.toLowerCase() === requestedEmail.toLowerCase()`（防 alias / proxyAddress 误匹配）

2. **mailNickname 前缀黑名单**（case-insensitive，必须带分隔符 `_` `.` `-` 才算前缀，避免误伤）：
   - `conf_` `conf.` `room_` `room.` `equip_` `res_` `resource_`
   - `shared_` `team_` `list_` `distro_` `group_`
   - `svc-` `svc_` `svc.`

3. **mailNickname 含**：
   - `.svc` `_svc`
   - 第一字符 `!`（FF 租户用 `!` 前缀把资源邮箱排序到通讯录顶部）

4. **displayName 关键词**（case-insensitive，要求**完整短语**出现，不做单词 substring 防误伤）：
   - `service account` `serviceaccount`（覆盖 `(ServiceAccount)` 后缀，toLowerCase 后裸版即匹配）
   - `shared credential` `shared login`
   - `conference room` `conf room` `meeting room`
   - `mfp mailbox`
   - `z_archive_`
   - `会议室`

**乐观放行原则**：模糊命中（如 `admin` / `test` / `temp` 单词）**不进黑名单**，因为存在真人合法用例（`Andy.admin` / `Robert Holshouser (Contractor)` 等）。误入组通过 audit 日志监控，事后补规则。

**Graph 调用**：
- 权限：Microsoft Graph `Application permissions` → `GroupMember.ReadWrite.All`（FF + AIxC 两租户均已 admin consent）
- API：`POST /groups/{AZURE_ENTRA_SYNC_GROUP_ID}/members/$ref`，body `{ "@odata.id": "https://graph.microsoft.com/v1.0/directoryObjects/{userId}" }`
- 失败处理：404 / 403 / 502 等所有错误**不抛异常**，logger 记录 warn 后继续 sync 主流程；下次 Outlook 同步触发时会重试

**审计**：
- 复用现有 `audit_logs` 表，**不新加表 / 不改 schema**
- Actor 通过 `username='itadmin'` 查到的系统管理员；`source='OUTLOOK_AUTO_SYNC'` 区分人工操作 vs 自动入组
- 新增 action `OUTLOOK_ATTENDEE_AUTO_ADD`
- `changes` 字段塞 JSON：
  - `decision`：`added` / `skipped_disabled` / `skipped_guest` / `skipped_no_mail` / `skipped_email_mismatch` / `skipped_resource_naming` / `skipped_user_not_found` / `failed`
  - `matched_rule`（仅 `skipped_resource_naming` 时）：`mailNickname_prefix:conf_` 等具体命中规则
  - `email` / `graph_user_id`（找到时）/ `graph_response_status`（调用 Graph 时）/ `error_message`（失败时）

**审计可视入口**：管理员可在现有审计日志页面通过 action filter `OUTLOOK_ATTENDEE_AUTO_ADD` 查看历史记录。新增前端 filter 选项不在 v1.3 范围。

**触发频率**：接 outlook-sync 的 `syncAttendees()` 调用，被 4 个上游驱动 —— delta poll（每 3 分钟硬编码）/ reconcile（按 `OutlookSyncSetting.reconcileCron`，默认每 2 小时，FF 当前配 `*/10`）/ Graph webhook 通知 / 手动 reconcile。**不另起独立 cron**。

**部署范围**：
- 代码两侧 prod 都上线
- FF UAT 走完整 MCP 验收（FF 是会议出勤主用方）
- AIxC 仅 smoke 验证（无 meeting-attendance 业务流，自动入组路径不会真触发）

**配置**：复用现有环境变量 `AZURE_TENANT_ID` / `AZURE_CLIENT_ID` / `AZURE_CLIENT_SECRET` / `AZURE_ENTRA_SYNC_GROUP_ID`，不新增 .env 项。

**明确不做**：
- 不调 `mailboxSettings.userPurpose` 检查 shared/room（需要额外 `MailboxSettings.Read` 权限，命名规则覆盖率够）
- 不做"自动入组失败的重试队列"（自动入组失败不阻塞 sync，下次 sync 自然重试）
- 不补 series-level preference（v1.2 layer-2 偏好仍由运营手工补 —— 配合 v1.2 现有上线策略偏差）
- 不做前端"自动入组审计页"（沿用现有审计日志页 filter）

### 功能 5：议程管理 + 任务分配 + 资料上传（v1.0 新增）

**背景**：线下/线上混合会议越来越多，运营反映会议进行前缺少结构化议程载体，议题、时长、主讲人靠口头或 IM 通知；物料（PPT / 文档）散落在 IM 群和邮件，会后回顾找不到；主讲人不主动上传 → 管理员追问，缺乏强约束的"任务分配"机制。v1.0 在会议层引入"议程段 + 主题项 + 上传任务 + 资料附件"四层模型，覆盖会前准备、会中陈述、会后回顾全链路。

**目标**：
- 给每场会议提供结构化议程载体（段 → 主题项的两层分组）
- 管理员可对议程项分配上传任务给指定用户，被分配人能看到「我的待办」
- 议程项级资料 + 会议级资料分开承载（会议纪要、签到表挂会议级；PPT、文档挂议程项级）
- 所有参会人能看到议程与资料；外部访客仅看议程标题（保护内部资料）

**In Scope**：
- 议程段 CRUD + 排序（拖拽，order 整数）
- 议程项 CRUD + 排序；字段：title / description / code / timeMinutes / presenterUserId / categoryTag
- 议程项级上传任务 CRUD（assignee + 可选 dueAt + 状态机 PENDING/UPLOADED/CANCELLED）
- 议程项级资料上传 / 下载 / 删除（multipart，单文件 ≤ 200MB）
- 会议级资料上传 / 下载 / 删除（同上，含 category 字段标识 MINUTES / MATERIAL 等业务分类）
- 「我的待办」列表（按当前用户 + status 过滤）
- 议程视图按角色派生：内部参会人完整视图、外部访客受限视图（仅 section + item title）
- 审计日志复用 `MeetingAttendanceAuditLog`，扩展 `MEETING_ATTENDANCE_AUDIT_ACTIONS` TS 常量（非 prisma enum）
- 5 张新表标准字段对齐 CLAUDE.md §标准字段（`createdById` / `organizationId` / `deletedAt` 等）

**Out of Scope**（二期承诺）：
- 议程模板（系列层复用）
- 议程版本历史 / diff（同上传文件覆盖式；不保留历史版本。已软删 attachment 保留 30 天后 cron 物理清理）
- S3 / OSS 存储（schema 已留 `storagePath`，仅切实现）
- 资料预览（PDF / 图片在线预览）
- 议程项级评论 / 讨论区
- 资料 OCR / 全文搜索

**接受标准**：
- 内部参会人在会议详情页可以看到完整议程树（段 → 项 → 资料）
- MeetingManager / 会议 creator 可以创建段 / 项、分配上传任务、对任意上传资料执行删除
- 被分配人在「我的待办」可以看到 PENDING 任务，上传完成后任务自动转 UPLOADED
- Leader / Employee 仅在被分配任务时可上传资料（仅限指向自己的 task）
- 外部访客视图不暴露 description / presenter / 任何资料附件元数据
- 议程项删除时，关联的 attachments + upload-tasks 同事务软删（`deletedAt` 标记）
- 同议程项多 assignee 各自上传的资料互相可见；仅本人或 manager / creator 能删
- 单文件 > 200MB 拒收（错误码 `ATTACHMENT_TOO_LARGE`）
- 非白名单 MIME 拒收（错误码 `ATTACHMENT_MIME_NOT_ALLOWED`）
- 关键操作（议程项 CRUD / 任务分配 / 资料上传 / 资料删除）写审计
- 5 张新表均使用 CLAUDE.md §标准字段（`id` / `createdAt` / `updatedAt` / `createdById` / `organizationId` / `deletedAt`），DataScope 零配置

**业务规则**：

1. **议程项软删 + 级联软删**：删除议程项时，关联 attachments + upload-tasks 在同事务里 `UPDATE deletedAt = now()`；删除段时，段下所有项也软删（连同其 attachments + tasks）。**所有 list/find 查询默认带 `WHERE deletedAt IS NULL`**。attachment 软删后 30 天 cron 物理清理 + unlink 文件。
2. **系列议程独立**：每场会议自己一套议程，**不在 series 层做议程模板**。理由：避免"模板 vs 实例"双向同步复杂度；运营反馈系列内会议议程差异大。
3. **会议结束后允许改议程**：保留可改但 UI 加 warning「该会议已结束/取消，是否仍要修改议程？」（i18n key `meeting.agenda.editAfterEndWarning`）；所有改动写审计（含 actor / 改前改后 / 时间 + `agendaModifiedAfterMeetingEnd: true` flag）。
4. **同议程项多 assignee 各管各自资料**：
   - 任务 A、B 都分给 item X，A、B 各自上传 attachment
   - A 能看到 / 下载 B 的 attachment，反之亦然
   - A 仅能删自己上传的，B 不能删 A 的
   - manager / creator 可删任意 attachment（强删）
5. **上传权限分层**：
   - `meeting:attachment:upload:any`（manager / creator）：可上传到任意议程项或会议级，不需要 task 关联
   - `meeting:attachment:upload:assigned`（Leader / Employee）：仅能上传到"分配给自己的 PENDING task 对应的议程项"，上传后 task 自动转 UPLOADED
   - 上传不需要 task 命中的场景（如 manager 直接上传）：attachment 直接挂 item，不创建 task
6. **外部访客（MeetingExternalAttendee）受限视图**：仅返回 section title + item title 数组；description / presenter / categoryTag / code / attachments 字段不返回（后端层面剥离，不依赖前端隐藏）。
7. **task 状态机**：
   - `PENDING` → `UPLOADED`（被分配人上传成功后自动转）
   - `PENDING` → `CANCELLED`（manager / creator 主动取消）
   - `UPLOADED` 不可回退；`CANCELLED` 不可回退
   - task 删除（DELETE endpoint）= 软删（`UPDATE deletedAt = now()`）；不再回退状态
8. **议程项 `code` 字段可选自填**：用户可填 `S1-ECAI051826-1` 这种业务编号，**系统不解析、不校验、不生成**；仅作展示与跨会议人工对照。重复允许（不建唯一索引）。
9. **categoryTag 值域固定**：`FFAI_EAI_EV` / `EAI_ROBOTICS` / `MIXED` / `OTHER`，二期可加值不删值。
10. **文件 MIME 白名单**（v1.0 锁定）：`application/pdf` / `application/vnd.openxmlformats-officedocument.wordprocessingml.document` (docx) / `application/vnd.openxmlformats-officedocument.presentationml.presentation` (pptx) / `application/vnd.openxmlformats-officedocument.spreadsheetml.sheet` (xlsx) / `image/jpeg` / `image/png` / `video/mp4` / `video/quicktime` (mov)。其他 MIME 拒收。**不含**老 Office (`application/msword` / `application/vnd.ms-*`) / `text/plain` / `text/csv`。
11. **重复分配任务静默跳过**：同 (agendaItemId, assigneeUserId) 已有非 `CANCELLED` 任务时，**后端跳过该人不重复创建**（不抛 409），响应中通过 `skippedExistingUserIds[]` 标注；前端 toast「已有 X 人已分配，跳过」。
12. **审计扩展**：复用 `MeetingAttendanceAuditLog`；扩展 `backend/src/modules/meeting-attendance/constants/audit.ts` 的 `MEETING_ATTENDANCE_AUDIT_ACTIONS` 常量对象，新增 10 个键；`audit_logs.action` 字段类型不变（`String VarChar(100)`），**不需要 prisma migration**（AuditAction 不是 prisma enum 而是 TS 常量）。

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

**环境变量**（v1.0 新增，落 `.env.example`；env 名 / 默认值见 `06-data-model.md §文件存储约定` 单源）：
- `MEETING_ATTACHMENT_STORAGE_ROOT` — 资料本地存储根目录（默认 `./var/meeting-attachments`），缺失症状：上传 500，日志 `ENOENT: no such file or directory`
- `MEETING_ATTACHMENT_MAX_BYTES` — 单文件最大字节数（默认 `209715200`，即 200MB），缺失症状：默认值生效
- `MEETING_ATTACHMENT_ALLOWED_MIMES` — MIME 白名单，逗号分隔（默认见业务规则 §10），缺失症状：默认白名单生效
- `MEETING_ATTACHMENT_PHYSICAL_DELETE_DAYS` — 软删 attachment 物理清理保留天数（默认 30），缺失症状：默认值生效
- `MEETING_ATTACHMENT_GC_INTERVAL_HOURS` — tmp 文件 + orphan 文件 GC 间隔（默认 1 小时），缺失症状：默认值生效

**明确不做**：
- 不复用 `platform_master.Attachment`：cuid / uuid 类型不兼容；platform_master 是跨模块共用资源，议程附件强耦合 meeting-attendance 业务语义（任务 / 议程项关联），独立建表更清晰
- 不做议程模板（一期）
- 不做版本历史（一期）
- 不做资料在线预览（一期）
- 不在 series 层挂议程（与"系列议程独立"决策一致）

### 业务流程（概览）

```mermaid
graph TD
  A[登录进入系统] --> B[会议列表]
  B --> C[创建会议/系列]
  C --> D[生成二维码]
  D --> E[参会人扫码签到]
  E --> F[出勤统计/报表]
  B --> G[参会名单维护]
  G --> E
```
