# 会议出勤（Meeting Attendance）- 用户场景文档

> **版本**: v1.0
> **最后更新**: 2026-03-02
> **编写者**: 待定

---

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

### 场景清单

| 场景 | 角色 | 触发条件 | 目标 | 结果 |
|------|------|----------|------|------|
| 创建会议并生成二维码 | 管理员/会议管理员 | 已登录 | 生成会议与二维码 | 会议创建成功，二维码可用 |
| 系统用户签到 | 普通员工 | 已登录并扫码 | 完成签到 | 出勤记录更新 |
| 访客免登录签到 | 访客 | 扫描访客二维码 | 完成签到 | 参会名单内签到成功，出勤记录更新 |
| 系列会议批量改期 | 管理员/会议管理员 | 需要调整时间 | 批量调整时段 | 未来会议时间更新且二维码重生成 |
| 查看报表 | 管理员/领导 | 已登录 | 获取出勤统计 | 报表正常展示 |
| 批量导入参会人 | 管理员/会议管理员 | 已登录 | 快速导入参会名单 | 参会名单与初始出勤记录生成 |
| 手动纳管会议 | 管理员/会议管理员 | 候选会议已同步到系统 | 选择要纳管的会议 | 自动生成或更新对应签到项 |
| 系列纳管并排除单次实例 | 管理员/会议管理员 | 系列已纳管 | 排除某次 occurrence/exception | 该实例不再进入签到同步，其他实例不受影响 |
| 强制接管绑定 | 管理员/会议管理员 | 会议已被他人绑定 | 接管责任与同步来源 | 绑定人与来源邮箱切换并写入审计 |
| 上线手工截断历史系列 | 管理员/会议管理员 | 上线切换窗口 | 截断本系统历史系列 | 删除未来未进行实例，后续由 Outlook 纳管生成 |
| 编辑会议议程 | 会议管理员 | 进入会议详情 | 维护议程段与议程项 | 议程结构保存，详情页议程段实时刷新 |
| 分配议程项上传任务 | 管理员/会议管理员 | 议程项已存在 | 为议程项指派资料上传责任人 | 创建上传任务并通知员工 |
| 员工查看待办并上传资料 | 普通员工 | 已被指派上传任务 | 完成议程项资料上传 | 任务状态变 `UPLOADED`，待办列表移除 |
| 参会人下载议程附件 | 任意参会人 | 进入会议详情 | 下载议程项附件 | 文件成功下载 |
| 删除议程项连带资源 | 会议管理员 | 议程项存在 | 移除议程项及其附件/任务 | 议程项、附件、上传任务全部软删（deletedAt 标记）并写审计；30 天后 cron 物理清理 |

---

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

### 用户角色

| 角色 | 描述 | 权限级别 |
|------|------|---------|
| 会议管理员 | 会议与参会人管理者 | 管理员 |
| 会议组织者 | 负责某会议的组织 | 管理员 |
| 领导 | 关注报表与统计 | 管理者 |
| 普通员工 | 参会人员 | 普通用户 |
| 访客 | 免登录参会人员（需在系统用户名单中） | 免登录 |

### 场景详情（示例）

- **场景名称**: 访客免登录签到
- **前置条件**: 访客获得二维码链接
- **正常路径**: 扫码打开页面 → 输入姓名/邮箱 → 匹配参会名单（系统用户） → 记录出勤
- **异常路径**: 不在参会名单 → 返回提示并拒绝签到

- **场景名称**: Outlook 候选会议纳管
- **前置条件**: 当前管理员邮箱在 Outlook 可见该事件
- **正常路径**: 管理员进入候选会议池 → 筛选并勾选会议 → 选择纳管 -> 系统创建/绑定本地会议与签到项（系列会议按 seriesMaster 统一纳管）
- **异常路径**: 会议已被他人绑定 -> 引导管理员执行“接管”

- **场景名称**: 系列单次实例排除
- **前置条件**: 目标系列已纳管
- **正常路径**: 在候选列表点击“排除此实例”或在纳管详情中维护排除列表 -> 系统记录 exclusion 规则 -> 增量/对账同步跳过该实例
- **异常路径**: 非系列绑定或实例不属于该系列 -> 返回参数错误并保持当前纳管状态不变

