Initialize backendized SurClaw report system

- Add React/Vite frontend for login, dashboard, reports, templates, users, settings, AI, speech, and media workflows.

- Add NestJS/Prisma/PostgreSQL backend with auth, dashboard stats, reports, templates, users, departments, settings, files, AI, speech, audit logs, and HTML sanitization.

- Add Prisma schema, migrations, seed data, persistent app sessions, Docker/Nginx deployment files, and upload volume configuration.

- Add Vitest, Playwright, backend integration tests, and project documentation for requirements, design, permissions, API contracts, testing, deployment, security, and progress.

- Configure production local fallback switch and remove unused Gemini direct dependency/env wiring.
This commit is contained in:
2026-05-02 01:37:20 +08:00
commit 014aca8619
162 changed files with 27116 additions and 0 deletions

View File

@@ -0,0 +1,74 @@
# 模块文档:认证与用户权限
## 涉及文件
- `src/pages/Login.tsx`
- `src/api/client.ts`
- `src/api/users.ts`
- `src/api/files.ts`
- `src/auth/AuthContext.tsx`
- `src/auth/backendUser.ts`
- `src/pages/UserManage.tsx`
- `src/components/Sidebar.tsx`
- `src/types.ts`
- `src/utils/storage.ts`
- `server/src/users`
- `server/src/files`
## 登录流程
`Login` 页面挂载时仍执行迁移期初始化,补齐默认模板、默认用户、字段库、图片资源和系统设置。提交登录表单后,前端通过 `AuthContext.login` 调用 `POST /api/auth/login`,后端验证 Argon2 密码哈希并写入 HttpOnly Session Cookie。
登录成功后,`AuthContext` 会把后端返回的安全用户 DTO 转成当前前端 `User` 结构,并同步到 `localStorage.currentUser``localStorage.users`。这是为了兼容字段、设置、用户管理和开发回退逻辑;生产构建默认不会用本地缓存恢复登录态。
`AuthProvider` 启动时会调用 `GET /api/auth/me` 恢复后端会话,退出登录调用 `POST /api/auth/logout` 并清理本地 `currentUser`
后端角色 `doctor` 会映射为当前前端历史类型中的 `user`。前端会保留本地签名和已有模板授权字段,避免迁移期丢失医生个人模板或签名。
## 默认账号
| 用户ID | 密码 | 角色 | 说明 |
| --- | --- | --- | --- |
| `admin` | `123456` | 超级管理员 | 初始最高权限。 |
| `manager` | `123456` | 管理员 | 外科管理员。 |
| `0001` | `123456` | 医生 | 外科医生账号。 |
## 权限规则
- `super`:可查看和管理全部用户、模板、报告、系统配置。
- `admin`:可管理同部门医生或自己;不可创建超级管理员;新增用户默认是医生。
- `user`:只能管理本人报告;不能进入模板管理和用户管理。
用户管理中还有这些约束:
- 不允许删除默认超级管理员 `admin`
- 不允许删除当前登录用户。
- 一个部门只能存在一个管理员。
- 创建医生前,该部门必须已有管理员。
- 禁用管理员类账号需要授权密钥。
`UserManage` 页面当前优先调用后端 `Users/Departments` API。只有开发模式或显式开启 `VITE_ENABLE_LOCAL_FALLBACK=true`API 不可用才保留本地列表回退,便于迁移期演示和离线开发;生产安全边界以服务端校验为准。
## 用户与部门 API
- `GET /api/users`:超级管理员返回全部用户;管理员返回自己和本部门医生;医生只返回自己。
- `POST /api/users`:超级管理员可创建管理员或医生;管理员只能创建本部门医生;后端校验一个部门只能有一个管理员,且创建医生前部门必须已有管理员。
- `PATCH /api/users/:id`:超级管理员可改角色、部门、状态和模板授权;管理员只能改本部门医生的基础信息、密码和状态;医生只能改自己的基础资料。
- `DELETE /api/users/:id`:禁止删除当前账号和默认超级管理员;有业务数据关联时返回冲突,建议改为禁用。
- `GET /api/departments`:超级管理员返回全部部门;管理员/医生返回本人部门。
- `PATCH /api/departments/:id/template-permissions`:超级管理员维护部门可用和可管理模板授权。
## 模板权限
用户有两个模板权限字段:
- `manageableTemplates`:可管理的模板 ID。
- `visibleTemplates`:新建报告时可使用的模板 ID。
超级管理员拥有全部模板。管理员可被超级管理员分配部门可管理模板;医生可见模板来自本部门可用模板和自己的个人模板。当前前端仍沿用 `visibleTemplates/manageableTemplates` 字段展示,但后端数据来源是 `TemplateDepartmentPermission`
## 签名
用户管理支持上传签名图片,前端会压缩为 JPEG Data URL再调用 `POST /api/users/:id/signature` 写入后端 `FileResource` 和本地文件目录。后端把 `signatureFileId` 写回用户表,并通过 `/api/files/:id/content` 提供受控读取。报告模板中若存在 `surgeonSignature` 绑定字段,报告编辑器会尝试用当前用户签名 URL 渲染。
只有本地回退开启时API 不可用才会保留本地 `User.signature` 作为迁移期回退;模板图片、关键帧和视频已迁移到同一文件资源体系,后续应继续正规化报告媒体关系。

