会议议程能力 v1.0 · 设计总览
模块:meeting-attendance · 分支:feature/meeting-agenda-management · 2026-05-20 · 一个 PR 完整交付
1. 一句话目标
2. 数据库设计
3. 后端 22 API
4. 文件上传链路
5. 关键技术决策
6. 前端 UI
7. 用户流程
8. 测试覆盖
9. 二期承诺
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 / updatedAt CLAUDE.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/agendaPOST/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:any 或 upload:assigned
会议级附件
3
POST/GET/DELETE /meetings/:id/attachments[/:aid]
upload:any
下载
2
GET /attachments/agenda-item/:id/downloadGET /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
下载链路(同样要小心)
Stream 返回(不 Buffer.from(file),否则 200MB 占内存)
Content-Disposition 用 RFC 5987:filename="ascii-fallback.pdf"; filename*=UTF-8''%E4%B8%AD... — 中文文件名才不会乱码
反向代理 timeout 同样要配 10min(下载比上传慢的情况都有)
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 [去上传 →] │ │
│ └──────────────────────────────────────────────┘ │
│ ┌──────────────────────────────────────────────┐ │
│ │ 📅 ... [去上传 →] │ │
│ └──────────────────────────────────────────────┘ │
│ [加载更多] │
└──────────────────────────────────────────────────┘
文件上传组件(共享)
拖拽 + 点击选择 · 多文件队列(≤ 10)· 并发 3
前端校验:MIME 白名单 8 项 + 单文件 ≤ 200MB · 失败立即提示,不发请求
拒目录拖入:webkitGetAsEntry().isDirectory 拦截 + toast
每文件独立 progress bar · 失败 toast + 重试按钮
i18n:meetingAttendance.attachment.tooLarge 等 ~110 个 key 双语
关键库选型
需求 选用 对比
拖排序 @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 字段不多,简单方案
上传 progress axios onUploadProgress vs 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. 议程段 CRUD 5 加 / 改 / 软删连带 / 权限拒绝 / 拖排序
B. 议程项 CRUD 5 加(含 categoryTag)/ 改 / 软删 / 拖排序 / 无效 presenter
C. 上传任务 CRUD 6 批量分配 / 列表 / 取消 / 删除 / 权限 / 我的待办分页
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 阶段跑)
拖排序持久化、议程项 CRUD UI、分配任务弹窗、员工上传 progress、200MB 上传、>200MB 前端拦截、.exe 拒绝、下载、议程项删除连带确认、双语切换、Employee 议程只读
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 用例全过)
后端
5 prisma 实体 + 3 enum + 1 migration(20 FK / 14 index)
22 API 端点 分 5 个 controller
8 services (业务 + cron GC + magic bytes + 下载 stream + storage 抽象)
11 错误码
6 权限点
file-type@^16 用于 magic bytes 校验
前端
2 新路由 :议程编辑 / 我的待办
1 新 component :FileUpload(共享,可复用其它模块)
3 个 API service :agenda / upload-task / attachment
~110 i18n key zh / en 双语
现有会议详情页 加议程展示段(不动签到/报表)
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