- **场景名称**: 强制接管绑定
- **前置条件**: 当前用户具备同步页面访问权限（Administrator/MeetingManager）
- **正常路径**: 候选列表显示“已绑定（绑定人）” -> 点击“接管” -> 系统更新绑定人 + 同步来源邮箱并记录审计 -> 后续同步由接管人负责
- **异常路径**: 接管来源邮箱无法读取该事件 -> 阻止接管并提示先修复邮箱可见性

---

### 场景：MeetingManager 编辑议程

- **角色**: 会议管理员（MeetingManager / 会议 creator）
- **前置条件**:
  - 会议处于 `SCHEDULED` 或 `IN_PROGRESS` 状态
  - 当前用户拥有 `meeting:agenda:update` 权限
- **正常路径**:

  | 步骤 | 操作 | 系统响应 |
  |------|------|---------|
  | 1 | 进入会议详情页 | 议程段显示当前议程结构（可能为空） |
  | 2 | 点「编辑议程」按钮 / Tab | 跳转 `/meetingattendance/[meetingId]/agenda/edit` |
  | 3 | 新增议程段 | 调用 POST agenda section 接口，左列 tree 追加段卡片 |
  | 4 | 段内新增议程项 | 创建议程项（title / description / timeMinutes / presenter / categoryTag） |
  | 5 | 拖拽段或项调整顺序 | 调用排序接口，前端即时 reorder |
  | 6 | 完成后返回会议详情 | 议程段刷新到最新结构 |

- **异常路径**:
  - 非管理员且非 creator 访问编辑页 → `AGENDA_FORBIDDEN_NOT_MANAGER_OR_CREATOR`，前端跳回详情
  - 会议状态为 `COMPLETED` / `CANCELLED` → **允许编辑**，但前端 warning 弹窗「该会议已结束/取消，是否仍要修改议程？」（i18n key `meeting.agenda.editAfterEndWarning`），确认后继续；audit 写 `agendaModifiedAfterMeetingEnd: true`
  - 字段校验失败（title 空、timeMinutes 如填则必须 > 0）→ 表单字段下方红字提示
- **后置条件**: 议程结构持久化，会议详情议程段、议程项徽章实时同步
- **业务规则**:
  - 议程项 `timeMinutes` 可选；如填则必须 > 0；累计可超过会议时长（仅警告，不阻止）
  - 议程项 `presenter` 可选；如填则必须为有效系统用户
  - 议程编辑采用「实时保存」策略，每次操作即调 API（不积累为整体提交）

---

### 场景：管理员分配上传任务

- **角色**: 会议管理员（MeetingManager / creator）
- **前置条件**:
  - 议程项已存在
  - 目标参会人在系统用户表中
- **正常路径**:

  | 步骤 | 操作 | 系统响应 |
  |------|------|---------|
  | 1 | 议程编辑页或会议详情议程段点议程项「分配任务」 | 弹窗打开，加载用户多选组件与 `dueAt` 日期选择 |
  | 2 | 多选 user(s) + 可选填 `dueAt` | 前端校验至少 1 人 |
  | 3 | 确认提交 | 后端创建 N 条 `MeetingAgendaItemUploadTask`（status=`PENDING`） |
  | 4 | 通知被指派人 | 进入「我的待办」列表 |
  | 5 | 议程项徽章刷新 | 显示 `PENDING N / UPLOADED M` |

- **异常路径**:
  - 同一议程项重复分配同一用户且任务未终态 → **后端静默跳过该人不重复创建**（不抛错），响应 `skippedExistingUserIds[]` 标注；前端 toast「已有 X 人已分配，跳过」，其余正常创建
  - 选择的用户不在系统 → 弹窗内联报错
  - 选择的用户不在会议参会名单 → 后端 `MEETING_ATTENDANCE_011` (403)
  - 议程项已被删除 → `AGENDA_ITEM_NOT_FOUND`，刷新议程段
- **后置条件**: 议程项任务列表更新；被指派人在「我的待办」可见
- **业务规则**:
  - 一名 user 对同一议程项同时只能有一个非终态任务
  - `dueAt` 可空；为空表示无明确截止
  - 同一议程项的任务可来源不同管理员，互不干扰

---

### 场景：员工查看待办并上传资料

- **角色**: 普通员工（被分配上传任务的 user）
- **前置条件**:
  - 已登录
  - 至少存在一条 `assigneeUserId = 当前用户 && status = PENDING` 的上传任务
