会议议程能力 v1.0 · 设计总览

模块:meeting-attendance · 分支:feature/meeting-agenda-management · 2026-05-20 · 一个 PR 完整交付

1. 一句话目标

meeting-attendance 模块加一套结构化三层议程(段→主题项→字段)+ 管理员显式分配上传任务 + 议程项级 + 会议级双层资料上传。员工"我的待办"自动收到任务,所有参会人能看议程 + 下载资料。

这里"结构化"是核心 — 我们没把议程当一坨富文本(最朴素方案 Meeting.agenda Text 一个字段),而是按你给的 Excel 真实结构(FFAI EC Meeting 议程模板)建模成三层实体 + 类型化字段,让"分配任务给 S1 这个议程项的 Presenter 上传材料"这种需求可以零字符串拼接就实现。

2. 数据库设计

5 个新实体 + 3 个枚举(schema platform_meeting_attendance

erDiagram Meeting ||--o{ MeetingAgendaSection : has Meeting ||--o{ MeetingAttachment : has MeetingAgendaSection ||--o{ MeetingAgendaItem : contains MeetingAgendaItem ||--o{ MeetingAgendaItemUploadTask : assigns MeetingAgendaItem ||--o{ MeetingAgendaItemAttachment : has MeetingAgendaSection { uuid id PK string meetingId FK int order string title uuid createdById FK uuid organizationId timestamp deletedAt } MeetingAgendaItem { uuid id PK uuid sectionId FK int order string title text description string code int timeMinutes uuid presenterUserId FK enum categoryTag } MeetingAgendaItemUploadTask { uuid id PK uuid agendaItemId FK uuid assigneeUserId FK uuid assignedById FK enum status timestamp dueAt timestamp completedAt } MeetingAgendaItemAttachment { uuid id PK uuid agendaItemId FK uuid uploadedById FK string filename string mimeType bigint size string storagePath } MeetingAttachment { uuid id PK string meetingId FK uuid uploadedById FK enum category string filename }

设计要点

要点说明为什么
三层结构(如 "0. Attendance Check")→ 主题项(如 "S1-ECAI051826-1 EAI Brain Updates")→ 字段(title / description / time / presenter / category / code)跟你给的 Excel 真实结构 1:1,新议程项不用动 schema
独立 attachment 实体MeetingAgendaItemAttachment(议程项级)+ MeetingAttachment(会议级)复用 platform_master.Attachment 通用模型通用 Attachment 的 ownerId 是 UUID,而 Meeting.id 是 cuid VarChar(32),类型不兼容;强行复用要么改 24 个现有 model,要么改全项目其它模块。新建独立 model 隔离风险
软删而非硬删所有 5 表加 deletedAt DateTime?,删除 = UPDATE deletedAt = now()。配 cron GC 30 天后物理清议程项里可能挂着员工辛苦上传的视频材料,硬删 = 不可恢复;30 天软删窗口 + audit 可追溯,是合规审计场景的常规设计
标准字段强制5 表都加 createdById / organizationId / deletedAt / createdAt / updatedAtCLAUDE.md §标准字段 强制要求,多租户 DataScope 自动生效,未来加 organizationId 隔离零改动
cuid + uuid 混用Meeting 还是 cuid(VarChar 32),议程 5 表用 UUID不改现有 Meeting(影响面太大),FK 单方向 VarChar→UUID 在 Prisma 支持,跨类型对照清晰
order int 字段拖排序用整数 order,reorder 接口 { ids: [...] } 一次性整批重写简单直观;不用 lexicographic 字符串(更复杂、调试难)

3 个枚举

枚举取值用途
UploadTaskStatusPENDING / UPLOADED / CANCELLED上传任务状态机,UPLOADED 和 CANCELLED 是终态不可回退
AgendaCategoryTagFFAI_EAI_EV / EAI_ROBOTICS / MIXED / OTHER按 EC Meeting Excel 的"主题分类"列。议程项可选标签
MeetingAttachmentCategoryMINUTES / MATERIAL / PRESENTATION / OTHER会议级附件分类(会议纪要 vs 准备材料 vs 演示文稿)

3. 后端 22 个 API

按 RESTful + 嵌套资源风格组织,base /api/v1/meeting-attendance/

分组端点数路径示例权限
议程查看 + 段 CRUD 5 GET /meetings/:id/agenda
POST/PATCH/DELETE /meetings/:id/agenda/sections[/:sid]
PATCH .../sections/reorder
read 所有参会人
update Manager+creator
议程项 CRUD 4 POST/PATCH/DELETE /agenda-sections/:sid/items[/:iid]
PATCH .../items/reorder
update Manager+creator
上传任务 4 POST/GET/PATCH/DELETE /agenda-items/:iid/upload-tasks[/:tid] task:assign Manager+creator
我的待办 1 GET /my-upload-tasks?status=PENDING&sort=dueAt_asc 登录即可
议程项级附件 3 POST/GET/DELETE /agenda-items/:iid/attachments[/:aid] upload:anyupload:assigned
会议级附件 3 POST/GET/DELETE /meetings/:id/attachments[/:aid] upload:any
下载 2 GET /attachments/agenda-item/:id/download
GET /attachments/meeting/:id/download
download 所有参会人

4. 文件上传链路(最技术含量的一段)

200MB 视频上传是这次最大技术难点。下面是端到端流程:

sequenceDiagram participant U as 浏览器 participant FE as Next.js 前端 participant CD as Caddy 反代 participant BE as NestJS 后端 participant FS as 磁盘存储 participant DB as PostgreSQL participant AU as AuditLog U->>FE: 拖拽 200MB MP4 FE->>FE: 1. 前端 MIME 白名单 + 大小校验
(失败立即提示,不发请求) FE->>CD: POST multipart/form-data + 进度 CD->>BE: 反代(max_body 200MB + timeout 10min) BE->>BE: 2. multer diskStorage 写 tmp 文件
limits.fileSize 200MB BE->>BE: 3. 读首 4KB magic bytes 二次校验
对照 Content-Type,不一致 = 415 BE->>FS: 4. rename tmp → 正式路径
${yyyy}/${mm}/${uuid}.${ext} BE->>DB: 5. INSERT attachment row
同事务 UPDATE task.status = UPLOADED BE->>AU: 6. ATTACHMENT_UPLOADED audit BE-->>FE: 201 + attachment id FE->>U: 进度 100% + 任务徽章变绿

这里防住的 5 类坑

防御
① 默认 multer memoryStorage 200MB 入内存爆 heap显式 diskStorage + tmp 在同盘(避免 EXDEV rename 失败)
② Caddy 默认 body 10MB / timeout 30s 截断部署文档明示 request_body { max_size 200MB } + 10min timeout
③ .exe 改头为 application/pdf 绕过白名单server 端 magic bytes 二次校验(用 file-type lib 读首 4KB)
④ 上传 80% 时关浏览器 → orphan tmp 文件永远不删req.on('aborted') 兜底 unlink + 每小时 cron GC 扫 storage_root vs DB diff
⑤ 文件 BigInt size JSON.stringify 抛 TypeError全局 BigInt.prototype.toJSON 补丁,size 序列化为 string

下载链路(同样要小心)

5. 关键技术决策(5 个)

① 议程项硬删 → 软删 + cron GC(doc-review 中改)
原决策:硬删连带 attachment / task;doc-review 抓出 7 个失败模式(OOM / 半路崩 / 并发 race / disk 满 / orphan 文件 / cascade race / magic bytes 安全)。改软删后:议程项 deletedAt 标记 → 子资源 service 层显式 cascade soft-delete → cron 30 天后物理清。员工误删能恢复 + 审计可追溯。
② 显式分任务(不自动从 Presenter 推断)
议程项有 presenterUserId 字段,但不自动生成上传任务给他。原因:演讲人 ≠ 准备资料的人;admin 经常需要分给"协助准备的同事"。显式 assign 更灵活,UI 也清晰。
③ 资料绑定两层(议程项级 + 会议级)
议程项级(attachment 挂在某个主题项下)适合"S1 战略主题的演示文稿";会议级(挂在 meeting 本身)适合"会议纪要" / "整体材料"。如果只一层会发现 70% 资料都挂在"我的待办"那种 jam-up 议程项上,没法表达"全场参考材料"。
④ 会议结束后允许改议程 + UI warning + audit
PRD 原决策。理由:admin 可能开完会才发现议程录入错了,硬拦反而逼用户绕过流程。warning + audit 标 agendaModifiedAfterMeetingEnd: true = 透明可追溯。
⑤ 重复分配静默跳过 + 返 skippedExistingUserIds[]
admin 批量勾选 10 个 user 分配任务,其中 3 个已经分过。如果整批 409 拒绝 → admin 必须先手动剔除已分过的,体验差。静默跳过新分 7 个 + UI toast "已有 3 人已分配" → admin 拿到结果不返工。

6. 前端 UI

议程编辑页 /meetingattendance/[id]/agenda/edit

┌──────────────────────────────────────────────────┐
│  会议 X · 议程编辑                  [返回] [保存]  │
├──────────────────────────────────────────────────┤
│  ┌──────────────┐  ┌──────────────────────────┐  │
│  │ 议程 tree    │  │ 选中项编辑表单              │  │
│  │              │  │                            │  │
│  │ ▼ 0. 签到段  │  │ 主题项标题                  │  │
│  │   • 自动签到 │  │ [_______________________]   │  │
│  │ ▼ 5. 战略    │  │ 描述                       │  │
│  │   • S1 EAI   │← │ [_______________________]   │  │
│  │   • S2 UES   │  │ 时长 (分钟)  [____]         │  │
│  │   • S3 ...   │  │ Presenter   [选员工▼]      │  │
│  │              │  │ 分类         [战略▼]        │  │
│  │ [+ 加段]     │  │                            │  │
│  └──────────────┘  └──────────────────────────┘  │
│  操作:拖排序 / 加项 / 删项 / 分配任务 / 上传      │
└──────────────────────────────────────────────────┘

"我的待办"页 /meetingattendance/my-tasks

┌──────────────────────────────────────────────────┐
│ 我的待办(PENDING 3)   筛选 [PENDING▼] 排序 [▼]  │
├──────────────────────────────────────────────────┤
│ ┌──────────────────────────────────────────────┐ │
│ │ 📅 2026 EC Meeting / 5.S1 EAI Brain Updates  │ │
│ │ 截止:2026-05-25 18:00   分配人:admin       │ │
│ │ 状态:PENDING        [去上传 →]              │ │
│ └──────────────────────────────────────────────┘ │
│ ┌──────────────────────────────────────────────┐ │
│ │ 📅 ...   [去上传 →]                          │ │
│ └──────────────────────────────────────────────┘ │
│            [加载更多]                            │
└──────────────────────────────────────────────────┘

文件上传组件(共享)

关键库选型

需求选用对比
拖排序@dnd-kit/core + @dnd-kit/sortable(项目已装)vs react-beautiful-dnd(已停止维护)。dnd-kit 是当前 React 拖拽事实标准
i18n项目自带嵌套 TS 对象(locales/meetingAttendance/{zh,en}.ts)vs next-intl / i18next。项目已成体系不重造
表单状态本地 useState + 显式保存按钮vs react-hook-form。MVP 字段不多,简单方案
上传 progressaxios onUploadProgressvs fetch + XMLHttpRequest。axios 项目已有

7. 用户流程

#场景关键步骤
1编辑议程MeetingManager进会议详情 → 编辑议程 → 加段/项 → 拖排序 → 保存
2分配上传任务MeetingManager议程项「分配任务」→ 弹窗选 user(s) + dueAt → 确认 → 重复者自动跳过
3员工接任务上传Employee"我的待办" → 点任务 → 跳议程项 → 拖文件 → 完成 → 任务自动 UPLOADED
4参会人下载资料所有参会人会议详情 → 议程展开 → 点附件名 → 下载(含中文文件名 RFC 5987 编码)
5议程项删除连带MeetingManager删项 → 弹窗"将连带删 N 附件 + M 任务,30 天内可恢复" → 确认 → 软删

8. 测试覆盖

L1 集成测试 39 用例(全过 / 22.7s)

分组用例数覆盖
A. 议程段 CRUD5加 / 改 / 软删连带 / 权限拒绝 / 拖排序
B. 议程项 CRUD5加(含 categoryTag)/ 改 / 软删 / 拖排序 / 无效 presenter
C. 上传任务 CRUD6批量分配 / 列表 / 取消 / 删除 / 权限 / 我的待办分页
D. 议程项级上传8正常 / assignee auto-flip / 非 owner 拒绝 / 200MB 边界 / MIME 白名单 / magic bytes 不匹配 / 软删项 / 重复
E. 会议级上传3正常 / 权限 / 列表
F. 下载4正常 / 非参会人 / 软删 / 200MB 不爆内存 + RFC 5987 中文 filename
G. 删除3本人删 / 非本人拒 / manager 强删
H. 边界 + 权限5会议结束后改议程 + audit / 外部访客字段裁剪 / multi-assignee 并发 / audit 字段断言

L2 MCP 测试(11 场景,写在 10-e2e-test-spec.md,user 手工验或 E2E 阶段跑)

9. 二期承诺(独立工单)

说明为什么 v1.0 不做
议程模板从 MeetingTemplate 派生议程结构MVP 范围控制,先验证议程能力被用起来再做模板
Excel 导入议程把你给的 Excel 直接 parse 进议程同上 + Excel 格式各部门各种各样,规则收敛后再做
议程变更 push 通知notification-engine 联动 Email / IM等 notification-engine 加 N 个事件后做
资料版本历史同 task 多次上传保留全部版本v1.0 覆盖式 + 30 天软删窗口够用
code 跨会议追踪S1-ECAI051826-1 这种 code 跨 N 场会议自动追踪需要单独 entity + 跨 meeting 查询能力,二期
议程审批流approval-center 联动大部分内部会议议程不需要审批,先观察使用
S3 / OSS 存储从本地 disk 换到对象存储storage 接口已抽象(LocalDiskStorage),二期只需新加 S3Storage 类不动业务代码

📦 最终交付(51 文件 / build 0 error / L1 39 用例全过)

后端

前端

13 份文档

从 🚧 In Development 推进到 ✅ v1.0:README / 01-prd / 02-user-journey / 03-architecture / 04-state-machine / 05-ui-interaction-spec / 06-data-model / 07-api / 08-error-codes / 09-test-scenarios / 10-e2e-test-spec / 11-user-guide / 99-changelog。

设计参考:FFAI EC Meeting 议程模板(工作簿2.xlsx)· 实施流程:plan-feature → contract-check → docs-main → doc-review 4-lens → database-main → backend-main → frontend-main → test-main