View File

@@ -0,0 +1,98 @@
# 模块文档:报告编辑器
## 涉及文件
- `src/pages/ReportEditor.tsx`
- `src/api/reports.ts`
- `src/api/ai.ts`
- `src/api/speech.ts`
- `src/api/library.ts`
- `src/api/files.ts`
- `src/utils/defaultContent.ts`
- `src/utils/print.ts`
- `src/types.ts`
## 模块职责
报告编辑器是系统核心模块,负责新建/编辑报告、字段同步、富文本排版、视频关键帧处理、AI 辅助撰写、语音输入、草稿恢复和报告保存。
## 页面模式
- `/report-editor`:新建报告。
- `/report-editor?id=RPT_xxx`:编辑已有报告。
- `/report-editor?id=RPT_xxx&restore=1`:从历史版本恢复内容后进入编辑器。
## 草稿机制
编辑器会在卸载、页面隐藏和部分操作后写入 `reportEditorDraft_${username}`。草稿包含正文 HTML、表单数据、视频列表、关键帧、当前标签页、模板 ID 和 AI 聊天上下文。
保存报告后会清理草稿。
## 智能字段
模板中的智能字段形如:
```html
<span class="field-value" data-bind="patientName" contenteditable="true"></span>
```
编辑器会把侧边表单状态同步到正文,也会在用户直接编辑正文中的绑定字段时反向更新表单。日期和时间字段会按字段配置做格式化和解析。
字段库优先从 `/api/library/fields` 读取,成功后同步 `formFieldsConfig``multiSelectOptions``anesthesiaOptions` 兼容缓存;只有开发模式或显式开启 `VITE_ENABLE_LOCAL_FALLBACK=true`API 不可用才继续读取本地缓存。
## 图片占位符
`.image-placeholder` 用于在模板或报告中保留图片位置。
- 普通占位符可点击选择图片,也可接收视频关键帧。
- `data-mode="manual"` 用于 Logo、签名等静态位置不允许拖入关键帧。
- 已填充图片后会增加 `.has-image`
模板图片资源优先从 `/api/files?kind=TEMPLATE_ASSET` 读取只有本地回退开启时API 不可用才继续读取本地 `imageAssets`
## 视频与抽帧
用户上传视频后,系统使用浏览器对象 URL 进行即时预览,同时优先通过 `POST /api/files``kind = VIDEO` 上传后端文件资源。抽帧逻辑按系统设置中的 `framePositions` 百分比定位视频时间,绘制到 canvas 后转为 JPEG并优先以 `kind = FRAME` 上传后端文件资源。
支持:
- 自动抽帧。
- 手动截帧。
- 点击关键帧跳转视频时间。
- 拖拽关键帧到报告占位符。
- 自动把选中的抽帧序号插入空置图片占位符。
报告保存前会等待当前视频和关键帧上传完成,并把 `fileId/url`、排序和抽帧信息通过 Reports API 同步到后端 `ReportMedia` 关系表。只有本地回退开启时API 不可用才保留本地对象 URL/Data URL 回退。
## AI 撰写
AI 面板支持两种模式:
- 对话模式:根据当前报告内容和图片上下文回答问题。
- 修改模式:选中 `.ai-region` 后要求模型返回 JSON其中包含 `reply``updatedHtml`
模型返回 HTML 后,系统会清理换行和 `<br>`,生成差异预览。用户确认后才写入目标 `.ai-content`
当前 AI 调用使用后端代理:
- `/api/ai/chat`
- 后端读取全局共用 Provider Key 并代理到 OpenAI 兼容 `/chat/completions`
- 支持图片以 `image_url` 内容传入
## 语音输入
讯飞听写通过后端 WebSocket 代理:
- 前端连接 `/api/speech/iat`,不再生成讯飞鉴权 URL也不读取 APPID/APIKey/APISecret。
- 浏览器采集麦克风音频,转换为 16k PCM 后发送音频帧。
- 后端读取 Settings API 中的 `xfSpeechConfig`,连接讯飞 IAT上游首帧由后端补齐 `common.app_id` 和默认 `business` 参数。
- 识别结果由后端转发回前端,并追加到 AI 输入框。
## 保存规则
- 保存草稿不强制患者姓名和住院号。
- 完成报告要求患者姓名和住院号。
- 保存时优先调用 `POST /api/reports``PATCH /api/reports/:id`;后端会先做 HTML 白名单清洗,再写入 PostgreSQL 并维护历史版本。
- 编辑已有已完成报告时,后端会把旧内容追加到历史记录并递增 `revision`
- 只有本地回退开启时API 不可用才保留原有本地 `localStorage.reports` 保存逻辑;生产构建默认关闭这条路径。
- 完成报告后跳转到报告管理页。

