From 911b96b883c0b9d862d26010aa79640a3ab40a7e Mon Sep 17 00:00:00 2001 From: admin <572701190@qq.com> Date: Sat, 2 May 2026 02:52:30 +0800 Subject: [PATCH] Add demo mode factory reset - Align the backend seeded default surgery template with the report editor's default report content. - Add backend demo defaults for the default template, Kimi provider, and Xunfei speech proxy configuration. - Change system reset into a super-admin demo mode factory reset that clears reports, audit logs, files, custom templates, and non-default users. - Keep only the default admin, manager, doctor, and default surgery template after demo reset. - Replace the old local-only reset all data button with a two-confirmation backend reset flow. - Add tests covering demo default alignment and database-backed demo reset behavior. - Update docs to describe demo mode reset semantics and production credential cautions. --- AGENTS.md | 2 + README.md | 1 + docs/api-contract.md | 9 +- docs/data-storage.md | 2 +- docs/features.md | 2 +- docs/installation.md | 2 +- docs/modules/system-settings.md | 7 +- docs/progress.md | 3 +- docs/requirements.md | 2 +- docs/security.md | 2 +- docs/testing.md | 3 +- server/prisma/seed.ts | 50 +++++-- server/src/database.integration.test.ts | 62 ++++++++ server/src/demo/demo-defaults.test.ts | 26 ++++ server/src/demo/demo-defaults.ts | 33 +++++ server/src/settings/settings.service.ts | 183 +++++++++++++++++++++--- src/pages/SystemSettings.tsx | 57 ++------ 17 files changed, 361 insertions(+), 85 deletions(-) create mode 100644 server/src/demo/demo-defaults.test.ts create mode 100644 server/src/demo/demo-defaults.ts diff --git a/AGENTS.md b/AGENTS.md index bba9fcb..e5f9cc3 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -104,6 +104,7 @@ npm run test:e2e - 页面级权限在前端用于体验控制,不能抵御绕过;生产安全边界以后端 API 权限校验为准。 - 报告和模板 HTML 保存时已做服务端白名单清洗;前端仍使用 HTML 渲染,继续修改时要留意 XSS 和打印兼容。 - AI Key 和讯飞语音密钥已由后端代理使用,普通用户读取系统设置时不会返回真实密钥。 +- 当前 demo mode 后端默认值包含演示用第三方服务凭据,生产化前必须替换或移除,并轮换曾经暴露过的密钥。 - 视频和关键帧文件已优先进入后端文件资源;报告保存时通过 `ReportMedia` 关系表关联,新建报告保存前仍依赖浏览器对象 URL 预览。 - `VITE_ENABLE_LOCAL_FALLBACK` 控制生产构建是否允许本地兼容回退;开发模式默认允许,生产默认关闭。 @@ -192,6 +193,7 @@ npm run test:e2e │ │ ├── audit/ # 审计日志写入和查询 API │ │ ├── auth/ # 登录、me、logout 接口 │ │ ├── dashboard/ # 工作台统计 API +│ │ ├── demo/ # 演示模式默认模板、AI 和语音配置 │ │ ├── reports/ # 报告 API、DTO、metadata 映射和测试 │ │ ├── templates/ # 模板 API、DTO、权限映射和测试 │ │ ├── users/ # 用户、部门和模板授权 API、DTO 映射和测试 diff --git a/README.md b/README.md index 5e391fc..c64584c 100644 --- a/README.md +++ b/README.md @@ -248,6 +248,7 @@ docker-compose down - 权限判断主要在前端,不能作为生产安全边界。 - 报告和模板 HTML 保存时已做服务端白名单清洗,但渲染仍使用 HTML,需要继续做安全评审。 - AI Key 和讯飞语音密钥已由后端代理使用;普通用户读取设置时不会拿到真实密钥。 +- 当前 demo mode 后端默认值包含演示用第三方服务凭据,生产部署前必须替换或移除,并轮换曾经暴露过的密钥。 - 视频和关键帧已优先上传后端文件资源,报告保存时通过 `ReportMedia` 关系表关联;新建报告保存前仍依赖本地预览对象。 生产化方向见 [docs/backendization-plan.md](./docs/backendization-plan.md)。 diff --git a/docs/api-contract.md b/docs/api-contract.md index 2de0cf9..d143b16 100644 --- a/docs/api-contract.md +++ b/docs/api-contract.md @@ -471,7 +471,14 @@ pageSize?: number ### `POST /api/settings/system/reset` -只有超级管理员可重置全局系统设置。 +只有超级管理员可执行演示模式恢复出厂设置。当前实现不只是重置系统设置,而是把当前租户恢复为 demo mode: + +- 用户只保留默认 `admin`、`manager`、`0001` 三个账号,并重置为默认角色、部门、状态和密码。 +- 报告、报告历史、报告媒体、文件资源和审计日志会被清空。 +- 模板只保留默认“腹腔镜胆囊切除术报告”,模板 HTML 与图文报告生成的默认报告内容保持一致。 +- 系统设置恢复为演示默认值,包含默认模板、抽帧策略、Kimi Provider 和讯飞语音代理配置。 + +前端必须做二次确认。该接口面向演示/测试环境,不应作为生产数据恢复或备份机制。 ## Signature Files API diff --git a/docs/data-storage.md b/docs/data-storage.md index 5524013..ab09a9b 100644 --- a/docs/data-storage.md +++ b/docs/data-storage.md @@ -50,7 +50,7 @@ - 报告保存为草稿或完成态时优先写入后端;开发回退开启时会同步 `reports` 缓存,API 不可用时才写入本地 `reports`。 - 新建报告成功保存后会清理当前用户草稿。 - 编辑已有报告时会把旧内容推入 `history`。 -- 开发回退模式下,系统设置页“重置全部数据”会执行 `localStorage.clear()` 并刷新页面;生产构建默认阻止把该操作误认为后端数据重置。 +- 系统设置页“恢复演示出厂设置”会调用后端 `POST /api/settings/system/reset` 恢复 demo mode,并在成功后清理当前浏览器缓存再刷新页面。 ## 迁移注意 diff --git a/docs/features.md b/docs/features.md index 1aef072..7218a1e 100644 --- a/docs/features.md +++ b/docs/features.md @@ -45,7 +45,7 @@ | AI 差异确认 | 真实可用 | 使用 `diff` 生成左右差异,确认后写入 AI 区域。 | | 讯飞语音听写 | 真实集成 | 前端使用麦克风采集音频并连接 `/api/speech/iat`;后端读取讯飞配置、生成鉴权 URL、补齐首帧 APPID/业务参数并转发 IAT 结果。需要浏览器权限、有效配置和网络。 | | AI/语音密钥管理 | 真实集成 | AI Key 和讯飞 APIKey/APISecret 均由后端代理读取和使用;普通用户读取设置时不返回真实密钥。 | -| 系统设置 | 真实集成 | `SystemSettings` 优先调用 `/api/settings/system` 读取、保存和重置抽帧、默认模板、AI Provider、语音配置;只有开发/显式回退模式下 API 不可用才回退本地缓存。 | +| 系统设置 | 真实集成 | `SystemSettings` 优先调用 `/api/settings/system` 读取和保存抽帧、默认模板、AI Provider、语音配置;“恢复演示出厂设置”会二次确认后调用后端 demo reset,清空报告/审计并恢复默认用户、模板和演示配置。只有开发/显式回退模式下 API 不可用才回退本地缓存。 | | 审计日志查看 | 真实集成 | 超级管理员和管理员可进入审计日志页,调用 `GET /api/audit-logs` 查看登录、报告、模板、用户、部门、设置和文件等操作;管理员只看本部门或自己相关日志。 | | Docker/Nginx 静态部署 | 真实可用 | 可构建静态文件并用 Nginx 托管 SPA。 | | 后端服务 | 后端骨架 | 已新增 NestJS API:健康检查、认证接口、数据库 Session、Dashboard API、报告 API、报告媒体关系、模板 API、字段库 API、用户/部门 API、设置 API、通用文件/签名文件 API、视频/关键帧文件上传、AI 代理、讯飞语音代理、HTML 清洗、审计日志查询、Prisma/PostgreSQL 数据模型、默认 seed 和权限策略。 | diff --git a/docs/installation.md b/docs/installation.md index a95fbd5..4524bfa 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -143,7 +143,7 @@ VITE_API_PROXY_TARGET="http://localhost:3002" | `manager` | `123456` | 管理员 | | `0001` | `123456` | 医生 | -登录后建议先进入“系统设置”确认 AI Provider、讯飞语音配置、默认模板和抽帧策略。AI 与语音密钥保存在后端 Settings API 中,不应写入源码、文档或提交记录。 +登录后建议先进入“系统设置”确认 AI Provider、讯飞语音配置、默认模板和抽帧策略。当前 demo mode 已内置演示用 AI 与语音配置;正式生产部署前必须替换或移除这些演示凭据,后续通过后端 Settings API 或正式密钥管理流程维护。 ## 初次验收 diff --git a/docs/modules/system-settings.md b/docs/modules/system-settings.md index f5abaac..c570788 100644 --- a/docs/modules/system-settings.md +++ b/docs/modules/system-settings.md @@ -50,11 +50,12 @@ 超级管理员可执行: -- 恢复系统设置出厂设置:优先调用 `POST /api/settings/system/reset`,只有本地回退开启时失败才重置本地 `systemSettings`。 -- 重置全部数据:仅本地回退开启时执行 `localStorage.clear()` 并刷新;生产构建默认阻止把本地清空误认为后端数据重置。 +- 恢复演示出厂设置:调用 `POST /api/settings/system/reset`,前端做二次确认后由后端恢复 demo mode。 + +当前 demo mode reset 会清空报告、报告历史、报告媒体、文件资源和审计日志;用户只保留 `admin`、`manager`、`0001` 三个默认账号;模板只保留“腹腔镜胆囊切除术报告”;系统设置恢复默认抽帧策略、默认模板、Kimi Provider 和讯飞语音代理配置。该能力用于演示环境快速回到可演示状态,不是生产备份/恢复方案。 ## 注意事项 - `systemSettings` 的本地混淆不等于安全加密;当前仅作为开发/显式本地回退模式下 API 不可用时的兼容缓存。 - 初始化、类型和系统设置页面已统一使用 `xfSpeechConfig`;当前由后端语音代理使用。 -- 默认 API Key 或语音密钥不应留在生产前端代码中。 +- demo mode 内置第三方服务演示凭据,生产部署前必须替换或移除,并通过正式密钥管理流程维护。 diff --git a/docs/progress.md b/docs/progress.md index dac5463..8d42173 100644 --- a/docs/progress.md +++ b/docs/progress.md @@ -36,7 +36,7 @@ - 后端认证、Dashboard API、报告 API、报告媒体关系、模板 API、字段库 API、用户/部门 API、设置 API、通用文件/签名文件 API、AI 代理、语音代理和审计日志 API 已可用;第三方调用摘要、限流和后端导出仍待加强。 - 本地存储仍可能包含病历兼容缓存、旧演示密码字段、模板图片和关键帧,不适合生产;历史浏览器数据中也可能残留旧版语音服务密钥。 - `systemSettings` 的混淆存储不是加密。 -- 旧版本曾在前端默认配置中包含服务密钥痕迹;当前源码默认值已清空,但生产化前仍应轮换曾经暴露过的第三方密钥。 +- 当前 demo mode 后端默认值包含演示用第三方服务凭据;生产化前必须移除或替换,并轮换曾经暴露过的第三方密钥。 - 报告正文和模板正文保存时已做服务端白名单清洗,但仍以 HTML 字符串存储并通过 `dangerouslySetInnerHTML` 渲染,需要持续安全测试。 - 大视频和大量 Base64 图片会快速占满浏览器存储空间。 - `document.execCommand` 已是过时 API,但当前编辑器大量依赖它。 @@ -74,3 +74,4 @@ | 2026-05-02 | 新增审计日志查询 API/页面、Auth Context 路由角色守卫,并把 Playwright E2E 改为真实后端 API seed。 | | 2026-05-02 | 新增安装与初始设置文档,补充首次启动、端口规划、数据库初始化、验收步骤和常见问题。 | | 2026-05-02 | 新增前端组件结构文档,梳理页面组件、公共组件、API/Auth/Utils 分层、数据流和大组件拆分边界。 | +| 2026-05-02 | 将默认“腹腔镜胆囊切除术报告”后端 seed 与前端默认报告内容对齐,并把系统设置重置改为演示模式恢复出厂设置。 | diff --git a/docs/requirements.md b/docs/requirements.md index 17629b7..a70e63e 100644 --- a/docs/requirements.md +++ b/docs/requirements.md @@ -64,7 +64,7 @@ - 超级管理员可配置 AI 服务商、接口地址、API Key 和模型名。 - 超级管理员可配置讯飞语音听写参数。 - 所有角色可设置默认报告模板。 -- 超级管理员可恢复系统设置;清空全部本地数据仅作为开发/显式本地回退模式下的浏览器缓存操作,不代表清空后端业务数据。 +- 超级管理员可恢复演示出厂设置;该操作会二次确认后清空当前租户报告、审计、自定义模板和非默认用户,并恢复默认演示账号、模板、AI 和语音配置。 ## 非功能需求与约束 diff --git a/docs/security.md b/docs/security.md index b4d23bd..1d29a57 100644 --- a/docs/security.md +++ b/docs/security.md @@ -8,7 +8,7 @@ - 开发模式 `localStorage.users` 仍保留兼容缓存和旧演示密码字段;生产构建默认关闭本地回退。 - AI Key 和讯飞语音密钥已由后端代理使用;普通用户读取系统设置时不会返回真实密钥。超级管理员仍可维护全局密钥,应避免把密钥写入源码、日志或文档。 -- 旧版本源码中存在默认服务密钥痕迹,应视为已暴露;当前默认值已清空,但生产化前仍需轮换曾经暴露过的第三方密钥。 +- 当前 demo mode 后端默认值包含演示用第三方服务凭据;旧版本源码中也存在默认服务密钥痕迹,应视为已暴露。生产化前必须移除或替换演示凭据,并轮换曾经暴露过的第三方密钥。 - 报告和模板 HTML 保存时已做服务端白名单清洗,但仍直接渲染 HTML,需要继续做绕过测试和打印兼容测试。 - 浏览器存储没有审计、备份、权限隔离和加密能力。 - 医疗病历数据属于敏感数据,不能直接用于公网生产。 diff --git a/docs/testing.md b/docs/testing.md index e02bdfa..83c2304 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -90,12 +90,13 @@ AI 第三方接口、讯飞语音上游 WebSocket、麦克风权限和真实视 | 后端模板兼容映射 | 已覆盖 | `server/src/templates/template.mapper.test.ts` | | 后端用户兼容映射 | 已覆盖 | `server/src/users/users.mapper.test.ts` | | 后端系统设置 schema | 已覆盖 | `server/src/settings/settings.schemas.test.ts` | +| 演示模式默认值 | 已覆盖 | `server/src/demo/demo-defaults.test.ts` 覆盖后端默认模板与前端报告编辑器默认内容一致,并校验语音演示配置完整。 | | 后端 AI 代理 schema | 已覆盖 | `server/src/ai/ai.schemas.test.ts` | | 后端语音代理首帧处理 | 已覆盖 | `server/src/speech/xf-frame.test.ts` | | 后端字段库 schema | 已覆盖 | `server/src/library/library.schemas.test.ts` | | 后端文件 schema | 已覆盖 | `server/src/files/files.schemas.test.ts` | | 后端 HTTP 集成 | 已覆盖 | `server/src/http.integration.test.ts` | -| 后端真实数据库集成 | 已覆盖 | `server/src/database.integration.test.ts`,含 Dashboard 角色范围、报告媒体关系表同步、报告 HTML 清洗、审计写入和审计查询权限。 | +| 后端真实数据库集成 | 已覆盖 | `server/src/database.integration.test.ts`,含 Dashboard 角色范围、报告媒体关系表同步、报告 HTML 清洗、审计写入、审计查询权限和演示模式恢复出厂设置。 | | 后端健康检查和认证 API | 已覆盖 | HTTP 集成测试覆盖健康检查、登录 session 和未登录保护;真实数据库集成覆盖 Argon2 登录、禁用账号和数据库 Session Store。 | | 模板编辑器深度交互 | 待 E2E | 依赖 contentEditable 和 execCommand。 | | 报告编辑器完整流程 | 部分覆盖 | 已覆盖保存修订版本和个人模板;模板切换、字段同步仍待补。 | diff --git a/server/prisma/seed.ts b/server/prisma/seed.ts index 77aa71c..33193a6 100644 --- a/server/prisma/seed.ts +++ b/server/prisma/seed.ts @@ -1,6 +1,11 @@ import { PrismaClient } from '@prisma/client'; import { PrismaPg } from '@prisma/adapter-pg'; import argon2 from 'argon2'; +import { + DEMO_DEFAULT_REPORT_CONTENT, + DEMO_SYSTEM_SETTINGS, + DEMO_TEMPLATE_ID, +} from '../src/demo/demo-defaults.js'; if (!process.env.DATABASE_URL) { throw new Error('DATABASE_URL is required to seed the database'); @@ -12,16 +17,6 @@ const prisma = new PrismaClient({ }), }); -const defaultTemplateContent = ` -
患者姓名:
-住院号:
-手术名称:
-请在此处填写手术步骤。
temporary
', + fields: [], + scope: 'DEPARTMENT', + ownerDepartmentId: surgeryDepartmentId, + }, + }); + + const resetSettings = await settingsService.resetSystemSettings(superActor); + + await expect(prisma.report.count({ where: { tenantId } })).resolves.toBe(0); + await expect(prisma.auditLog.count({ where: { tenantId } })).resolves.toBe(0); + + const users = await prisma.user.findMany({ where: { tenantId }, orderBy: { username: 'asc' } }); + expect(users.map((user) => [user.username, user.name, user.role, user.status])).toEqual([ + ['0001', '张医生', 'DOCTOR', 'ACTIVE'], + ['admin', '超级管理员', 'SUPER', 'ACTIVE'], + ['manager', '科室管理员', 'ADMIN', 'ACTIVE'], + ]); + + const templates = await prisma.template.findMany({ where: { tenantId } }); + expect(templates).toHaveLength(1); + expect(templates[0]).toMatchObject({ + name: '腹腔镜胆囊切除术报告', + content: DEMO_DEFAULT_REPORT_CONTENT, + scope: 'DEPARTMENT', + }); + expect(resetSettings.defaultTemplate).toBe(templates[0].id); + expect(resetSettings.aiProviders.kimi.apiKey).not.toBe(''); + expect(resetSettings.xfSpeechConfig).toEqual(DEMO_XF_SPEECH_CONFIG); + }); }); const toActor = ( diff --git a/server/src/demo/demo-defaults.test.ts b/server/src/demo/demo-defaults.test.ts new file mode 100644 index 0000000..142f4f6 --- /dev/null +++ b/server/src/demo/demo-defaults.test.ts @@ -0,0 +1,26 @@ +import { describe, expect, it } from 'vitest'; +import { defaultReportContent } from '../../../src/utils/defaultContent'; +import { + DEMO_DEFAULT_REPORT_CONTENT, + DEMO_SYSTEM_SETTINGS, + DEMO_TEMPLATE_ID, + DEMO_XF_SPEECH_CONFIG, +} from './demo-defaults'; + +describe('demo defaults', () => { + it('keeps the backend default template aligned with the report editor default content', () => { + expect(DEMO_TEMPLATE_ID).toBe('tpl_default_surgery'); + expect(DEMO_DEFAULT_REPORT_CONTENT).toBe(defaultReportContent); + expect(DEMO_DEFAULT_REPORT_CONTENT).toContain('西 安 交 通 大 学 第 一 附 属 医 院'); + expect(DEMO_DEFAULT_REPORT_CONTENT).toContain('data-ai-id="手术步骤"'); + }); + + it('includes complete demo speech configuration for the backend proxy', () => { + expect(DEMO_SYSTEM_SETTINGS.defaultTemplate).toBe(DEMO_TEMPLATE_ID); + expect(DEMO_SYSTEM_SETTINGS.aiProviders.kimi.apiKey).not.toBe(''); + expect(DEMO_SYSTEM_SETTINGS.xfSpeechConfig).toEqual(DEMO_XF_SPEECH_CONFIG); + expect(DEMO_XF_SPEECH_CONFIG.appId).not.toBe(''); + expect(DEMO_XF_SPEECH_CONFIG.apiKey).not.toBe(''); + expect(DEMO_XF_SPEECH_CONFIG.apiSecret).not.toBe(''); + }); +}); diff --git a/server/src/demo/demo-defaults.ts b/server/src/demo/demo-defaults.ts new file mode 100644 index 0000000..b3353c0 --- /dev/null +++ b/server/src/demo/demo-defaults.ts @@ -0,0 +1,33 @@ +export const DEMO_TEMPLATE_ID = 'tpl_default_surgery'; + +export const DEMO_DEFAULT_REPORT_CONTENT = "\n\n 姓名: × \n 性别: × \n 年龄: × \n 科别: × \n 床号: × \n 住院号: ×\n
\n\n\n 手术日期: ×\n
\n\n 术前诊断: ×\n
\n\n 术中诊断: ×\n
\n\n 手术名称: ×\n
\n\n| 手术开始时间: × | \n手术终止时间: × | \n
| 手术者: × | \n助手: × | \n
| 麻醉师: × | \n麻醉方式: × | \n
\n 手术步骤、术中出现的情况及处理:\n
\n\n1.患者仰卧位,麻醉成功后,常规消毒术野、铺无菌巾,于脐下穿刺建立CO2气腹,气腹压力为12mmHg,进镜探查无穿刺损伤,分别于剑突下2.0cm、右锁中线肋缘下2.0cm各点穿刺置穿刺器,插入相应手术器械。
2.腹腔镜探查:腹腔内无腹水形成,无明显粘连,肝脏色红质软,无明显结节硬化改变,胆囊大小约 cm× cm× cm,壁轻度水肿,张力可,胆囊三角解剖关系清楚,胆囊管及胆总管无明显扩张。胃、十二指肠、小肠、结肠、脾脏及盆腔未见明显异常。术中诊断:胆囊结石伴慢性胆囊炎。遂行腹腔镜胆囊切除术。
3.切除胆囊:钳夹胆囊颈部并解剖胆囊三角,游离出胆囊动脉及胆囊管,明确胆囊与胆总管的关系,距胆总管0.3cm处近端以一枚可吸收夹,远端夹一枚钛夹夹闭胆囊管,两夹间以剪刀剪断胆囊管,另用一枚可吸收夹夹闭胆囊动脉后离断。顺行游离胆囊浆膜,完整切除胆囊后装入标本袋取出。胆囊床严密止血并覆盖止血材料。
4.检查腹腔内无活动性出血及漏胆后,清点器械纱布无误,拔除腔镜器械,排出腹腔残余气体,缝合各刺孔,术毕。
5.手术顺利,麻醉满意。切除的标本经家属过目后送病理。术中出血约 ml,术中输血成分,输血量,是否有输血不良反应。
| \n \n ×\n 插入/点击放置图片\n \n 图A 腹腔镜探查 \n | \n \n \n ×\n 插入/点击放置图片\n \n 图B 胆囊管夹闭与离断 \n | \n \n \n ×\n 插入/点击放置图片\n \n 图C 胆囊动脉夹闭与离断 \n | \n
| \n \n ×\n 插入/点击放置图片\n \n 图D 胆囊剥离与床面止血 \n | \n \n \n ×\n 插入/点击放置图片\n \n 图E 胆囊取出与钛夹确认 \n | \n \n \n ×\n 插入/点击放置图片\n \n 图F 止血材料覆盖及检查 \n | \n
\n 手术后情况: ×\n
\n \n\n 切除标本描述: ×\n
\n \n\n 是否送病理检查: ×\n
\n \n\n 冰冻病理结果: ×\n
\n \n\n 手术者签名:×插入/点击放置图片\n
\n \n\n \n
\n ×\n
\n