- **正常路径**:

  | 步骤 | 操作 | 系统响应 |
  |------|------|---------|
  | 1 | 进入「我的待办」`/meetingattendance/my-tasks` | 列表加载 PENDING 任务卡片 |
  | 2 | 点任务卡片 | 跳转会议详情议程项位置（锚点定位） |
  | 3 | 点「上传资料」按钮 | 弹文件选择/拖拽组件 |
  | 4 | 选择/拖入文件（≤200MB / 类型合法） | 前端校验通过后开始上传，进度条显示 |
  | 5 | 上传完成 | 后端写 `MeetingAgendaItemAttachment` 并 auto-flip 该 user 当前任务 `PENDING → UPLOADED` |
  | 6 | 返回待办页 | 已上传任务从列表移除（或按筛选切到 UPLOADED 区） |

- **异常路径**:
  - 文件 > 200MB → `ATTACHMENT_TOO_LARGE`，前端先拦截不发请求
  - mimeType 不在允许列表 → `ATTACHMENT_MIME_NOT_ALLOWED`，前端先拦截
  - 试图上传到他人议程任务上下文 → `UPLOAD_TASK_NOT_OWNED`，按钮置灰
  - 网络中断 → 上传失败提示，可重试
- **后置条件**:
  - 议程项附件列表 +1
  - 当前 user 的对应任务 status = `UPLOADED`
  - 同议程项其他 user 的任务不受影响
- **业务规则**:
  - 上传成功即自动 flip 任务状态，无需手动「标记完成」
  - 同一议程项可被多个 user 并行上传；附件归属议程项不归属任务

---

### 场景：参会人下载议程附件

- **角色**: 任意参会人（required / optional / organizer）
- **前置条件**:
  - 会议存在且当前用户在参会名单内
  - 议程项存在附件
- **正常路径**:

  | 步骤 | 操作 | 系统响应 |
  |------|------|---------|
  | 1 | 进入会议详情页 | 议程段渲染议程项及附件列表 |
  | 2 | 展开议程项 | 显示附件文件名 / 大小 / 上传人 / 时间 |
  | 3 | 点附件文件名 | 后端 stream 下载（大文件不进内存） |

- **异常路径**:
  - 非参会人访问 → 403 拒绝，前端跳回列表
  - 附件已被删除 → `ATTACHMENT_NOT_FOUND`，附件行灰显
- **后置条件**: 文件成功下载到本地；后端可记录下载审计（按需）
- **业务规则**:
  - 大文件采用流式下载，避免后端内存爆
  - 下载不改变附件 / 任务状态

---

### 场景：议程项删除连带 attachment + task 软删（30 天 cron 物理清理）

- **角色**: 会议管理员（MeetingManager / creator）
- **前置条件**: 议程项存在；当前用户拥有 `meeting:agenda:update` 权限
- **正常路径**:

  | 步骤 | 操作 | 系统响应 |
  |------|------|---------|
  | 1 | 议程编辑页选中议程项 → 点「删除」 | 弹警告确认：「将连带删除该项的 N 个附件 + M 个待办任务（30 天内可联系 admin 恢复，之后物理清理）」 |
  | 2 | 管理员确认 | 后端事务里对 attachment / upload task / agenda item 三表统一 `UPDATE deletedAt = now()` |
  | 3 | 写入审计 `AGENDA_ITEM_DELETED`，记录连带影响数 | 议程段刷新（查询默认 `WHERE deletedAt IS NULL` 过滤） |
  | 4 | 已分配该项任务的员工待办列表自动剔除 | 不报错（任务已被软删过滤） |

- **异常路径**:
  - 非管理员触发 → `AGENDA_FORBIDDEN_NOT_MANAGER_OR_CREATOR`
  - 会议已 `COMPLETED` / `CANCELLED` → **允许删除**，但前端 warning 弹窗「该会议已结束/取消，是否仍要修改议程？」，确认后继续；audit 加 `agendaModifiedAfterMeetingEnd: true`
  - 后端事务失败 → 整体回滚，UI 提示「删除失败，请重试」
- **后置条件**:
  - 议程项与其附件、待办任务全部软删（`deletedAt` 非空）；DB 行仍在，list/find 默认过滤
  - 审计日志含 `attachmentDeletedCount` / `taskDeletedCount`
  - 30 天后 cron 物理清理 attachment + unlink 底层文件
- **业务规则**:
  - 议程项 = 资源根；附件 / 任务无独立生命周期
  - 软删 + 30 天物理清理窗口，兼顾"避免孤儿"和"可恢复"
  - 删除前必须给管理员明确连带数量 + 恢复提示