View File

@@ -0,0 +1,53 @@
# 模块文档:报告管理与查看
## 涉及文件
- `src/pages/ReportManage.tsx`
- `src/pages/ReportView.tsx`
- `src/api/reports.ts`
- `server/src/reports/*`
- `src/utils/print.ts`
- `src/types.ts`
## 报告管理
`ReportManage` 优先通过 `src/api/reports.ts` 调用 `GET /api/reports` 读取报告列表,后端根据当前 Session 用户做权限过滤:
- 医生只显示本人创建的报告。
- 管理员只显示本部门报告。
- 超级管理员显示全部报告。
前端仍支持按标题、患者姓名、住院号搜索,按状态和时间筛选。只有开发模式或显式开启 `VITE_ENABLE_LOCAL_FALLBACK=true`API 不可用才会回退读取 `localStorage.reports`,用于迁移期旧数据和离线测试。
## 操作能力
- 查看:进入 `/report-view/:id`
- 编辑:进入 `/report-editor?id=...`
- 删除:优先调用 `DELETE /api/reports/:id` 做后端软删除再同步本地兼容缓存只有本地回退开启时API 不可用才会从本地 `reports` 中移除。
- 历史版本:查看 `Report.history`,可恢复某个历史内容到编辑器。
- 导出 PDF调用浏览器打印。
- 导出 JSON下载结构化字段数据。
- 批量导出和批量删除:基于表格选中项操作。
医生只能编辑或删除自己的报告。
## 历史恢复
历史版本恢复时,管理页把目标 HTML 写入 `sessionStorage.restore_${reportId}`,再跳转到编辑器。编辑器读取后填入正文并移除 session 键。
## 报告查看
`ReportView` 优先通过 `GET /api/reports/:id` 获取报告,后端检查查看权限。只有本地回退开启时,失败才会回退本地报告和前端权限判断。报告正文通过 `dangerouslySetInnerHTML` 渲染,保存侧已由后端做第一版 HTML 白名单清洗,页面提供浏览器打印入口。
## 后端报告模型
后端 `Report` 表保存标题、患者姓名、住院号、HTML 正文、作者、部门、状态和修订版本。`Report.metadata` 暂存患者扩展字段;视频和关键帧文件内容写入 `FileResource`,报告中的媒体引用、排序和抽帧信息写入 `ReportMedia` 关系表。Reports API 返回时仍组装成前端兼容的 `videos``capturedFrames` 字段。
## 导出 JSON 内容
当前报告 JSON 导出主要包含:
- `meta`:报告 ID、标题、创建/更新时间、作者和状态。
- `fields`:按 `DEFAULT_FORM_FIELDS` 提取的报告字段。
报告正文 HTML 和视频帧数据不在这个结构化字段导出范围内。

View File

@@ -0,0 +1,60 @@
# 模块文档:系统设置
## 涉及文件
- `src/pages/SystemSettings.tsx`
- `src/api/settings.ts`
- `src/types.ts`
- `src/utils/storage.ts`
- `server/src/settings`
## 模块职责
系统设置负责维护视频抽帧、AI 接口、讯飞语音和默认模板配置。超级管理员可以看到全部设置;其他用户主要配置默认报告模板。
页面优先调用 `GET /api/settings/system` 读取设置,保存时调用 `PATCH /api/settings/system`。只有开发模式或显式开启 `VITE_ENABLE_LOCAL_FALLBACK=true`API 不可用才保留 `localStorage.systemSettings` 回退。后端使用 `SystemSetting` 表保存全局设置,并用 `scope = user:<id>` 保存个人默认模板。
## 视频抽帧配置
配置字段包括:
- `frameCount`:抽取帧数。
- `framePositions`:每帧对应的视频进度百分比。
- `frameMode`:调整帧数时使用整体均匀抽取或保持当前抽帧。
- `autoInsertFrames`:是否自动插入关键帧。
- `autoInsertDelay`:自动插入延迟。
- `autoInsertFrameIndices`:哪些抽帧序号参与自动插入。
保存时会对 `framePositions` 排序,并把 `frameCount` 同步为位置数组长度。
## AI 接口配置
`activeAiProvider` 指向当前服务商,`aiProviders` 保存各服务商配置。默认服务商包括:
- Kimi
- DeepSeek
- OpenAI
- Custom
系统通过后端 `/api/ai/models` 测试连接和获取模型列表,通过 `/api/ai/chat` 在报告编辑器中生成内容。后端读取全局共用 Provider Key 并代理 OpenAI 兼容 `/models``/chat/completions`,普通用户读取设置时不会拿到 AI Key。
## 讯飞语音配置
`xfSpeechConfig` 保存讯飞 WebSocket IAT 所需的 APPID、APIKey 和 APISecret。报告编辑器只连接本系统 `/api/speech/iat`,由后端读取这些配置、生成讯飞鉴权 URL 并转发音频和识别结果。普通用户读取设置时不会拿到 APIKey/APISecret。
## 默认模板
`defaultTemplate` 指向模板 ID。新建报告时如果当前用户可见该模板编辑器会自动加载对应模板内容否则回退到默认内置报告内容。
## 重置能力
超级管理员可执行:
- 恢复系统设置出厂设置:优先调用 `POST /api/settings/system/reset`,只有本地回退开启时失败才重置本地 `systemSettings`
- 重置全部数据:仅本地回退开启时执行 `localStorage.clear()` 并刷新;生产构建默认阻止把本地清空误认为后端数据重置。
## 注意事项
- `systemSettings` 的本地混淆不等于安全加密;当前仅作为开发/显式本地回退模式下 API 不可用时的兼容缓存。
- 初始化、类型和系统设置页面已统一使用 `xfSpeechConfig`;当前由后端语音代理使用。
- 默认 API Key 或语音密钥不应留在生产前端代码中。

View File

@@ -0,0 +1,88 @@
# 模块文档:模板管理
## 涉及文件
- `src/pages/TemplateManage.tsx`
- `src/api/templates.ts`
- `src/api/library.ts`
- `src/api/files.ts`
- `server/src/templates/*`
- `server/src/library/*`
- `server/src/files/*`
- `src/utils/defaultContent.ts`
- `src/utils/print.ts`
- `src/types.ts`
## 模块职责
模板管理用于维护报告模板 HTML、字段库、图片资源、AI 可编辑区域和模板导入导出。医生不可进入该模块。
## 模板列表
模板列表优先通过 `GET /api/templates?access=manage` 读取,后端根据当前 Session 用户过滤可管理模板:
- 超级管理员可管理全部模板。
- 管理员可管理本部门模板或被授权可管理模板。
- 医生不能进入模板管理页,但可以通过报告编辑器新建自己的个人模板。
只有开发模式或显式开启 `VITE_ENABLE_LOCAL_FALLBACK=true`API 不可用才回退读取 `localStorage.templates`用于迁移期旧数据和离线测试。API 返回空列表会按真实空数据展示,不再自动把本地模板当作生产数据。
支持:
- 新增模板。
- 编辑模板名称和描述。
- 删除或批量删除模板。
- 保存当前模板内容。
- 打印模板预览。
- 单个导出和批量导出 JSON。
- 从 JSON 模板包导入。
新增、编辑、保存内容和删除模板会优先调用后端 `POST/PATCH/DELETE /api/templates`,后端会对模板 HTML 做白名单清洗;成功后同步 `localStorage.templates` 作为兼容缓存。只有本地回退开启时API 失败才允许写本地模板。
## 模板内容编辑
模板正文是 `contentEditable` HTML。工具栏支持常见富文本命令、表格、图片占位符和 AI 区域插入。编辑器内部维护 undo/redo 栈,并根据内容高度扩展 A4 页面高度。
## 字段库
字段定义优先通过 `GET/PATCH /api/library/fields` 读写,后端保存到 `SystemSetting.key = fieldLibrary``formFieldsConfig` 继续作为迁移期兼容缓存,并仅在本地回退开启且 API 不可用时作为回退。字段支持:
- 显示/隐藏。
- 系统锁定字段保护。
- 文本、单选、多选、日期、时间、签名、图片等类型。
- 选项维护。
- 时间格式和默认值。
- 下划线样式。
插入字段时,模板会写入带 `data-bind``.field-value`,报告编辑器按该 key 进行表单同步。
## 图片资源
模板图片资源优先通过通用文件 API 上传和读取:
- `GET /api/files?kind=TEMPLATE_ASSET`
- `POST /api/files`
- `DELETE /api/files/:id`
页面仍把资源同步到 `imageAssets` 作为迁移期兼容缓存。只有本地回退开启时API 不可用的上传图片才会回退为本地 Data URL。
## 后端模板模型
后端 `Template` 表保存模板名称、描述、HTML 正文、字段 JSON、模板范围、归属部门或归属用户。部门授权由 `TemplateDepartmentPermission` 表维护,支持 `canUse``canManage`。医生个人模板使用 `scope = PERSONAL``ownerUserId` 归属本人。
## 导入导出格式
单模板导出大致结构:
```json
{
"version": "1.0",
"type": "surclaw_template_package",
"title": "模板名称",
"description": "模板描述",
"content": "模板 HTML",
"fields": []
}
```
批量导出使用 `type: "surclaw_template_package_batch"`,包含 `templates` 数组。当前导入逻辑只接受单模板包。