From 8809a28e9cd9dd0db0962f3895b1d3a883af4989 Mon Sep 17 00:00:00 2001 From: selfrelease Date: Mon, 10 Aug 2026 12:13:52 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=96=B0=E5=A2=9E=E6=B1=87=E6=8A=A5PPT?= =?UTF-8?q?=E4=B8=8E=E7=94=9F=E6=88=90=E8=84=9A=E6=9C=AC=EF=BC=8C=E4=BF=AE?= =?UTF-8?q?=E5=A4=8Dtsconfig=E5=BC=83=E7=94=A8=E5=91=8A=E8=AD=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增医科高校智能学习中心汇报PPT(27页,面向高校管理者与广电领导) - 新增 pptxgenjs 生成脚本 scripts/gen-pptx.js - 新增 SYSTEM-OVERVIEW.md 系统功能全景文档 - 新增 deploy.sh 部署脚本 - 补充 rate-limit guard、ai-stream controller、exam-prep 仓储等后端模块 - 补充 exam-recommendations、markdown、mock-exam、offline-indicator 前端组件 - tsconfig.json: 显式设置 rootDir=./src,加 ignoreDeprecations=6.0 静音 baseUrl 弃用告警 Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .vscode/settings.json | 2 - SYSTEM-OVERVIEW.md | 334 ++++ collegeAIcenter.code-workspace | 9 - deploy.sh | 164 ++ scripts/gen-pptx.js | 1395 +++++++++++++++++ src/common/rate-limit/ai-rate-limit.guard.ts | 83 + src/infra/ai-gateway/ai-stream.controller.ts | 89 ++ .../entities/question-bank.entity.ts | 30 + .../in-memory-question-bank.repository.ts | 71 + .../typeorm-question-bank.repository.ts | 55 + tsconfig.json | 2 + web/src/components/exam-recommendations.tsx | 87 + web/src/components/markdown.tsx | 59 + web/src/components/mock-exam.tsx | 322 ++++ web/src/components/offline-indicator.tsx | 35 + 医科高校智能学习中心-汇报.pptx | Bin 0 -> 704699 bytes 16 files changed, 2726 insertions(+), 11 deletions(-) delete mode 100644 .vscode/settings.json create mode 100644 SYSTEM-OVERVIEW.md delete mode 100644 collegeAIcenter.code-workspace create mode 100755 deploy.sh create mode 100644 scripts/gen-pptx.js create mode 100644 src/common/rate-limit/ai-rate-limit.guard.ts create mode 100644 src/infra/ai-gateway/ai-stream.controller.ts create mode 100644 src/modules/exam-prep/entities/question-bank.entity.ts create mode 100644 src/modules/exam-prep/repositories/in-memory-question-bank.repository.ts create mode 100644 src/modules/exam-prep/repositories/typeorm-question-bank.repository.ts create mode 100644 web/src/components/exam-recommendations.tsx create mode 100644 web/src/components/markdown.tsx create mode 100644 web/src/components/mock-exam.tsx create mode 100644 web/src/components/offline-indicator.tsx create mode 100644 医科高校智能学习中心-汇报.pptx diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index 7a73a41..0000000 --- a/.vscode/settings.json +++ /dev/null @@ -1,2 +0,0 @@ -{ -} \ No newline at end of file diff --git a/SYSTEM-OVERVIEW.md b/SYSTEM-OVERVIEW.md new file mode 100644 index 0000000..776bd63 --- /dev/null +++ b/SYSTEM-OVERVIEW.md @@ -0,0 +1,334 @@ +# 医科高校 AI 学习中心 · 系统功能全景图 + +## 系统定位 + +**医科类高校 AI 学习中心**是面向医学院校的智能学习平台,以"能力"为核心,贯穿基础医学→临床医学→见习实习→住院医师培养的完整成长链。系统将学生在校的所有学习活动(课程、对练、科研、协作)统一汇入能力图谱,形成数据贯通的闭环成长体系。 + +--- + +## 用户角色与核心价值 + +``` +┌─────────────────────────────────────────────────────────────────────┐ +│ 医科高校 AI 学习中心 │ +├────────────────┬───────────────────┬────────────────────────────────┤ +│ 🎓 学生 │ 👨‍🏫 导师 │ 🔧 管理员 │ +│ │ │ │ +│ • 全程学习汇聚 │ • 题目质量把关 │ • AI 技能定义与治理 │ +│ • AI 智能训练 │ • 成果点评指导 │ • 权限与合规管理 │ +│ • 能力画像可视化│ • 学生画像监测 │ • 审计日志与安全 │ +│ • 职业规划闭环 │ • 教学问责 │ │ +│ • 科研能力培养 │ │ │ +│ • 负责任AI协作 │ │ │ +└────────────────┴───────────────────┴────────────────────────────────┘ +``` + +--- + +## 功能模块全景 + +### 一、学生端功能矩阵 + +#### 1. 个人学习空间 +**价值**:一站式汇聚成长链上所有学习内容,告别分散记录 + +| 功能 | 说明 | +|------|------| +| 成果记录 | 支持 9 种类型:课程记录、作业、实验报告、临床见习/实习、OSCE 考核、文献阅读、科研成果、证书、执业资格备考 | +| 自动归档 | 按学年、学期、临床轮转科室三维度自动归类 | +| 里程碑标记 | 标记阶段性成长节点,关联该阶段内学习成果 | +| 智能检索 | 按类型/学年/学期/科室筛选,时间倒序,分页浏览 | +| 能力关联 | 每条成果自动映射到能力图谱中的能力标签 | + +#### 2. AI 评估与反馈 +**价值**:实时获得 AI 对学习进度的过程性与阶段性评价 + +| 功能 | 说明 | +|------|------| +| 形成性评价 | 每新增一条学习成果,30 秒内获得 AI 过程性反馈 | +| 终结性评价 | 完成里程碑时,基于该阶段全部成果生成综合画像 | +| 发展建议 | 每条建议精确指向能力图谱中的具体能力维度 | +| 可信标注 | 所有 AI 评价附带来源依据与置信度,可追溯核验 | + +#### 3. 课程对练 +**价值**:AI 生成高质量练习题,导师把关,模拟真实考试场景 + +``` +课程内容 ──AI生成题目──→ 导师审核 ──通过──→ 学生对练 ──→ 结果诊断 + │ + └─退回(附原因) +``` + +| 功能 | 说明 | +|------|------| +| 智能出题 | 支持 A1-A4 型选择题、病例分析题、临床决策题 | +| 权威保障 | 每题绑定权威知识源(教材/指南/官方题库) | +| 导师审核 | 题目经导师通过后才可使用,退回需注明原因 | +| 模拟对练 | 逐题呈现、限时作答(120秒/题)、即时判定 | +| 诊断报告 | 正确率、用时、薄弱环节分析(<60% 能力标签) | +| 改进建议 | 针对每个薄弱环节生成具体建议 | +| 间隔重复 | SM-2 算法优先复现错题与即将遗忘的题 | +| 模拟考试 | 限时批量答题,交卷后统一批阅 | + +#### 4. 临床情景对话对练 +**价值**:模拟真实问诊场景,训练临床沟通与决策能力 + +| 功能 | 说明 | +|------|------| +| 情景呈现 | 基于权威知识源的临床情景(含难度、时长、考核重点) | +| 多轮对话 | 与 AI 模拟患者进行最多 50 轮对话 | +| 角色扮演 | AI 以设定角色应答,保持情景连贯 | +| 三维评估 | 问诊完整性 + 临床推理 + 医患沟通 三方面评分 | +| 上下文记忆 | 携带最近 10 轮历史,保持对话连贯 | + +#### 5. 能力画像 +**价值**:全方位量化呈现个人能力分布,辅助就业与发展决策 + +``` + 知识掌握 + ▲ + 职业倾向 / \ 临床技能 + / \ + AI协同素养───科研能力 + \ + 人文素养 +``` + +| 功能 | 说明 | +|------|------| +| 六维画像 | 知识掌握、临床技能、科研能力、人文素养、AI协同素养、职业倾向 | +| 量化评分 | 每个维度 0-100 分值 | +| 数据溯源 | 每项结论可追溯到具体学习成果或对练记录 | +| 动态更新 | 能力图谱数据更新后自动重算 | +| 成长趋势 | 能力维度时间序列,可视化成长曲线 | + +#### 6. 职业规划 +**价值**:明确职业目标与能力差距,获得可执行的发展计划 + +``` +设定职业目标 → 匹配岗位胜任力模型 → 能力差距分析 → 生成发展规划 + ↑ │ + └── 能力更新后重算 ←──┘ + (动态闭环) +``` + +| 功能 | 说明 | +|------|------| +| 目标设定 | 选择或自定义职业目标 | +| 胜任力模型 | 参照权威医学框架(科学与学术、临床能力、健康与社会、职业素养) | +| 差距分析 | 逐项对照当前能力水平与岗位要求 | +| 行动建议 | 每个差距项提供具体行动与目标水平 | +| 资源推荐 | 为差距项推荐学习资源或对练任务 | +| 动态闭环 | 能力提升后重算差距,规划自动更新 | + +#### 7. 研究资料查询 +**价值**:高效检索医学文献,AI辅助总结与引用管理,支撑科研与论文写作 + +``` +自然语言描述 → PICO/MeSH检索式 → 多源检索 → 证据分级总结 → 引用导出 +``` + +| 功能 | 说明 | +|------|------| +| 检索式生成 | 自然语言自动转为 PICO 要素 + MeSH 主题词的专业检索式 | +| 多源检索 | 支持 PubMed、CNKI、万方、UpToDate、Cochrane 等 | +| 结果筛选 | 按发表时间、来源类型、相关度筛选 | +| 证据分级 | 对纳入总结的每条资料标注证据强度 | +| AI 总结 | 生成带引用的结论,每条可追溯来源 | +| 引用管理 | 支持 Vancouver / GB-T 7714 格式,导出 EndNote / NoteExpress | +| 版权安全 | 仅提供摘要与链接,不存储或分发受版权保护全文 | + +#### 8. AI 协同训练 +**价值**:系统性培养负责任使用AI的能力——这在医学领域尤为关键 + +| 评估维度 | 说明 | +|----------|------| +| 提问能力 | 能否向AI提出精准、有效的问题 | +| 批判性验证 | 能否对AI输出进行来源核验与批判性思考 | +| 责任边界意识 | 是否清楚AI辅助与人工决策的边界 | +| 协同工作流 | 能否有效组织人机协作流程 | + +- 每维度各有真实任务载体的训练任务 +- 直接采用未核验AI输出时会被要求来源核验 +- 评估结果计入能力画像与岗位胜任力对照 +- 支持导师对协同质量进行点评 + +#### 9. 补充模块(v0.2+) + +| 模块 | 价值 | +|------|------| +| 轮转见习 | 记录临床轮转经历 | +| 技能视频 | 搜索与学习操作技能视频 | +| 执业医师备考 | AI出题 + 间隔重复 + 模拟考试 + 个性化推荐 | +| 学术交流 | 记录学术交流与会议参与 | +| 医学问答 | AI流式多轮医学知识对话 | +| 病例推演 | 逐步推演临床病例,保持全程上下文 | +| 用药安全 | 用药安全性检查与学习 | + +--- + +### 二、导师端功能矩阵 + +| 功能 | 价值 | +|------|------| +| **题目审核** | 审核AI生成题目的准确性,通过或退回(附原因),保障教学内容质量 | +| **成果点评** | 对学生学习成果进行教学点评,点评对学生可见,促进学生反思与改进 | +| **带教学生画像** | 查看所带学生的能力画像(脱敏),精准把握学生发展状况,针对性指导 | + +--- + +### 三、管理端功能矩阵 + +| 功能 | 价值 | +|------|------| +| **技能治理** | 定义、配置、启用AI技能;每个技能含输入规格、处理逻辑、知识源、输出格式、可信度规则五要素 | +| **审计日志** | 追踪所有技能配置变更(操作者、时间、内容) | +| **权限管理** | 按角色分级定义访问权限范围 | +| **合规监控** | 查看越权拒绝事件,确保数据安全 | + +--- + +## 系统架构示意 + +``` +┌──────────────────────────────────────────────────────────────────────┐ +│ 用户界面层 │ +│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ +│ │ 学生端 │ │ 导师端 │ │ 管理端 │ Next.js + Tailwind │ +│ └────┬─────┘ └────┬─────┘ └────┬─────┘ │ +└───────┼───────────────┼───────────────┼──────────────────────────────┘ + │ │ │ + └───────────────┼───────────────┘ + ▼ +┌──────────────────────────────────────────────────────────────────────┐ +│ 接入层 · 认证鉴权 │ +│ JWT 认证 · 角色路由 · API 网关 │ +└────────────────────────────┬─────────────────────────────────────────┘ + │ + ┌────────────────────┼────────────────────┐ + ▼ ▼ ▼ +┌───────────────┐ ┌─────────────────┐ ┌───────────────────┐ +│ 横切关注点 │ │ 业务模块层 │ │ 底座层 │ +│ │ │ │ │ │ +│ • 合规模块 │ │ • 学习空间 │ │ • 能力图谱 │ +│ 权限分级 │ │ • AI评估引擎 │ │ 统一能力标签 │ +│ 数据脱敏 │ │ • 课程对练引擎 │ │ 跨模块数据打通 │ +│ 授权管理 │ │ • 临床对话对练 │ │ │ +│ 审计日志 │ │ • 画像引擎 │ │ • 技能能力中心 │ +│ │ │ • 职业规划 │ │ 统一AI技能框架 │ +│ • AI 可信层 │ │ • 研究资料查询 │ │ 8+可复用技能 │ +│ 可信度标注 │ │ • AI协同培养 │ │ │ +│ 来源溯源 │ │ • 医学问答 │ │ │ +│ 未核验标记 │ │ • 病例推演 │ │ │ +│ │ │ • 执业备考 │ │ │ +└───────────────┘ │ • 轮转/学术/… │ │ │ + └────────┬────────┘ └─────────┬─────────┘ + │ │ + └──────────┬──────────┘ + ▼ +┌──────────────────────────────────────────────────────────────────────┐ +│ 基础设施层 │ +│ │ +│ ┌──────────┐ ┌──────────────┐ ┌───────────────────────────────┐ │ +│ │PostgreSQL │ │ AI 网关 │ │ 资料来源适配器 │ │ +│ │ 数据库 │ │ (通义千问等) │ │ PubMed/CNKI/万方/UpToDate/… │ │ +│ └──────────┘ └──────────────┘ └───────────────────────────────┘ │ +└──────────────────────────────────────────────────────────────────────┘ +``` + +--- + +## 核心设计理念 + +### 1. 能力贯通 —— 打破数据孤岛 + +所有学习活动通过统一的**能力图谱**连接。学习空间新增一条成果、课程对练完成一次测验、临床对话获得评分——这些数据都映射到相同的能力标签体系上,使得: + +- 学生画像基于全量行为数据生成,而非单一模块的局部视图 +- 职业规划能精确对照当前能力与目标岗位的差距 +- 各模块的使用数据相互增强,形成正反馈循环 + +### 2. AI 可信优先 —— 医学场景零容忍幻觉 + +在医学教育场景下,AI 幻觉(生成不实信息)的风险极高。系统采用: + +- **所有 AI 输出必须附带可信度标注**(来源 + 置信度) +- **无法溯源的内容标注为"未经核验"** +- **题目必须绑定权威知识源**,否则不进入使用流程 +- **总结中无法追溯的结论标注"未验证"**,不作为引用输出 + +### 3. 合规默认 —— 敏感数据天然受保护 + +医学生画像包含敏感的健康、心理数据,系统采用: + +- **默认脱敏**:非本人访问画像,敏感字段自动脱敏 +- **知情同意授权**:跨用途使用必须有学生有效授权 +- **即时撤销**:学生撤销授权后立即失效 +- **全审计**:所有敏感操作留痕(保留≥12个月) +- **最小采集**:仅采集与画像维度直接相关的数据 + +### 4. 人机协同 —— AI 辅助而非替代 + +- AI 生成题目 → **导师审核后才可使用** +- AI 文献总结 → **明确提示"不替代阅读原文"** +- AI 协同训练 → **直接采用未核验输出会被要求核验** +- 所有 AI 建议 → **可追溯依据,用户可核验** + +--- + +## 用户使用流程示例 + +### 典型学生日常 + +``` +1. 登录 → 进入学习空间 +2. 记录今日临床见习成果 → 系统自动归档 + 能力标签映射 +3. 30秒后收到 AI 形成性评价与发展建议 +4. 进入课程对练 → AI 出题(已过导师审核)→ 模拟答题 → 查看诊断报告 +5. 进入临床对话对练 → 模拟问诊 → 获得三维评估 +6. 查看能力画像 → 六维度分数 + 成长趋势 +7. 进入职业规划 → 查看差距分析 → 按推荐资源学习 +8. 能力提升后 → 画像自动更新 → 发展规划动态调整(闭环) +``` + +### 典型导师工作 + +``` +1. 登录 → 进入导师工作台 +2. 审核 AI 生成题目 → 通过准确的 / 退回有问题的 +3. 对学生学习成果进行点评 +4. 查看所带学生画像 → 了解学生能力分布 → 针对性指导 +``` + +--- + +## 技术规格摘要 + +| 维度 | 规格 | +|------|------| +| 前端 | Next.js 14 + React 18 + TypeScript + Tailwind CSS | +| 后端 | NestJS 10.4 + TypeScript + TypeORM | +| 数据库 | PostgreSQL(可选,开发环境支持内存模式) | +| AI 引擎 | OpenAI兼容接口(默认通义千问),支持 SSE 流式输出 | +| 认证 | JWT + bcrypt | +| 外部数据源 | PubMed、CNKI、万方、UpToDate、Cochrane | +| 速率保护 | AI调用 20次/分钟、500次/天 | +| 后端模块数 | 21 个业务模块 | + +--- + +## 系统差异化优势 + +| 维度 | 传统教学平台 | 本系统 | +|------|-------------|--------| +| 数据关联 | 各模块数据孤立 | 统一能力图谱贯通所有模块 | +| AI 输出信任 | 缺乏验证机制 | 全链路可信度标注 + 来源溯源 | +| 学习反馈 | 周期性人工评价 | 实时AI形成性评价 + 阶段终结性评价 | +| 职业发展 | 静态指导 | 动态闭环:能力变化→规划自动更新 | +| AI 素养 | 无系统培养 | 四维度协同能力训练+评估 | +| 数据隐私 | 粗放管理 | 知情同意+即时撤销+默认脱敏+全审计 | +| 教学质量 | 缺乏把控 | AI生成+人工审核双层保障 | + +--- + +*本文档描述系统面向用户的功能与价值。技术架构与实现细节参见 `.kiro/specs/college-ai-center/design.md`。* diff --git a/collegeAIcenter.code-workspace b/collegeAIcenter.code-workspace deleted file mode 100644 index 61e510d..0000000 --- a/collegeAIcenter.code-workspace +++ /dev/null @@ -1,9 +0,0 @@ -{ - "folders": [ - { - "path": "." - } - ], - "settings": { - } -} \ No newline at end of file diff --git a/deploy.sh b/deploy.sh new file mode 100755 index 0000000..6e10610 --- /dev/null +++ b/deploy.sh @@ -0,0 +1,164 @@ +#!/usr/bin/env bash +# +# College AI Center — 一键部署脚本 +# +# 用法: +# ./deploy.sh +# +# 作用:把本地代码同步到远程服务器,在服务器上安装依赖、构建前后端、 +# 配置 PostgreSQL 数据库、systemd 服务与 nginx(域名 mc.all8ai.top),并启动。 +# +# 与服务器上已有的 k12 应用互不影响(使用独立端口 3100/4100、独立目录、 +# 名称匹配的 nginx server_name,不占用 default_server)。 +# +# 依赖(本机):sshpass、rsync、ssh +# --------------------------------------------------------------------------- +set -euo pipefail + +# ====== 可配置参数 ====== +SERVER_IP="${SERVER_IP:-101.96.196.218}" +SERVER_USER="${SERVER_USER:-root}" +SERVER_PASS="${SERVER_PASS:-Why_701208}" + +REMOTE_DIR="/opt/college-ai-center" +BACKEND_PORT=3100 +FRONTEND_PORT=4100 + +# 数据库 +DB_NAME="college_ai_center" +DB_USER="cac_user" +DB_PASS="${DB_PASS:-Cac_$(echo "$SERVER_PASS" | md5sum 2>/dev/null | cut -c1-12 || echo Pg88dev)}" + +# 大模型配置(如需启用,部署后在服务器 .env 中填写 LLM_API_KEY) +LLM_API_KEY="${LLM_API_KEY:-}" +LLM_BASE_URL="${LLM_BASE_URL:-https://dashscope.aliyuncs.com/compatible-mode/v1}" +LLM_MODEL="${LLM_MODEL:-qwen-plus}" + +JWT_SECRET="${JWT_SECRET:-cac-prod-$(openssl rand -hex 16 2>/dev/null || echo change-me-in-prod)}" + +SSH_OPTS="-o StrictHostKeyChecking=no -o ConnectTimeout=20" +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +ssh_run() { sshpass -p "$SERVER_PASS" ssh $SSH_OPTS "$SERVER_USER@$SERVER_IP" "$@"; } + +echo "==> [1/6] 检查本机依赖" +for c in sshpass rsync ssh; do + command -v "$c" >/dev/null 2>&1 || { echo "缺少命令:$c,请先安装(mac: brew install $c)"; exit 1; } +done + +echo "==> [2/6] 同步代码到 $SERVER_IP:$REMOTE_DIR" +ssh_run "mkdir -p $REMOTE_DIR" +sshpass -p "$SERVER_PASS" rsync -az --delete -e "ssh $SSH_OPTS" \ + --exclude 'node_modules' \ + --exclude 'web/node_modules' \ + --exclude '.next' \ + --exclude 'web/.next' \ + --exclude 'dist' \ + --exclude '.git' \ + --exclude 'coverage' \ + --exclude '*.log' \ + "$SCRIPT_DIR/" "$SERVER_USER@$SERVER_IP:$REMOTE_DIR/" + +echo "==> [3/6] 在服务器上准备数据库" +ssh_run "sudo -u postgres psql -tc \"SELECT 1 FROM pg_roles WHERE rolname='$DB_USER'\" | grep -q 1 || sudo -u postgres psql -c \"CREATE ROLE $DB_USER LOGIN PASSWORD '$DB_PASS';\"" +ssh_run "sudo -u postgres psql -tc \"SELECT 1 FROM pg_database WHERE datname='$DB_NAME'\" | grep -q 1 || sudo -u postgres psql -c \"CREATE DATABASE $DB_NAME OWNER $DB_USER;\"" +ssh_run "sudo -u postgres psql -c \"ALTER DATABASE $DB_NAME OWNER TO $DB_USER;\" >/dev/null 2>&1 || true" + +echo "==> [4/6] 写入后端 .env" +ssh_run "cat > $REMOTE_DIR/.env < [5/6] 安装依赖并构建(后端 + 前端)" +ssh_run "set -e; cd $REMOTE_DIR && npm install --no-audit --no-fund && npm run build" +ssh_run "set -e; cd $REMOTE_DIR/web && npm install --no-audit --no-fund && BACKEND_URL=http://127.0.0.1:$BACKEND_PORT npm run build" + +echo "==> [6/6] 配置 systemd 服务" + +# 后端 systemd +ssh_run "cat > /etc/systemd/system/cac-backend.service < /etc/systemd/system/cac-frontend.service </dev/null 2>&1 || true" + +# 若 ufw 启用,则放行端口 +ssh_run "if ufw status 2>/dev/null | grep -q 'Status: active'; then ufw allow $BACKEND_PORT/tcp; ufw allow $FRONTEND_PORT/tcp; fi || true" + +echo "==> 启动服务" +ssh_run "systemctl daemon-reload && systemctl enable cac-backend cac-frontend >/dev/null 2>&1 && systemctl restart cac-backend cac-frontend" + +echo "==> 等待服务就绪" +sleep 6 +ssh_run "systemctl is-active cac-backend cac-frontend; echo '--- backend health ---'; curl -s -o /dev/null -w 'backend(api/auth/login probe): %{http_code}\n' -X POST http://127.0.0.1:$BACKEND_PORT/api/auth/login -H 'Content-Type: application/json' -d '{}'; echo '--- frontend ---'; curl -s -o /dev/null -w 'frontend: %{http_code}\n' http://127.0.0.1:$FRONTEND_PORT" + +echo "" +echo "============================================================" +echo " 部署完成!" +echo " 前端访问: http://${SERVER_IP}:$FRONTEND_PORT" +echo " 后端 API: http://${SERVER_IP}:$BACKEND_PORT/api" +echo " 数据库 : $DB_NAME (用户 $DB_USER)" +echo "" +echo " 测试账户:student / mentor / admin(密码同 *123,如 student123)" +echo "" +echo " 常用运维:" +echo " systemctl status cac-backend cac-frontend" +echo " journalctl -u cac-backend -f" +echo " journalctl -u cac-frontend -f" +echo "============================================================" diff --git a/scripts/gen-pptx.js b/scripts/gen-pptx.js new file mode 100644 index 0000000..272f151 --- /dev/null +++ b/scripts/gen-pptx.js @@ -0,0 +1,1395 @@ +/** + * 生成「医科高校智能学习中心」汇报 PPT + * 面向高校管理者与广电领导,展现学生/学校/广电三方价值 + * 文案刻意减少"AI赋能/智能化"等流行词,改用具体教育场景与管理动作 + */ +const pptxgen = require('pptxgenjs'); + +const pres = new pptxgen(); +pres.layout = 'LAYOUT_WIDE'; // 13.33 x 7.5 英寸 +pres.author = 'College AI Center'; +pres.title = '医科高校智能学习中心'; + +// ===== 主题色 ===== +const C = { + ink: '1B3A5B', // 深藏青(主色/标题) + steel: '2C5F8A', // 钢青(副标题/线条) + edu: '2A7B7B', // 教育青(学生/学校) + gold: 'C8923D', // 琥珀金(广电/亮点) + rose: 'B5483D', // 警示红(挑战/差异) + bg: 'FFFFFF', + bgSoft: 'F4F6F8', + bgPanel: 'EDF1F4', + text: '2D3748', + textMid: '5A6A7A', + textLight: '8A98A6', + white: 'FFFFFF', + divider: 'D8DEE4', +}; + +// ===== 辅助函数 ===== +const W = 13.33, H = 7.5; + +function addSlide(bg = C.bg) { + const s = pres.addSlide(); + s.background = { color: bg }; + return s; +} + +// 页面顶部色条 + 章节标签 + 主标题 +function header(s, section, title, accent = C.ink) { + s.addShape('rect', { x: 0, y: 0, w: W, h: 0.12, fill: { color: accent } }); + s.addText(section, { + x: 0.6, y: 0.32, w: 12, h: 0.35, + fontSize: 12, color: C.textLight, fontFace: 'Microsoft YaHei', charSpacing: 2, + }); + s.addText(title, { + x: 0.6, y: 0.62, w: 12, h: 0.7, + fontSize: 26, bold: true, color: C.ink, fontFace: 'Microsoft YaHei', + }); + s.addShape('rect', { x: 0.62, y: 1.32, w: 0.5, h: 0.05, fill: { color: accent } }); +} + +// 底部页码 +function footer(s, n) { + s.addText(`${n}`, { + x: W - 1.0, y: H - 0.45, w: 0.8, h: 0.3, + fontSize: 10, color: C.textLight, align: 'right', fontFace: 'Microsoft YaHei', + }); + s.addText('医科高校智能学习中心', { + x: 0.6, y: H - 0.45, w: 6, h: 0.3, + fontSize: 10, color: C.textLight, fontFace: 'Microsoft YaHei', + }); +} + +// 卡片:标题 + 正文(用于功能要点) +function card(s, x, y, w, h, title, lines, opts = {}) { + const fill = opts.fill || C.bgSoft; + const bar = opts.bar || C.edu; + s.addShape('roundRect', { + x, y, w, h, fill: { color: fill }, + line: { color: C.divider, width: 0.5 }, rectRadius: 0.08, + }); + s.addShape('rect', { x: x + 0.18, y: y + 0.22, w: 0.06, h: h - 0.44, fill: { color: bar } }); + s.addText(title, { + x: x + 0.36, y: y + 0.18, w: w - 0.5, h: 0.4, + fontSize: 14, bold: true, color: C.ink, fontFace: 'Microsoft YaHei', + }); + const body = Array.isArray(lines) ? lines.map(t => ({ text: t, options: { bullet: { code: '2022' }, indentLevel: 0 } })) : lines; + s.addText(body, { + x: x + 0.36, y: y + 0.62, w: w - 0.55, h: h - 0.8, + fontSize: 11.5, color: C.text, fontFace: 'Microsoft YaHei', + lineSpacingMultiple: 1.25, valign: 'top', + }); +} + +// 大数字 + 标签(用于指标卡) +function metric(s, x, y, w, num, label, accent = C.edu) { + s.addShape('roundRect', { + x, y, w, h: 1.5, fill: { color: C.bgSoft }, + line: { color: C.divider, width: 0.5 }, rectRadius: 0.08, + }); + s.addText(num, { + x, y: y + 0.18, w, h: 0.7, + fontSize: 34, bold: true, color: accent, align: 'center', fontFace: 'Microsoft YaHei', + }); + s.addText(label, { + x, y: y + 0.95, w, h: 0.4, + fontSize: 12, color: C.textMid, align: 'center', fontFace: 'Microsoft YaHei', + }); +} + +// 三色角色徽标 +function roleBadge(s, x, y, text, color) { + s.addShape('roundRect', { + x, y, w: 1.6, h: 0.4, fill: { color: color }, rectRadius: 0.06, + }); + s.addText(text, { + x, y: y + 0.02, w: 1.6, h: 0.36, + fontSize: 12, bold: true, color: C.white, align: 'center', fontFace: 'Microsoft YaHei', + }); +} + +// ============================================================ +// 第 1 页 · 封面 +// ============================================================ +{ + const s = addSlide(C.ink); + // 装饰几何 + s.addShape('rect', { x: 0, y: 0, w: 0.35, h: H, fill: { color: C.gold } }); + s.addShape('rect', { x: 0.35, y: 0, w: 0.08, h: H, fill: { color: C.edu } }); + s.addShape('rect', { x: 0, y: H - 1.6, w: W, h: 0.02, fill: { color: C.steel } }); + + s.addText('医科高校', { + x: 1.0, y: 2.0, w: 11, h: 1.0, + fontSize: 52, bold: true, color: C.white, fontFace: 'Microsoft YaHei', + }); + s.addText('智能学习中心', { + x: 1.0, y: 2.85, w: 11, h: 1.0, + fontSize: 52, bold: true, color: C.white, fontFace: 'Microsoft YaHei', + }); + s.addText('以能力为核心 · 贯穿医学人才培养全链路', { + x: 1.0, y: 4.0, w: 11, h: 0.6, + fontSize: 20, color: C.gold, fontFace: 'Microsoft YaHei', + }); + s.addText('面向高校管理者与广电合作伙伴 · 汇报材料', { + x: 1.0, y: 5.9, w: 11, h: 0.4, + fontSize: 14, color: 'AEBFD0', fontFace: 'Microsoft YaHei', + }); + s.addText('基础医学 → 临床医学 → 见习实习 → 住院医师培养', { + x: 1.0, y: 6.4, w: 11, h: 0.4, + fontSize: 13, color: '8FA3B8', fontFace: 'Microsoft YaHei', charSpacing: 1, + }); +} + +// ============================================================ +// 第 2 页 · 目录 +// ============================================================ +{ + const s = addSlide(); + header(s, 'CONTENTS', '目录'); + const items = [ + ['01', '医学教育的现实挑战', C.rose], + ['02', '系统定位与三方共建', C.ink], + ['03', '学生价值:贯穿成长链的学习与训练', C.edu], + ['04', '学校价值:人才培养的数据底座', C.edu], + ['05', '广电价值:算力支撑与数据通道', C.gold], + ['06', '设计底线与差异化优势', C.steel], + ]; + items.forEach((it, i) => { + const y = 1.7 + i * 0.85; + s.addText(it[0], { + x: 0.9, y, w: 1.0, h: 0.6, + fontSize: 30, bold: true, color: it[2], fontFace: 'Microsoft YaHei', + }); + s.addShape('rect', { x: 2.0, y: y + 0.28, w: 0.4, h: 0.03, fill: { color: C.divider } }); + s.addText(it[1], { + x: 2.6, y: y + 0.08, w: 9.5, h: 0.5, + fontSize: 18, color: C.text, fontFace: 'Microsoft YaHei', + }); + }); + footer(s, 2); +} + +// ============================================================ +// 第 3 页 · 第一部分:医学教育的现实挑战 +// ============================================================ +{ + const s = addSlide(); + header(s, 'PART 01 · 现实挑战', '医学教育正在面对的四个痛点', C.rose); + const pains = [ + ['成长数据分散', '课程、作业、见习、实习、考核、科研各自为政,\n学生四年下来的成长轨迹难以拼成完整图景。'], + ['过程性反馈缺失', '阶段性评价周期长,学生做完一次见习、写完\n一份报告,往往要等数周才得到老师反馈。'], + ['临床训练机会有限', '真实病例和标准化病人接触不足,问诊沟通、\n临床决策这些核心能力很难反复练习。'], + ['培养质量难量化', '缺乏全维度、可追溯的能力证据,就业推荐\n与住培选拔大多依赖期末成绩和主观印象。'], + ]; + pains.forEach((p, i) => { + const x = 0.6 + (i % 2) * 6.1; + const y = 1.7 + Math.floor(i / 2) * 2.5; + s.addShape('roundRect', { + x, y, w: 5.9, h: 2.25, fill: { color: C.bgSoft }, + line: { color: C.divider, width: 0.5 }, rectRadius: 0.1, + }); + s.addShape('rect', { x: x + 0.25, y: y + 0.25, w: 0.08, h: 1.75, fill: { color: C.rose } }); + s.addText(p[0], { + x: x + 0.5, y: y + 0.22, w: 5.2, h: 0.45, + fontSize: 17, bold: true, color: C.ink, fontFace: 'Microsoft YaHei', + }); + s.addText(p[1], { + x: x + 0.5, y: y + 0.78, w: 5.2, h: 1.3, + fontSize: 12.5, color: C.textMid, fontFace: 'Microsoft YaHei', + lineSpacingMultiple: 1.3, valign: 'top', + }); + }); + footer(s, 3); +} + +// ============================================================ +// 第 4 页 · 第二部分:系统定位 +// ============================================================ +{ + const s = addSlide(); + header(s, 'PART 02 · 系统定位', '一个学习中心,贯穿培养全链路', C.ink); + + s.addShape('roundRect', { + x: 0.6, y: 1.7, w: 12.1, h: 1.6, fill: { color: C.bgPanel }, + line: { color: C.divider, width: 0.5 }, rectRadius: 0.1, + }); + s.addText('面向医学院校的学习中心平台。以"能力"为核心,把学生在校的所有学习活动——课程、对练、科研、协作——统一汇入能力图谱,形成数据贯通的闭环成长体系。', { + x: 0.9, y: 1.9, w: 11.5, h: 1.2, + fontSize: 15, color: C.text, fontFace: 'Microsoft YaHei', + lineSpacingMultiple: 1.4, valign: 'middle', + }); + + // 成长链路 + const chain = ['基础医学', '临床医学', '见习实习', '住院医师培养']; + chain.forEach((t, i) => { + const x = 1.2 + i * 2.9; + s.addShape('roundRect', { + x, y: 3.7, w: 2.5, h: 0.7, fill: { color: C.edu }, rectRadius: 0.08, + }); + s.addText(t, { + x, y: 3.74, w: 2.5, h: 0.62, + fontSize: 15, bold: true, color: C.white, align: 'center', fontFace: 'Microsoft YaHei', + }); + if (i < 3) { + s.addText('→', { + x: x + 2.5, y: 3.7, w: 0.4, h: 0.7, + fontSize: 20, color: C.steel, align: 'center', valign: 'middle', fontFace: 'Microsoft YaHei', + }); + } + }); + + s.addText('统一能力图谱贯穿四个阶段,所有学习行为映射到同一套能力标签', { + x: 0.6, y: 4.7, w: 12, h: 0.4, + fontSize: 12.5, color: C.textMid, align: 'center', fontFace: 'Microsoft YaHei', + }); + + // 三方共建 + const parts = [ + [C.edu, '学生', '贯穿成长链的\n学习与训练'], + [C.edu, '学校', '全景式人才\n培养数据底座'], + [C.gold, '广电', '算力与数据\n通道支撑'], + ]; + parts.forEach((p, i) => { + const x = 1.5 + i * 3.6; + s.addShape('roundRect', { + x, y: 5.4, w: 3.2, h: 1.5, fill: { color: C.white }, + line: { color: p[0], width: 1.5 }, rectRadius: 0.1, + }); + s.addText(p[1], { + x, y: 5.55, w: 3.2, h: 0.5, + fontSize: 18, bold: true, color: p[0], align: 'center', fontFace: 'Microsoft YaHei', + }); + s.addText(p[2], { + x, y: 6.05, w: 3.2, h: 0.8, + fontSize: 12.5, color: C.textMid, align: 'center', fontFace: 'Microsoft YaHei', + lineSpacingMultiple: 1.2, + }); + }); + footer(s, 4); +} + +// ============================================================ +// 第 5 页 · 三方价值总览 +// ============================================================ +{ + const s = addSlide(); + header(s, 'PART 02 · 价值总览', '三方共建,各取所需', C.ink); + + const cols = [ + [C.edu, '学生', '通过多种交互提升能力', [ + '学习空间:成长记录一站式汇聚', + '对练训练:课程+临床双场景模拟', + '能力画像:六维量化,成长可视', + '职业规划:目标导向,动态闭环', + '科研辅助:多源检索,证据分级', + '协同素养:负责任使用工具的能力', + ]], + [C.edu, '学校', '了解学生全面情况', [ + '导师端:题目审核+成果点评+带教画像', + '管理端:技能治理+权限+审计+合规', + '能力图谱:打破数据孤岛,全量贯通', + '培养质量:可量化、可追溯、可比较', + '教学质量:系统生成+人工审核双层保障', + '决策依据:基于行为数据而非主观印象', + ]], + [C.gold, '广电', '提供算力与数据支持', [ + '算力支撑:大模型推理算力供给', + '流式输出:支撑对话对练实时交互', + '数据通道:多源医学文献接入', + '社会价值:算力转化为人才培养质量', + '可扩展:技能库持续扩展,需求增长', + '品牌效益:广电算力赋能医学教育', + ]], + ]; + cols.forEach((c, i) => { + const x = 0.6 + i * 4.1; + s.addShape('roundRect', { + x, y: 1.7, w: 3.9, h: 5.3, fill: { color: C.bgSoft }, + line: { color: c[0], width: 1.2 }, rectRadius: 0.1, + }); + s.addShape('rect', { x, y: 1.7, w: 3.9, h: 0.7, fill: { color: c[0] } }); + s.addText(c[1], { + x, y: 1.78, w: 3.9, h: 0.55, + fontSize: 20, bold: true, color: C.white, align: 'center', fontFace: 'Microsoft YaHei', + }); + s.addText(c[2], { + x: x + 0.2, y: 2.55, w: 3.5, h: 0.4, + fontSize: 12.5, color: c[0], align: 'center', fontFace: 'Microsoft YaHei', italic: true, + }); + const items = c[3].map(t => ({ text: t, options: { bullet: { code: '2022' } } })); + s.addText(items, { + x: x + 0.3, y: 3.05, w: 3.5, h: 3.8, + fontSize: 12, color: C.text, fontFace: 'Microsoft YaHei', + lineSpacingMultiple: 1.35, valign: 'top', + }); + }); + footer(s, 5); +} + +// ============================================================ +// 第 6 页 · 第三部分章节页:学生价值 +// ============================================================ +{ + const s = addSlide(C.edu); + s.addShape('rect', { x: 0, y: 0, w: 0.35, h: H, fill: { color: C.white } }); + s.addText('PART 03', { + x: 1.0, y: 2.4, w: 11, h: 0.6, + fontSize: 18, color: 'BFE3E3', fontFace: 'Microsoft YaHei', charSpacing: 4, + }); + s.addText('学生价值', { + x: 1.0, y: 2.9, w: 11, h: 1.0, + fontSize: 44, bold: true, color: C.white, fontFace: 'Microsoft YaHei', + }); + s.addText('贯穿成长链的学习与训练 · 六个支撑', { + x: 1.0, y: 4.0, w: 11, h: 0.6, + fontSize: 18, color: 'D9EFEF', fontFace: 'Microsoft YaHei', + }); +} + +// ============================================================ +// 第 7 页 · 学生价值总览 +// ============================================================ +{ + const s = addSlide(); + header(s, 'PART 03 · 学生价值', '六个支撑,贯穿在校全程', C.edu); + const items = [ + ['学习空间', '成长记录一站式汇聚,\n9种成果类型自动归档'], + ['对练训练', '课程对练+临床对话,\n模拟考场与诊室'], + ['能力画像', '六维量化评分,\n成长趋势可视'], + ['职业规划', '目标导向,差距分析,\n动态闭环更新'], + ['科研辅助', '多源文献检索,\n证据分级与引用管理'], + ['协同素养', '负责任使用工具,\n四维度系统培养'], + ]; + items.forEach((it, i) => { + const x = 0.6 + (i % 3) * 4.1; + const y = 1.7 + Math.floor(i / 3) * 2.5; + s.addShape('roundRect', { + x, y, w: 3.9, h: 2.25, fill: { color: C.bgSoft }, + line: { color: C.divider, width: 0.5 }, rectRadius: 0.1, + }); + s.addShape('rect', { x: x + 0.25, y: y + 0.25, w: 0.08, h: 1.75, fill: { color: C.edu } }); + s.addText(it[0], { + x: x + 0.5, y: y + 0.25, w: 3.2, h: 0.45, + fontSize: 17, bold: true, color: C.ink, fontFace: 'Microsoft YaHei', + }); + s.addText(it[1], { + x: x + 0.5, y: y + 0.8, w: 3.2, h: 1.2, + fontSize: 12.5, color: C.textMid, fontFace: 'Microsoft YaHei', + lineSpacingMultiple: 1.3, valign: 'top', + }); + }); + footer(s, 7); +} + +// ============================================================ +// 第 8 页 · 学习空间 +// ============================================================ +{ + const s = addSlide(); + header(s, 'PART 03 · 学生价值 01', '学习空间:让成长有迹可循', C.edu); + + // 左:9种成果类型 + s.addText('九种成果类型,覆盖在校全程', { + x: 0.6, y: 1.6, w: 6, h: 0.4, + fontSize: 15, bold: true, color: C.ink, fontFace: 'Microsoft YaHei', + }); + const types = ['课程记录', '作业', '实验报告', '临床见习', '临床实习', 'OSCE考核', '文献阅读', '科研成果', '证书/资格备考']; + types.forEach((t, i) => { + const x = 0.6 + (i % 3) * 2.05; + const y = 2.1 + Math.floor(i / 3) * 0.65; + s.addShape('roundRect', { + x, y, w: 1.9, h: 0.5, fill: { color: C.white }, + line: { color: C.edu, width: 1 }, rectRadius: 0.06, + }); + s.addText(t, { + x, y: y + 0.02, w: 1.9, h: 0.46, + fontSize: 12, color: C.edu, align: 'center', fontFace: 'Microsoft YaHei', + }); + }); + + // 右:三个特性 + s.addText('三个特性', { + x: 7.2, y: 1.6, w: 5.5, h: 0.4, + fontSize: 15, bold: true, color: C.ink, fontFace: 'Microsoft YaHei', + }); + const feats = [ + ['三维自动归档', '按学年、学期、临床轮转科室\n三个维度自动归类'], + ['能力标签映射', '每条成果自动映射到\n能力图谱中的能力标签'], + ['里程碑标记', '标记阶段性成长节点,\n关联该阶段内全部成果'], + ]; + feats.forEach((f, i) => { + const y = 2.1 + i * 1.4; + s.addShape('roundRect', { + x: 7.2, y, w: 5.5, h: 1.25, fill: { color: C.bgSoft }, + line: { color: C.divider, width: 0.5 }, rectRadius: 0.08, + }); + s.addText(f[0], { + x: 7.4, y: y + 0.15, w: 5.1, h: 0.4, + fontSize: 13.5, bold: true, color: C.edu, fontFace: 'Microsoft YaHei', + }); + s.addText(f[1], { + x: 7.4, y: y + 0.55, w: 5.1, h: 0.6, + fontSize: 11.5, color: C.textMid, fontFace: 'Microsoft YaHei', + lineSpacingMultiple: 1.2, + }); + }); + + // 底部价值条 + s.addShape('roundRect', { + x: 0.6, y: 6.3, w: 12.1, h: 0.7, fill: { color: C.edu }, rectRadius: 0.08, + }); + s.addText('价值:告别分散记录,学生四年成长轨迹在一个空间里完整呈现,为画像与职业规划提供数据基础', { + x: 0.8, y: 6.35, w: 11.7, h: 0.6, + fontSize: 12.5, color: C.white, align: 'center', valign: 'middle', fontFace: 'Microsoft YaHei', + }); + footer(s, 8); +} + +// ============================================================ +// 第 9 页 · 对练训练——课程对练 +// ============================================================ +{ + const s = addSlide(); + header(s, 'PART 03 · 学生价值 02', '对练训练(一):把考场搬进系统', C.edu); + + // 流程链 + const flow = ['系统出题', '导师审核', '学生对练', '诊断报告', '间隔重复']; + flow.forEach((t, i) => { + const x = 0.8 + i * 2.45; + s.addShape('roundRect', { + x, y: 1.7, w: 2.1, h: 0.65, fill: { color: i === 1 ? C.gold : C.edu }, rectRadius: 0.08, + }); + s.addText(t, { + x, y: 1.74, w: 2.1, h: 0.57, + fontSize: 13, bold: true, color: C.white, align: 'center', fontFace: 'Microsoft YaHei', + }); + if (i < 4) { + s.addText('→', { + x: x + 2.1, y: 1.7, w: 0.35, h: 0.65, + fontSize: 18, color: C.steel, align: 'center', valign: 'middle', + }); + } + }); + s.addText('导师审核环节用金色标出——题目必须经导师通过后才可使用,退回需注明原因', { + x: 0.6, y: 2.5, w: 12, h: 0.35, + fontSize: 11.5, color: C.textMid, align: 'center', fontFace: 'Microsoft YaHei', italic: true, + }); + + // 四个要点 + const pts = [ + ['题型覆盖', 'A1-A4型选择题、病例分析题、\n临床决策题,贴近真实考试'], + ['权威绑定', '每题绑定权威知识源\n(教材/指南/官方题库)'], + ['限时作答', '逐题呈现,120秒/题,\n即时判定,模拟真实考场'], + ['错题复现', 'SM-2算法优先复现错题\n与即将遗忘的题'], + ]; + pts.forEach((p, i) => { + const x = 0.6 + (i % 2) * 6.1; + const y = 3.1 + Math.floor(i / 2) * 1.5; + card(s, x, y, 5.9, 1.35, p[0], [p[1]], { bar: C.edu }); + }); + + // 诊断报告说明 + s.addShape('roundRect', { + x: 0.6, y: 6.2, w: 12.1, h: 0.8, fill: { color: C.bgPanel }, rectRadius: 0.08, + }); + s.addText('诊断报告:正确率、用时、薄弱环节分析(低于60%的能力标签自动标记),并针对每个薄弱环节生成具体改进建议', { + x: 0.8, y: 6.3, w: 11.7, h: 0.6, + fontSize: 12.5, color: C.text, align: 'center', valign: 'middle', fontFace: 'Microsoft YaHei', + }); + footer(s, 9); +} + +// ============================================================ +// 第 10 页 · 对练训练——临床情景对话 +// ============================================================ +{ + const s = addSlide(); + header(s, 'PART 03 · 学生价值 02', '对练训练(二):把诊室搬进系统', C.edu); + + // 左:对话场景示意 + s.addShape('roundRect', { + x: 0.6, y: 1.7, w: 5.8, h: 5.1, fill: { color: C.bgSoft }, + line: { color: C.divider, width: 0.5 }, rectRadius: 0.1, + }); + s.addText('模拟问诊对话', { + x: 0.8, y: 1.85, w: 5.4, h: 0.4, + fontSize: 15, bold: true, color: C.ink, fontFace: 'Microsoft YaHei', + }); + const dialog = [ + ['学生', '您哪里不舒服?', C.edu], + ['模拟患者', '胸口闷,活动后加重,有三四天了。', C.textMid], + ['学生', '有没有放射痛?伴随出汗吗?', C.edu], + ['模拟患者', '偶尔往左肩串,出汗不明显。', C.textMid], + ['学生', '既往有高血压、糖尿病吗?', C.edu], + ['模拟患者', '高血压五年,没规律吃药。', C.textMid], + ]; + dialog.forEach((d, i) => { + const y = 2.4 + i * 0.6; + s.addText(`${d[0]}:`, { + x: 0.9, y, w: 0.8, h: 0.4, + fontSize: 11.5, bold: true, color: d[2], fontFace: 'Microsoft YaHei', + }); + s.addText(d[1], { + x: 1.7, y, w: 4.5, h: 0.4, + fontSize: 11.5, color: C.text, fontFace: 'Microsoft YaHei', + }); + }); + s.addText('· 最多50轮对话 · 携带最近10轮历史保持连贯 · 角色扮演保持情景一致', { + x: 0.8, y: 6.1, w: 5.4, h: 0.5, + fontSize: 10.5, color: C.textLight, fontFace: 'Microsoft YaHei', + }); + + // 右:三维评估 + s.addText('三维评估,量化临床能力', { + x: 6.7, y: 1.7, w: 6, h: 0.4, + fontSize: 15, bold: true, color: C.ink, fontFace: 'Microsoft YaHei', + }); + const evals = [ + ['问诊完整性', '主诉、现病史、既往史、\n个人史是否覆盖到位'], + ['临床推理', '鉴别诊断思路是否清晰,\n假设是否合理推进'], + ['医患沟通', '提问方式、同理心、\n信息传达是否有效'], + ]; + evals.forEach((e, i) => { + const y = 2.2 + i * 1.5; + s.addShape('roundRect', { + x: 6.7, y, w: 6, h: 1.35, fill: { color: C.white }, + line: { color: C.edu, width: 1 }, rectRadius: 0.08, + }); + s.addShape('rect', { x: 6.7, y, w: 0.1, h: 1.35, fill: { color: C.edu } }); + s.addText(e[0], { + x: 6.95, y: y + 0.18, w: 5.6, h: 0.4, + fontSize: 14, bold: true, color: C.edu, fontFace: 'Microsoft YaHei', + }); + s.addText(e[1], { + x: 6.95, y: y + 0.6, w: 5.6, h: 0.65, + fontSize: 12, color: C.textMid, fontFace: 'Microsoft YaHei', + lineSpacingMultiple: 1.25, + }); + }); + footer(s, 10); +} + +// ============================================================ +// 第 11 页 · 能力画像 +// ============================================================ +{ + const s = addSlide(); + header(s, 'PART 03 · 学生价值 03', '能力画像:六维量化,看见自己', C.edu); + + // 六维雷达示意(用六边形+标签模拟) + const cx = 3.5, cy = 4.2; + const dims = [ + ['知识掌握', 0, -1.6], + ['临床技能', 1.4, -0.8], + ['科研能力', 1.4, 0.8], + ['人文素养', 0, 1.6], + ['协同素养', -1.4, 0.8], + ['职业倾向', -1.4, -0.8], + ]; + // 外框六边形 + s.addShape('hexagon', { + x: cx - 1.8, y: cy - 1.8, w: 3.6, h: 3.6, + fill: { color: C.bgSoft }, line: { color: C.edu, width: 1.5 }, + }); + // 内层六边形(数据示意) + s.addShape('hexagon', { + x: cx - 1.1, y: cy - 1.1, w: 2.2, h: 2.2, + fill: { color: C.edu, transparency: 75 }, line: { color: C.edu, width: 1 }, + }); + dims.forEach((d) => { + s.addText(d[0], { + x: cx + d[1] - 0.9, y: cy + d[2] - 0.2, w: 1.8, h: 0.4, + fontSize: 11.5, bold: true, color: C.ink, align: 'center', fontFace: 'Microsoft YaHei', + }); + }); + s.addText('六维能力雷达(示意)', { + x: 0.6, y: 6.2, w: 5.8, h: 0.35, + fontSize: 11, color: C.textLight, align: 'center', fontFace: 'Microsoft YaHei', + }); + + // 右:特性 + s.addText('画像的四个特性', { + x: 7.0, y: 1.7, w: 5.7, h: 0.4, + fontSize: 15, bold: true, color: C.ink, fontFace: 'Microsoft YaHei', + }); + const feats = [ + ['每维0-100分', '六维度量化评分,\n分布一目了然'], + ['数据可溯源', '每项结论可追溯到\n具体学习成果或对练记录'], + ['动态更新', '能力图谱数据更新后\n自动重算,无需手动维护'], + ['成长趋势', '能力维度时间序列,\n可视化成长曲线'], + ]; + feats.forEach((f, i) => { + const y = 2.2 + i * 1.15; + s.addShape('roundRect', { + x: 7.0, y, w: 5.7, h: 1.0, fill: { color: C.bgSoft }, + line: { color: C.divider, width: 0.5 }, rectRadius: 0.08, + }); + s.addText(f[0], { + x: 7.2, y: y + 0.12, w: 5.3, h: 0.35, + fontSize: 13, bold: true, color: C.edu, fontFace: 'Microsoft YaHei', + }); + s.addText(f[1], { + x: 7.2, y: y + 0.48, w: 5.3, h: 0.45, + fontSize: 11.5, color: C.textMid, fontFace: 'Microsoft YaHei', + lineSpacingMultiple: 1.2, + }); + }); + footer(s, 11); +} + +// ============================================================ +// 第 12 页 · 职业规划 +// ============================================================ +{ + const s = addSlide(); + header(s, 'PART 03 · 学生价值 04', '职业规划:目标导向,动态闭环', C.edu); + + // 闭环流程 + const flow = [ + ['设定职业目标', '选择或自定义\n目标岗位'], + ['匹配胜任力模型', '参照权威医学框架\n四大胜任力域'], + ['能力差距分析', '逐项对照当前水平\n与岗位要求'], + ['生成发展规划', '每个差距项提供\n行动与目标水平'], + ]; + flow.forEach((f, i) => { + const x = 0.7 + i * 3.05; + s.addShape('roundRect', { + x, y: 1.8, w: 2.8, h: 1.3, fill: { color: C.edu }, rectRadius: 0.08, + }); + s.addText(f[0], { + x, y: 1.9, w: 2.8, h: 0.4, + fontSize: 13, bold: true, color: C.white, align: 'center', fontFace: 'Microsoft YaHei', + }); + s.addText(f[1], { + x: x + 0.15, y: 2.35, w: 2.5, h: 0.65, + fontSize: 11, color: 'D9EFEF', align: 'center', fontFace: 'Microsoft YaHei', + lineSpacingMultiple: 1.2, + }); + if (i < 3) { + s.addText('→', { + x: x + 2.8, y: 1.8, w: 0.25, h: 1.3, + fontSize: 18, color: C.steel, align: 'center', valign: 'middle', + }); + } + }); + + // 闭环回箭头说明 + s.addShape('roundRect', { + x: 0.7, y: 3.4, w: 11.9, h: 0.7, fill: { color: C.bgPanel }, rectRadius: 0.08, + }); + s.addText('动态闭环:能力提升后 → 画像自动更新 → 差距重算 → 发展规划自动调整,无需学生手动维护', { + x: 0.9, y: 3.48, w: 11.5, h: 0.55, + fontSize: 12.5, color: C.edu, align: 'center', valign: 'middle', bold: true, fontFace: 'Microsoft YaHei', + }); + + // 胜任力模型四域 + s.addText('岗位胜任力模型 · 参照权威医学框架', { + x: 0.6, y: 4.4, w: 12, h: 0.4, + fontSize: 14, bold: true, color: C.ink, fontFace: 'Microsoft YaHei', + }); + const domains = [ + ['科学与学术', '基础医学知识、\n循证医学能力'], + ['临床能力', '病史采集、临床推理、\n操作技能'], + ['健康与社会', '公共卫生、预防医学、\n社会因素认知'], + ['职业素养', '伦理、沟通、\n团队协作、自我提升'], + ]; + domains.forEach((d, i) => { + const x = 0.6 + i * 3.05; + s.addShape('roundRect', { + x, y: 4.9, w: 2.8, h: 1.5, fill: { color: C.bgSoft }, + line: { color: C.edu, width: 1 }, rectRadius: 0.08, + }); + s.addText(d[0], { + x, y: 5.05, w: 2.8, h: 0.4, + fontSize: 13, bold: true, color: C.edu, align: 'center', fontFace: 'Microsoft YaHei', + }); + s.addText(d[1], { + x: x + 0.15, y: 5.5, w: 2.5, h: 0.8, + fontSize: 11, color: C.textMid, align: 'center', fontFace: 'Microsoft YaHei', + lineSpacingMultiple: 1.2, + }); + }); + s.addText('资源推荐:为每个差距项推荐对应的学习资源或对练任务,直接可执行', { + x: 0.6, y: 6.6, w: 12, h: 0.35, + fontSize: 11.5, color: C.textMid, fontFace: 'Microsoft YaHei', italic: true, + }); + footer(s, 12); +} + +// ============================================================ +// 第 13 页 · 科研辅助 +// ============================================================ +{ + const s = addSlide(); + header(s, 'PART 03 · 学生价值 05', '科研辅助:多源检索,证据分级', C.edu); + + // 流程链 + const flow = ['自然语言描述', 'PICO/MeSH检索式', '多源检索', '证据分级总结', '引用导出']; + flow.forEach((t, i) => { + const x = 0.6 + i * 2.5; + s.addShape('roundRect', { + x, y: 1.7, w: 2.3, h: 0.65, fill: { color: C.edu }, rectRadius: 0.08, + }); + s.addText(t, { + x, y: 1.74, w: 2.3, h: 0.57, + fontSize: 11.5, bold: true, color: C.white, align: 'center', fontFace: 'Microsoft YaHei', + }); + if (i < 4) { + s.addText('→', { + x: x + 2.3, y: 1.7, w: 0.2, h: 0.65, + fontSize: 16, color: C.steel, align: 'center', valign: 'middle', + }); + } + }); + + // 数据源 + s.addText('接入的医学数据源', { + x: 0.6, y: 2.7, w: 6, h: 0.4, + fontSize: 14, bold: true, color: C.ink, fontFace: 'Microsoft YaHei', + }); + const sources = ['PubMed', 'CNKI 中国知网', '万方数据', 'UpToDate', 'Cochrane']; + sources.forEach((t, i) => { + const x = 0.6 + (i % 3) * 2.05; + const y = 3.15 + Math.floor(i / 3) * 0.65; + s.addShape('roundRect', { + x, y, w: 1.9, h: 0.5, fill: { color: C.white }, + line: { color: C.edu, width: 1 }, rectRadius: 0.06, + }); + s.addText(t, { + x, y: y + 0.02, w: 1.9, h: 0.46, + fontSize: 12, color: C.edu, align: 'center', fontFace: 'Microsoft YaHei', + }); + }); + + // 右:特性 + const feats = [ + ['证据分级', '对纳入总结的每条资料\n标注证据强度'], + ['带引用总结', '生成带引用的结论,\n每条可追溯来源'], + ['引用管理', 'Vancouver / GB-T 7714 格式,\n导出 EndNote / NoteExpress'], + ['版权安全', '仅提供摘要与链接,\n不存储或分发受版权全文'], + ]; + feats.forEach((f, i) => { + const x = 6.8 + (i % 2) * 3.0; + const y = 2.7 + Math.floor(i / 2) * 1.7; + card(s, x, y, 2.85, 1.55, f[0], [f[1]], { bar: C.edu }); + }); + + // 底部价值 + s.addShape('roundRect', { + x: 0.6, y: 6.3, w: 12.1, h: 0.7, fill: { color: C.edu }, rectRadius: 0.08, + }); + s.addText('价值:把文献检索从"大海捞针"变成"精准取证",支撑课题研究与论文写作,同时培养循证思维', { + x: 0.8, y: 6.35, w: 11.7, h: 0.6, + fontSize: 12.5, color: C.white, align: 'center', valign: 'middle', fontFace: 'Microsoft YaHei', + }); + footer(s, 13); +} + +// ============================================================ +// 第 14 页 · 协同素养 +// ============================================================ +{ + const s = addSlide(); + header(s, 'PART 03 · 学生价值 06', '协同素养:负责任地使用工具', C.edu); + + s.addShape('roundRect', { + x: 0.6, y: 1.6, w: 12.1, h: 0.9, fill: { color: C.bgPanel }, rectRadius: 0.08, + }); + s.addText('在医学领域,能否负责任地使用智能工具本身就是一种核心能力。系统从四个维度系统培养,并把结果计入能力画像与岗位胜任力对照。', { + x: 0.8, y: 1.7, w: 11.7, h: 0.7, + fontSize: 13, color: C.text, align: 'center', valign: 'middle', fontFace: 'Microsoft YaHei', + lineSpacingMultiple: 1.3, + }); + + const dims = [ + ['提问能力', '能否向工具提出\n精准、有效的问题'], + ['批判性验证', '能否对工具输出进行\n来源核验与批判性思考'], + ['责任边界意识', '是否清楚工具辅助\n与人工决策的边界'], + ['协同工作流', '能否有效组织\n人机协作流程'], + ]; + dims.forEach((d, i) => { + const x = 0.6 + i * 3.05; + s.addShape('roundRect', { + x, y: 2.8, w: 2.8, h: 2.0, fill: { color: C.bgSoft }, + line: { color: C.edu, width: 1.2 }, rectRadius: 0.1, + }); + s.addShape('rect', { x, y: 2.8, w: 2.8, h: 0.55, fill: { color: C.edu } }); + s.addText(d[0], { + x, y: 2.87, w: 2.8, h: 0.45, + fontSize: 14, bold: true, color: C.white, align: 'center', fontFace: 'Microsoft YaHei', + }); + s.addText(d[1], { + x: x + 0.2, y: 3.5, w: 2.4, h: 1.1, + fontSize: 12, color: C.textMid, align: 'center', fontFace: 'Microsoft YaHei', + lineSpacingMultiple: 1.3, valign: 'middle', + }); + }); + + // 机制说明 + const mechs = [ + '每个维度都有真实任务载体的训练任务,不是空讲理论', + '直接采用未核验输出时,系统会要求来源核验', + '评估结果计入能力画像与岗位胜任力对照', + '支持导师对协同质量进行点评', + ]; + mechs.forEach((m, i) => { + const y = 5.1 + i * 0.5; + s.addText('·', { x: 0.8, y, w: 0.3, h: 0.4, fontSize: 14, color: C.edu, bold: true, fontFace: 'Microsoft YaHei' }); + s.addText(m, { x: 1.1, y, w: 11.5, h: 0.4, fontSize: 12, color: C.text, fontFace: 'Microsoft YaHei' }); + }); + footer(s, 14); +} + +// ============================================================ +// 第 15 页 · 补充模块 +// ============================================================ +{ + const s = addSlide(); + header(s, 'PART 03 · 学生价值 07', '补充模块:覆盖更多学习场景', C.edu); + const mods = [ + ['轮转见习', '记录临床轮转经历,\n按科室归档'], + ['技能视频', '搜索与学习\n临床操作技能视频'], + ['执业医师备考', '出题+间隔重复+\n模拟考试+个性化推荐'], + ['学术交流', '记录学术交流与\n会议参与'], + ['医学知识问答', '流式多轮\n医学知识对话'], + ['病例推演', '逐步推演临床病例,\n保持全程上下文'], + ['用药安全', '用药安全性\n检查与学习'], + ['持续扩展', '技能库可持续扩展,\n按需新增能力'], + ]; + mods.forEach((m, i) => { + const x = 0.6 + (i % 4) * 3.05; + const y = 1.7 + Math.floor(i / 4) * 2.5; + s.addShape('roundRect', { + x, y, w: 2.85, h: 2.25, fill: { color: C.bgSoft }, + line: { color: C.divider, width: 0.5 }, rectRadius: 0.1, + }); + s.addShape('rect', { x: x + 0.2, y: y + 0.25, w: 0.08, h: 1.75, fill: { color: i === 7 ? C.gold : C.edu } }); + s.addText(m[0], { + x: x + 0.4, y: y + 0.25, w: 2.3, h: 0.45, + fontSize: 14, bold: true, color: C.ink, fontFace: 'Microsoft YaHei', + }); + s.addText(m[1], { + x: x + 0.4, y: y + 0.8, w: 2.3, h: 1.2, + fontSize: 11.5, color: C.textMid, fontFace: 'Microsoft YaHei', + lineSpacingMultiple: 1.25, valign: 'top', + }); + }); + footer(s, 15); +} + +// ============================================================ +// 第 16 页 · 学生价值小结(一个日常) +// ============================================================ +{ + const s = addSlide(); + header(s, 'PART 03 · 学生价值', '一个学生的日常:闭环成长的一天', C.edu); + const steps = [ + ['1', '登录学习空间', '记录今日临床见习成果,\n系统自动归档+能力标签映射'], + ['2', '收到过程性评价', '30秒内获得反馈\n与发展建议'], + ['3', '课程对练', '系统出题(已过导师审核)→\n模拟答题 → 查看诊断报告'], + ['4', '临床对话对练', '模拟问诊 →\n获得三维评估'], + ['5', '查看能力画像', '六维度分数 +\n成长趋势'], + ['6', '职业规划', '查看差距分析 →\n按推荐资源学习'], + ['7', '闭环更新', '能力提升后 → 画像自动更新 →\n发展规划动态调整'], + ]; + steps.forEach((st, i) => { + const y = 1.7 + i * 0.72; + s.addShape('ellipse', { + x: 0.7, y: y + 0.05, w: 0.45, h: 0.45, fill: { color: C.edu }, + }); + s.addText(st[0], { + x: 0.7, y: y + 0.07, w: 0.45, h: 0.41, + fontSize: 14, bold: true, color: C.white, align: 'center', valign: 'middle', fontFace: 'Microsoft YaHei', + }); + s.addText(st[1], { + x: 1.4, y, w: 3.0, h: 0.55, + fontSize: 13.5, bold: true, color: C.ink, fontFace: 'Microsoft YaHei', valign: 'middle', + }); + s.addText(st[2], { + x: 4.6, y, w: 8.0, h: 0.55, + fontSize: 12, color: C.textMid, fontFace: 'Microsoft YaHei', valign: 'middle', + }); + if (i < 6) { + s.addShape('rect', { x: 0.91, y: y + 0.5, w: 0.03, h: 0.27, fill: { color: C.divider } }); + } + }); + footer(s, 16); +} + +// ============================================================ +// 第 17 页 · 第四部分章节页:学校价值 +// ============================================================ +{ + const s = addSlide(C.edu); + s.addShape('rect', { x: 0, y: 0, w: 0.35, h: H, fill: { color: C.white } }); + s.addText('PART 04', { + x: 1.0, y: 2.4, w: 11, h: 0.6, + fontSize: 18, color: 'BFE3E3', fontFace: 'Microsoft YaHei', charSpacing: 4, + }); + s.addText('学校价值', { + x: 1.0, y: 2.9, w: 11, h: 1.0, + fontSize: 44, bold: true, color: C.white, fontFace: 'Microsoft YaHei', + }); + s.addText('人才培养的数据底座 · 导师+管理+贯通', { + x: 1.0, y: 4.0, w: 11, h: 0.6, + fontSize: 18, color: 'D9EFEF', fontFace: 'Microsoft YaHei', + }); +} + +// ============================================================ +// 第 18 页 · 学校价值总览 +// ============================================================ +{ + const s = addSlide(); + header(s, 'PART 04 · 学校价值', '三个层面,支撑培养决策', C.edu); + const items = [ + ['导师端', '教学把关与精准指导', [ + '题目审核:系统生成→导师把关', + '成果点评:促进学生反思改进', + '带教画像:脱敏查看学生能力分布', + ]], + ['管理端', '治理与合规并重', [ + '技能治理:定义、配置、启用', + '权限管理:按角色分级', + '审计日志:配置变更可追溯', + '合规监控:越权拒绝可查', + ]], + ['数据底座', '能力图谱贯通,打破孤岛', [ + '统一能力标签连接所有模块', + '画像基于全量行为数据', + '培养质量可量化、可追溯', + ]], + ]; + items.forEach((it, i) => { + const x = 0.6 + i * 4.1; + s.addShape('roundRect', { + x, y: 1.7, w: 3.9, h: 5.1, fill: { color: C.bgSoft }, + line: { color: C.edu, width: 1.2 }, rectRadius: 0.1, + }); + s.addShape('rect', { x, y: 1.7, w: 3.9, h: 1.0, fill: { color: C.edu } }); + s.addText(it[0], { + x, y: 1.82, w: 3.9, h: 0.5, + fontSize: 18, bold: true, color: C.white, align: 'center', fontFace: 'Microsoft YaHei', + }); + s.addText(it[1], { + x, y: 2.3, w: 3.9, h: 0.35, + fontSize: 12, color: 'D9EFEF', align: 'center', fontFace: 'Microsoft YaHei', + }); + const lines = it[2].map(t => ({ text: t, options: { bullet: { code: '2022' } } })); + s.addText(lines, { + x: x + 0.3, y: 2.95, w: 3.4, h: 3.6, + fontSize: 12.5, color: C.text, fontFace: 'Microsoft YaHei', + lineSpacingMultiple: 1.4, valign: 'top', + }); + }); + footer(s, 18); +} + +// ============================================================ +// 第 19 页 · 导师端 +// ============================================================ +{ + const s = addSlide(); + header(s, 'PART 04 · 学校价值 01', '导师端:让带教有据可依', C.edu); + + const items = [ + ['题目审核', '系统生成的题目经导师通过后才可使用,\n退回需注明原因。保障教学内容准确性与权威性。', '系统出题 → 导师审核 → 通过/退回(附原因)'], + ['成果点评', '对学生学习成果进行教学点评,\n点评对学生可见,促进学生反思与改进。', '导师点评 → 学生可见 → 反思改进'], + ['带教学生画像', '查看所带学生的能力画像(脱敏),\n精准把握学生发展状况,针对性指导。', '脱敏画像 → 能力分布 → 针对性指导'], + ]; + items.forEach((it, i) => { + const y = 1.7 + i * 1.75; + s.addShape('roundRect', { + x: 0.6, y, w: 12.1, h: 1.55, fill: { color: C.bgSoft }, + line: { color: C.divider, width: 0.5 }, rectRadius: 0.08, + }); + s.addShape('rect', { x: 0.6, y, w: 0.1, h: 1.55, fill: { color: C.edu } }); + s.addText(it[0], { + x: 0.9, y: y + 0.15, w: 3.0, h: 0.45, + fontSize: 16, bold: true, color: C.edu, fontFace: 'Microsoft YaHei', + }); + s.addText(it[1], { + x: 0.9, y: y + 0.65, w: 6.8, h: 0.8, + fontSize: 12.5, color: C.text, fontFace: 'Microsoft YaHei', + lineSpacingMultiple: 1.3, valign: 'top', + }); + s.addShape('roundRect', { + x: 8.0, y: y + 0.35, w: 4.5, h: 0.85, fill: { color: C.white }, + line: { color: C.edu, width: 1 }, rectRadius: 0.06, + }); + s.addText(it[2], { + x: 8.15, y: y + 0.4, w: 4.2, h: 0.75, + fontSize: 11, color: C.edu, align: 'center', valign: 'middle', fontFace: 'Microsoft YaHei', + lineSpacingMultiple: 1.2, + }); + }); + footer(s, 19); +} + +// ============================================================ +// 第 20 页 · 管理端 +// ============================================================ +{ + const s = addSlide(); + header(s, 'PART 04 · 学校价值 02', '管理端:治理与合规并重', C.edu); + + // 技能五要素 + s.addText('技能治理 · 每个技能含五要素', { + x: 0.6, y: 1.6, w: 12, h: 0.4, + fontSize: 15, bold: true, color: C.ink, fontFace: 'Microsoft YaHei', + }); + const elems = ['输入规格', '处理逻辑', '知识源', '输出格式', '可信度规则']; + elems.forEach((e, i) => { + const x = 0.6 + i * 2.45; + s.addShape('roundRect', { + x, y: 2.05, w: 2.3, h: 0.6, fill: { color: C.edu }, rectRadius: 0.08, + }); + s.addText(e, { + x, y: 2.09, w: 2.3, h: 0.52, + fontSize: 12.5, bold: true, color: C.white, align: 'center', fontFace: 'Microsoft YaHei', + }); + }); + s.addText('管理员可定义、配置、启用技能;每个技能的变更都进入审计日志(操作者、时间、内容)', { + x: 0.6, y: 2.8, w: 12, h: 0.35, + fontSize: 11.5, color: C.textMid, fontFace: 'Microsoft YaHei', italic: true, + }); + + // 三大治理能力 + const govs = [ + ['权限管理', '按角色分级定义访问权限范围,\n学生、导师、管理员各司其职'], + ['审计日志', '追踪所有技能配置变更,\n操作者、时间、内容全程留痕'], + ['合规监控', '查看越权拒绝事件,\n确保敏感数据访问安全'], + ]; + govs.forEach((g, i) => { + const x = 0.6 + i * 4.1; + card(s, x, 3.4, 3.9, 1.8, g[0], [g[1]], { bar: C.edu }); + }); + + // 合规默认原则 + s.addShape('roundRect', { + x: 0.6, y: 5.5, w: 12.1, h: 1.5, fill: { color: C.bgPanel }, rectRadius: 0.08, + }); + s.addText('合规默认 · 敏感数据天然受保护', { + x: 0.8, y: 5.6, w: 11.7, h: 0.4, + fontSize: 14, bold: true, color: C.ink, fontFace: 'Microsoft YaHei', + }); + const rules = ['默认脱敏:非本人访问画像,敏感字段自动脱敏', '知情同意:跨用途使用必须有学生有效授权', '即时撤销:学生撤销授权后立即失效', '全审计:敏感操作留痕,保留≥12个月', '最小采集:仅采集与画像维度直接相关的数据']; + rules.forEach((r, i) => { + const x = 0.9 + (i % 3) * 4.0; + const y = 6.1 + Math.floor(i / 3) * 0.4; + s.addText('· ' + r, { + x, y, w: 3.8, h: 0.35, + fontSize: 10.5, color: C.text, fontFace: 'Microsoft YaHei', + }); + }); + footer(s, 20); +} + +// ============================================================ +// 第 21 页 · 数据贯通 +// ============================================================ +{ + const s = addSlide(); + header(s, 'PART 04 · 学校价值 03', '数据贯通:从孤岛到全景', C.edu); + + // 对比:传统 vs 本系统 + s.addText('传统平台:各模块数据孤立', { + x: 0.6, y: 1.6, w: 12, h: 0.4, + fontSize: 14, bold: true, color: C.rose, fontFace: 'Microsoft YaHei', + }); + const isolated = ['课程系统', '作业系统', '见习记录', '考核系统', '科研管理']; + isolated.forEach((t, i) => { + const x = 0.6 + i * 2.45; + s.addShape('roundRect', { + x, y: 2.05, w: 2.3, h: 0.6, fill: { color: C.white }, + line: { color: C.rose, width: 1, dashType: 'dash' }, rectRadius: 0.08, + }); + s.addText(t, { + x, y: 2.09, w: 2.3, h: 0.52, + fontSize: 12, color: C.rose, align: 'center', fontFace: 'Microsoft YaHei', + }); + }); + s.addText('数据互不相通,学生画像是单一模块的局部视图', { + x: 0.6, y: 2.75, w: 12, h: 0.3, + fontSize: 11, color: C.textMid, fontFace: 'Microsoft YaHei', italic: true, + }); + + // 本系统 + s.addText('本系统:统一能力图谱贯通所有模块', { + x: 0.6, y: 3.3, w: 12, h: 0.4, + fontSize: 14, bold: true, color: C.edu, fontFace: 'Microsoft YaHei', + }); + // 中心能力图谱 + s.addShape('ellipse', { + x: 5.4, y: 4.0, w: 2.5, h: 1.0, fill: { color: C.edu }, + }); + s.addText('能力图谱', { + x: 5.4, y: 4.1, w: 2.5, h: 0.8, + fontSize: 15, bold: true, color: C.white, align: 'center', valign: 'middle', fontFace: 'Microsoft YaHei', + }); + const connected = ['学习空间', '课程对练', '临床对话', '能力画像', '职业规划', '科研查询']; + connected.forEach((t, i) => { + const angle = (i / 6) * Math.PI * 2; + const cx = 6.65, cy = 4.5, r = 2.2; + const x = cx + Math.cos(angle) * r - 0.9; + const y = cy + Math.sin(angle) * r * 0.55 - 0.25; + s.addShape('roundRect', { + x, y, w: 1.8, h: 0.5, fill: { color: C.bgSoft }, + line: { color: C.edu, width: 1 }, rectRadius: 0.06, + }); + s.addText(t, { + x, y: y + 0.02, w: 1.8, h: 0.46, + fontSize: 11, color: C.edu, align: 'center', fontFace: 'Microsoft YaHei', + }); + }); + + // 三个收益 + const gains = [ + '学生画像基于全量行为数据,而非单一模块局部视图', + '职业规划能精确对照当前能力与目标岗位的差距', + '各模块使用数据相互增强,形成正反馈循环', + ]; + gains.forEach((g, i) => { + const y = 6.0 + i * 0.4; + s.addText('· ' + g, { + x: 0.8, y, w: 12, h: 0.35, + fontSize: 12, color: C.text, fontFace: 'Microsoft YaHei', + }); + }); + footer(s, 21); +} + +// ============================================================ +// 第 22 页 · 第五部分章节页:广电价值 +// ============================================================ +{ + const s = addSlide(C.gold); + s.addShape('rect', { x: 0, y: 0, w: 0.35, h: H, fill: { color: C.white } }); + s.addText('PART 05', { + x: 1.0, y: 2.4, w: 11, h: 0.6, + fontSize: 18, color: 'F0E0C0', fontFace: 'Microsoft YaHei', charSpacing: 4, + }); + s.addText('广电价值', { + x: 1.0, y: 2.9, w: 11, h: 1.0, + fontSize: 44, bold: true, color: C.white, fontFace: 'Microsoft YaHei', + }); + s.addText('算力支撑与数据通道 · 投入转化为人才培养', { + x: 1.0, y: 4.0, w: 11, h: 0.6, + fontSize: 18, color: 'F5E8D0', fontFace: 'Microsoft YaHei', + }); +} + +// ============================================================ +// 第 23 页 · 广电价值总览 +// ============================================================ +{ + const s = addSlide(); + header(s, 'PART 05 · 广电价值', '算力与数据通道,支撑平台运行', C.gold); + + const items = [ + ['算力支撑', '大模型推理算力供给', '支撑出题、评价、对话、\n总结等全部智能交互'], + ['流式输出', 'SSE流式响应', '对话对练实时逐字输出,\n支撑自然交互体验'], + ['数据通道', '多源医学文献接入', 'PubMed、CNKI、万方、\nUpToDate、Cochrane'], + ['社会价值', '算力转化为培养质量', '每一次调用都转化为\n学生的能力提升'], + ]; + items.forEach((it, i) => { + const x = 0.6 + (i % 2) * 6.1; + const y = 1.7 + Math.floor(i / 2) * 2.5; + s.addShape('roundRect', { + x, y, w: 5.9, h: 2.25, fill: { color: C.bgSoft }, + line: { color: C.gold, width: 1.2 }, rectRadius: 0.1, + }); + s.addShape('rect', { x: x + 0.25, y: y + 0.25, w: 0.08, h: 1.75, fill: { color: C.gold } }); + s.addText(it[0], { + x: x + 0.5, y: y + 0.22, w: 5.2, h: 0.4, + fontSize: 17, bold: true, color: C.ink, fontFace: 'Microsoft YaHei', + }); + s.addText(it[1], { + x: x + 0.5, y: y + 0.65, w: 5.2, h: 0.35, + fontSize: 12.5, color: C.gold, bold: true, fontFace: 'Microsoft YaHei', + }); + s.addText(it[2], { + x: x + 0.5, y: y + 1.05, w: 5.2, h: 1.0, + fontSize: 12, color: C.textMid, fontFace: 'Microsoft YaHei', + lineSpacingMultiple: 1.3, valign: 'top', + }); + }); + footer(s, 23); +} + +// ============================================================ +// 第 24 页 · 合作模式与投入产出 +// ============================================================ +{ + const s = addSlide(); + header(s, 'PART 05 · 广电价值', '合作模式与投入产出', C.gold); + + // 指标卡 + metric(s, 0.6, 1.7, 2.9, '21', '业务模块(后端)', C.gold); + metric(s, 3.7, 1.7, 2.9, '8+', '可复用技能', C.gold); + metric(s, 6.8, 1.7, 2.9, '5', '接入数据源', C.gold); + metric(s, 9.9, 1.7, 2.9, '3', '用户角色', C.gold); + + // 调用保护 + s.addText('调用保护机制', { + x: 0.6, y: 3.5, w: 12, h: 0.4, + fontSize: 15, bold: true, color: C.ink, fontFace: 'Microsoft YaHei', + }); + const limits = [ + ['20次/分钟', '单用户调用上限,\n防止瞬时滥用'], + ['500次/天', '单用户每日上限,\n保障算力公平分配'], + ['兜底降级', '算力不可用时回退\n规则/缓存/默认,\n不拖垮主流程'], + ]; + limits.forEach((l, i) => { + const x = 0.6 + i * 4.1; + s.addShape('roundRect', { + x, y: 3.95, w: 3.9, h: 1.5, fill: { color: C.bgSoft }, + line: { color: C.gold, width: 1 }, rectRadius: 0.08, + }); + s.addText(l[0], { + x, y: 4.1, w: 3.9, h: 0.45, + fontSize: 16, bold: true, color: C.gold, align: 'center', fontFace: 'Microsoft YaHei', + }); + s.addText(l[1], { + x: x + 0.2, y: 4.6, w: 3.5, h: 0.8, + fontSize: 11.5, color: C.textMid, align: 'center', fontFace: 'Microsoft YaHei', + lineSpacingMultiple: 1.25, + }); + }); + + // 投入产出 + s.addShape('roundRect', { + x: 0.6, y: 5.7, w: 12.1, h: 1.3, fill: { color: C.gold }, rectRadius: 0.08, + }); + s.addText('投入产出', { + x: 0.8, y: 5.8, w: 11.7, h: 0.35, + fontSize: 13, bold: true, color: C.white, fontFace: 'Microsoft YaHei', + }); + s.addText('算力投入 → 支撑学生日常学习与训练调用 → 转化为医学人才培养质量 → 广电算力赋能医学教育的社会效益与品牌价值。技能库可持续扩展,算力需求随应用深化稳步增长。', { + x: 0.8, y: 6.15, w: 11.7, h: 0.8, + fontSize: 12, color: C.white, fontFace: 'Microsoft YaHei', + lineSpacingMultiple: 1.3, valign: 'top', + }); + footer(s, 24); +} + +// ============================================================ +// 第 25 页 · 第六部分:设计底线 +// ============================================================ +{ + const s = addSlide(); + header(s, 'PART 06 · 设计底线', '四个底线,确保系统可信可用', C.steel); + + const lines = [ + ['能力贯通', '打破数据孤岛', '所有学习活动通过统一能力图谱连接。\n学习空间新增成果、对练完成测验、对话获得评分——\n都映射到同一套能力标签,画像基于全量行为数据。'], + ['可信优先', '医学场景零容忍幻觉', '所有输出附带可信度标注(来源+置信度);\n无法溯源的内容标注"未经核验";题目必须绑定权威知识源;\n总结中无法追溯的结论不作为引用输出。'], + ['合规默认', '敏感数据天然受保护', '默认脱敏、知情同意授权、即时撤销、\n全审计留痕(≥12个月)、最小采集。\n医学生画像包含敏感健康心理数据,天然受保护。'], + ['人机协同', '辅助而非替代', '系统生成题目→导师审核后才可使用;\n文献总结明确提示"不替代阅读原文";\n直接采用未核验输出会被要求核验;所有建议可追溯依据。'], + ]; + lines.forEach((l, i) => { + const x = 0.6 + (i % 2) * 6.1; + const y = 1.7 + Math.floor(i / 2) * 2.7; + s.addShape('roundRect', { + x, y, w: 5.9, h: 2.45, fill: { color: C.bgSoft }, + line: { color: C.steel, width: 1 }, rectRadius: 0.1, + }); + s.addShape('rect', { x: x + 0.25, y: y + 0.25, w: 0.08, h: 1.95, fill: { color: C.steel } }); + s.addText(l[0], { + x: x + 0.5, y: y + 0.22, w: 3.0, h: 0.4, + fontSize: 16, bold: true, color: C.ink, fontFace: 'Microsoft YaHei', + }); + s.addText(l[1], { + x: x + 3.6, y: y + 0.25, w: 2.1, h: 0.35, + fontSize: 11.5, color: C.steel, align: 'right', bold: true, fontFace: 'Microsoft YaHei', + }); + s.addText(l[2], { + x: x + 0.5, y: y + 0.7, w: 5.2, h: 1.6, + fontSize: 11.5, color: C.textMid, fontFace: 'Microsoft YaHei', + lineSpacingMultiple: 1.3, valign: 'top', + }); + }); + footer(s, 25); +} + +// ============================================================ +// 第 26 页 · 与传统教学平台差异 +// ============================================================ +{ + const s = addSlide(); + header(s, 'PART 06 · 差异化优势', '与传统教学平台的七个差异', C.steel); + + const rows = [ + ['数据关联', '各模块数据孤立', '统一能力图谱贯通所有模块'], + ['学习反馈', '周期性人工评价', '实时过程性评价 + 阶段终结性评价'], + ['职业发展', '静态指导', '动态闭环:能力变化→规划自动更新'], + ['协同素养', '无系统培养', '四维度协同能力训练+评估'], + ['数据隐私', '粗放管理', '知情同意+即时撤销+默认脱敏+全审计'], + ['教学质量', '缺乏把控', '系统生成+人工审核双层保障'], + ['输出信任', '缺乏验证机制', '全链路可信度标注 + 来源溯源'], + ]; + // 表头 + const headY = 1.7; + s.addShape('rect', { x: 0.6, y: headY, w: 12.1, h: 0.5, fill: { color: C.steel } }); + s.addText('维度', { x: 0.7, y: headY + 0.05, w: 2.5, h: 0.4, fontSize: 12.5, bold: true, color: C.white, fontFace: 'Microsoft YaHei' }); + s.addText('传统教学平台', { x: 3.3, y: headY + 0.05, w: 4.0, h: 0.4, fontSize: 12.5, bold: true, color: C.white, fontFace: 'Microsoft YaHei' }); + s.addText('本系统', { x: 7.5, y: headY + 0.05, w: 5.0, h: 0.4, fontSize: 12.5, bold: true, color: C.white, fontFace: 'Microsoft YaHei' }); + + rows.forEach((r, i) => { + const y = headY + 0.5 + i * 0.62; + if (i % 2 === 0) { + s.addShape('rect', { x: 0.6, y, w: 12.1, h: 0.62, fill: { color: C.bgSoft } }); + } + s.addText(r[0], { x: 0.7, y: y + 0.08, w: 2.5, h: 0.45, fontSize: 12, bold: true, color: C.ink, fontFace: 'Microsoft YaHei' }); + s.addText(r[1], { x: 3.3, y: y + 0.08, w: 4.0, h: 0.45, fontSize: 11.5, color: C.rose, fontFace: 'Microsoft YaHei' }); + s.addText(r[2], { x: 7.5, y: y + 0.08, w: 5.0, h: 0.45, fontSize: 11.5, color: C.edu, fontFace: 'Microsoft YaHei' }); + }); + footer(s, 26); +} + +// ============================================================ +// 第 27 页 · 结尾 +// ============================================================ +{ + const s = addSlide(C.ink); + s.addShape('rect', { x: 0, y: 0, w: 0.35, h: H, fill: { color: C.gold } }); + s.addShape('rect', { x: 0.35, y: 0, w: 0.08, h: H, fill: { color: C.edu } }); + + s.addText('让每一个医学生的成长', { + x: 1.0, y: 2.2, w: 11, h: 0.9, + fontSize: 40, bold: true, color: C.white, fontFace: 'Microsoft YaHei', + }); + s.addText('都被看见、可追溯、有依据', { + x: 1.0, y: 3.0, w: 11, h: 0.9, + fontSize: 40, bold: true, color: C.gold, fontFace: 'Microsoft YaHei', + }); + + const summary = [ + ['贯穿成长链', '基础医学 → 临床医学 → 见习实习 → 住院医师培养'], + ['数据可追溯', '每项能力结论可追溯到具体学习成果'], + ['培养有依据', '基于全量行为数据,而非主观印象'], + ['算力有价值', '广电算力投入转化为人才培养质量'], + ]; + summary.forEach((it, i) => { + const y = 4.4 + i * 0.55; + s.addText('· ', { x: 1.0, y, w: 0.3, h: 0.45, fontSize: 16, color: C.gold, bold: true, fontFace: 'Microsoft YaHei' }); + s.addText(it[0], { + x: 1.3, y, w: 2.2, h: 0.45, + fontSize: 15, bold: true, color: C.white, fontFace: 'Microsoft YaHei', + }); + s.addText(it[1], { + x: 3.6, y, w: 9, h: 0.45, + fontSize: 13, color: 'AEBFD0', fontFace: 'Microsoft YaHei', + }); + }); + + s.addText('医科高校智能学习中心', { + x: 1.0, y: 6.8, w: 11, h: 0.4, + fontSize: 14, color: '8FA3B8', fontFace: 'Microsoft YaHei', charSpacing: 2, + }); +} + +// ===== 生成文件 ===== +pres.writeFile({ fileName: '医科高校智能学习中心-汇报.pptx' }) + .then(fn => console.log('已生成: ' + fn)); diff --git a/src/common/rate-limit/ai-rate-limit.guard.ts b/src/common/rate-limit/ai-rate-limit.guard.ts new file mode 100644 index 0000000..86dc025 --- /dev/null +++ b/src/common/rate-limit/ai-rate-limit.guard.ts @@ -0,0 +1,83 @@ +import { + CanActivate, + ExecutionContext, + HttpException, + HttpStatus, + Injectable, + Logger, +} from '@nestjs/common'; + +import type { AuthenticatedUser } from '@modules/auth'; + +/** + * AI 调用速率限制守卫。 + * + * 基于内存滑动窗口,按用户限制 AI 密集型端点的调用频率, + * 防止滥用导致的成本失控。 + * + * 配置(环境变量): + * - `AI_RATE_LIMIT_PER_MINUTE`:每分钟最大调用次数(默认 20) + * - `AI_RATE_LIMIT_PER_DAY`:每日最大调用次数(默认 500) + * + * 注意:内存实现仅适用于单实例部署;多实例需替换为 Redis 等共享存储。 + */ +@Injectable() +export class AIRateLimitGuard implements CanActivate { + private readonly logger = new Logger(AIRateLimitGuard.name); + + private readonly perMinute = parseInt( + process.env.AI_RATE_LIMIT_PER_MINUTE ?? '20', + 10, + ); + private readonly perDay = parseInt( + process.env.AI_RATE_LIMIT_PER_DAY ?? '500', + 10, + ); + + /** 每个用户的调用时间戳记录(毫秒)。 */ + private readonly callLog = new Map(); + + canActivate(context: ExecutionContext): boolean { + const request = context.switchToHttp().getRequest<{ + user?: AuthenticatedUser; + }>(); + const userId = request.user?.id ?? 'anonymous'; + + const now = Date.now(); + const minuteAgo = now - 60_000; + const dayAgo = now - 86_400_000; + + const log = this.callLog.get(userId) ?? []; + + // 清理超过一天的记录 + const recentLog = log.filter((ts) => ts > dayAgo); + + const callsLastMinute = recentLog.filter((ts) => ts > minuteAgo).length; + const callsLastDay = recentLog.length; + + if (callsLastMinute >= this.perMinute) { + throw new HttpException( + { + code: 'RATE_LIMIT_EXCEEDED', + message: `AI 调用过于频繁,请稍后再试(每分钟上限 ${this.perMinute} 次)`, + }, + HttpStatus.TOO_MANY_REQUESTS, + ); + } + + if (callsLastDay >= this.perDay) { + throw new HttpException( + { + code: 'DAILY_LIMIT_EXCEEDED', + message: `今日 AI 调用次数已达上限(${this.perDay} 次),请明日再来`, + }, + HttpStatus.TOO_MANY_REQUESTS, + ); + } + + recentLog.push(now); + this.callLog.set(userId, recentLog); + + return true; + } +} diff --git a/src/infra/ai-gateway/ai-stream.controller.ts b/src/infra/ai-gateway/ai-stream.controller.ts new file mode 100644 index 0000000..36afe75 --- /dev/null +++ b/src/infra/ai-gateway/ai-stream.controller.ts @@ -0,0 +1,89 @@ +import { + Body, + Controller, + Inject, + Post, + Res, + UseGuards, +} from '@nestjs/common'; +import { Response } from 'express'; + +import { AuthenticatedUser, CurrentUser } from '@modules/auth'; +import { AIRateLimitGuard } from '@common/rate-limit/ai-rate-limit.guard'; + +import { AI_GATEWAY, AIGateway, AIStreamRequest } from './ai-gateway.types'; + +/** + * AI 流式生成端点。 + * + * 通过 Server-Sent Events 逐 chunk 返回 AI 生成内容, + * 前端可实时显示生成过程(打字机效果)。 + * + * 受 `AIRateLimitGuard` 速率限制保护,防止 AI 调用滥用。 + */ +@Controller('api/ai') +@UseGuards(AIRateLimitGuard) +export class AIStreamController { + constructor( + @Inject(AI_GATEWAY) private readonly ai: AIGateway, + ) {} + + /** + * POST /api/ai/stream + * + * Body: { prompt, systemPrompt?, messages?, temperature?, maxTokens? } + * + * 返回 SSE 流: + * - data: {"chunk":"..."} — 每个生成 chunk + * - data: {"done":true,"content":"完整内容","annotation":{...}} — 生成结束 + * - data: {"error":"错误信息"} — 出错 + */ + @Post('stream') + async stream( + @Body() body: { + prompt: string; + systemPrompt?: string; + messages?: Array<{ role: 'system' | 'user' | 'assistant'; content: string }>; + temperature?: number; + maxTokens?: number; + }, + @Res() res: Response, + @CurrentUser() _user: AuthenticatedUser, + ) { + res.setHeader('Content-Type', 'text/event-stream'); + // no-transform 阻止下游代理(Next.js dev proxy)对 SSE 流进行 gzip 压缩, + // 压缩会缓冲整个响应导致前端无法实时收到 chunk(打字机效果失效)。 + res.setHeader('Cache-Control', 'no-cache, no-transform'); + res.setHeader('Content-Encoding', 'identity'); + res.setHeader('Connection', 'keep-alive'); + res.setHeader('X-Accel-Buffering', 'no'); + res.flushHeaders(); + + const request: AIStreamRequest = { + prompt: body.prompt, + systemPrompt: body.systemPrompt, + messages: body.messages, + temperature: body.temperature, + maxTokens: body.maxTokens, + }; + + try { + const result = await this.ai.generateStream(request, (chunk) => { + res.write(`data: ${JSON.stringify({ chunk })}\n\n`); + }); + + res.write( + `data: ${JSON.stringify({ + done: true, + content: result.content, + annotation: result.annotation, + })}\n\n`, + ); + } catch (error: unknown) { + const message = error instanceof Error ? error.message : '生成失败'; + res.write(`data: ${JSON.stringify({ error: message })}\n\n`); + } finally { + res.end(); + } + } +} diff --git a/src/modules/exam-prep/entities/question-bank.entity.ts b/src/modules/exam-prep/entities/question-bank.entity.ts new file mode 100644 index 0000000..be2930d --- /dev/null +++ b/src/modules/exam-prep/entities/question-bank.entity.ts @@ -0,0 +1,30 @@ +import { Column, CreateDateColumn, Entity, Index, PrimaryColumn } from 'typeorm'; + +/** + * 执业医师备考内置题库实体。 + * 用于持久化存储 AI 生成或人工录入的题库题目,支持自动查重。 + */ +@Entity({ name: 'question_bank' }) +export class QuestionBankEntity { + @PrimaryColumn({ type: 'varchar', length: 128 }) + id!: string; + + @Index() + @Column({ type: 'varchar', length: 64 }) + subject!: string; + + @Column({ type: 'text' }) + content!: string; + + @Column({ type: 'jsonb', default: () => "'[]'" }) + options!: string[]; + + @Column({ type: 'varchar', length: 8 }) + correctAnswer!: string; + + @Column({ type: 'text' }) + explanation!: string; + + @CreateDateColumn({ type: 'timestamptz' }) + createdAt!: Date; +} diff --git a/src/modules/exam-prep/repositories/in-memory-question-bank.repository.ts b/src/modules/exam-prep/repositories/in-memory-question-bank.repository.ts new file mode 100644 index 0000000..6080042 --- /dev/null +++ b/src/modules/exam-prep/repositories/in-memory-question-bank.repository.ts @@ -0,0 +1,71 @@ +import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'; +import { dirname, join } from 'node:path'; + +import { Injectable, Logger, OnModuleInit } from '@nestjs/common'; + +import type { QuestionBankEntry, QuestionBankRepository } from '../exam-prep.types'; + +const DATA_DIR = join(process.cwd(), '.data'); +const DATA_FILE = join(DATA_DIR, 'question-bank.json'); + +/** + * 内存题库仓库,附带 JSON 文件持久化。 + * 适用于 DB_ENABLED=false 的本地开发/测试场景,重启后可恢复数据。 + */ +@Injectable() +export class InMemoryQuestionBankRepository implements QuestionBankRepository, OnModuleInit { + private readonly logger = new Logger(InMemoryQuestionBankRepository.name); + private store = new Map(); + + onModuleInit() { + this.loadFromDisk(); + } + + async save(entry: QuestionBankEntry): Promise { + this.store.set(entry.id, entry); + this.flushToDisk(); + } + + async list(subject?: string): Promise { + const all = Array.from(this.store.values()).sort( + (a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime(), + ); + if (subject) { + return all.filter((q) => q.subject === subject); + } + return all; + } + + async findById(id: string): Promise { + return this.store.get(id); + } + + private loadFromDisk(): void { + if (!existsSync(DATA_FILE)) { + this.logger.log(`题库持久化文件不存在,等待首次写入: ${DATA_FILE}`); + return; + } + try { + const raw = readFileSync(DATA_FILE, 'utf-8'); + const parsed = JSON.parse(raw) as QuestionBankEntry[]; + if (Array.isArray(parsed)) { + this.store = new Map(parsed.map((q) => [q.id, q])); + this.logger.log(`已从 ${DATA_FILE} 加载 ${this.store.size} 条题库记录`); + } + } catch (err: any) { + this.logger.warn(`题库持久化文件读取失败: ${err.message}`); + } + } + + private flushToDisk(): void { + try { + if (!existsSync(DATA_DIR)) { + mkdirSync(DATA_DIR, { recursive: true }); + } + const data = Array.from(this.store.values()); + writeFileSync(DATA_FILE, JSON.stringify(data, null, 2), 'utf-8'); + } catch (err: any) { + this.logger.warn(`题库持久化文件写入失败: ${err.message}`); + } + } +} diff --git a/src/modules/exam-prep/repositories/typeorm-question-bank.repository.ts b/src/modules/exam-prep/repositories/typeorm-question-bank.repository.ts new file mode 100644 index 0000000..6e1e0e1 --- /dev/null +++ b/src/modules/exam-prep/repositories/typeorm-question-bank.repository.ts @@ -0,0 +1,55 @@ +import { Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; + +import { QuestionBankEntity } from '../entities/question-bank.entity'; +import type { QuestionBankEntry, QuestionBankRepository } from '../exam-prep.types'; + +/** + * 基于 TypeORM / PostgreSQL 的题库持久化实现。 + */ +@Injectable() +export class TypeOrmQuestionBankRepository implements QuestionBankRepository { + constructor( + @InjectRepository(QuestionBankEntity) + private readonly repo: Repository, + ) {} + + async save(entry: QuestionBankEntry): Promise { + const entity = new QuestionBankEntity(); + entity.id = entry.id; + entity.subject = entry.subject; + entity.content = entry.content; + entity.options = entry.options; + entity.correctAnswer = entry.correctAnswer; + entity.explanation = entry.explanation; + entity.createdAt = new Date(entry.createdAt); + await this.repo.save(entity); + } + + async list(subject?: string): Promise { + const where = subject ? { subject } : {}; + const rows = await this.repo.find({ + where, + order: { createdAt: 'DESC' }, + }); + return rows.map((r) => this.toEntry(r)); + } + + async findById(id: string): Promise { + const row = await this.repo.findOne({ where: { id } }); + return row ? this.toEntry(row) : undefined; + } + + private toEntry(r: QuestionBankEntity): QuestionBankEntry { + return { + id: r.id, + subject: r.subject, + content: r.content, + options: r.options, + correctAnswer: r.correctAnswer, + explanation: r.explanation, + createdAt: r.createdAt.toISOString(), + }; + } +} diff --git a/tsconfig.json b/tsconfig.json index 6a978e1..e480bc6 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -9,7 +9,9 @@ "target": "ES2021", "sourceMap": true, "outDir": "./dist", + "rootDir": "./src", "baseUrl": "./", + "ignoreDeprecations": "6.0", "incremental": true, "skipLibCheck": true, "strictNullChecks": true, diff --git a/web/src/components/exam-recommendations.tsx b/web/src/components/exam-recommendations.tsx new file mode 100644 index 0000000..227b64f --- /dev/null +++ b/web/src/components/exam-recommendations.tsx @@ -0,0 +1,87 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { Lightbulb, TrendingDown } from "lucide-react"; + +import { Badge } from "@/components/ui/badge"; +import { examPrepApi } from "@/lib/services"; + +interface Recommendation { + subject: string; + label: string; + priority: number; + reason: string; + correctRate: number | null; + totalAnswered: number; + daysSinceLastPractice: number; +} + +const PRIORITY_VARIANT: Record = { + 1: "destructive", + 2: "info", + 3: "warning", + 4: "success", +}; + +/** + * 个性化备考推荐横幅:展示薄弱科目与今日复习建议。 + */ +export function ExamRecommendations({ + onPickSubject, +}: { + onPickSubject?: (subject: string) => void; +}) { + const [recs, setRecs] = useState([]); + const [summary, setSummary] = useState(""); + const [loaded, setLoaded] = useState(false); + + useEffect(() => { + examPrepApi + .getRecommendations() + .then((res) => { + setRecs(res.recommendations ?? []); + setSummary(res.summary ?? ""); + setLoaded(true); + }) + .catch(() => setLoaded(true)); + }, []); + + if (!loaded || recs.length === 0) return null; + + // 只展示优先级最高的前 3 个 + const top = recs.slice(0, 3); + + return ( +
+
+ + + +
+

智能备考推荐

+

{summary}

+
+
+
+ {top.map((r) => ( + + ))} +
+
+ ); +} diff --git a/web/src/components/markdown.tsx b/web/src/components/markdown.tsx new file mode 100644 index 0000000..adbd109 --- /dev/null +++ b/web/src/components/markdown.tsx @@ -0,0 +1,59 @@ +"use client"; + +import ReactMarkdown from "react-markdown"; +import remarkGfm from "remark-gfm"; + +import { cn } from "@/lib/utils"; + +/** + * 统一的 Markdown 渲染组件。 + * + * 用于渲染 AI 生成的富文本内容(标题、列表、加粗、代码、表格等), + * 样式与应用主题一致,支持暗色模式。可在流式输出过程中实时渲染。 + */ +export function Markdown({ + content, + className, +}: { + content: string; + className?: string; +}) { + return ( +
+ {content} +
+ ); +} diff --git a/web/src/components/mock-exam.tsx b/web/src/components/mock-exam.tsx new file mode 100644 index 0000000..8eaff07 --- /dev/null +++ b/web/src/components/mock-exam.tsx @@ -0,0 +1,322 @@ +"use client"; + +import { useEffect, useRef, useState } from "react"; +import { CheckCircle2, Clock, FileText, XCircle } from "lucide-react"; +import { toast } from "sonner"; + +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Select } from "@/components/ui/select"; +import { ApiError } from "@/lib/api"; +import { examPrepApi } from "@/lib/services"; + +const SUBJECTS = [ + { value: "internal", label: "内科学" }, + { value: "surgery", label: "外科学" }, + { value: "pediatrics", label: "儿科学" }, + { value: "obgyn", label: "妇产科学" }, + { value: "pharmacology", label: "药理学" }, + { value: "pathology", label: "病理学" }, + { value: "diagnostics", label: "诊断学" }, + { value: "ethics", label: "医学伦理" }, +]; + +/* eslint-disable @typescript-eslint/no-explicit-any */ + +type Phase = "config" | "exam" | "result"; + +/** + * 模拟考试组件:限时批量答题 → 交卷统一批阅。 + */ +export function MockExam() { + const [phase, setPhase] = useState("config"); + const [count, setCount] = useState(10); + const [timeLimit, setTimeLimit] = useState(20); + const [busy, setBusy] = useState(false); + const [error, setError] = useState(null); + + const [exam, setExam] = useState(null); + const [answers, setAnswers] = useState>({}); + const [result, setResult] = useState(null); + const [remainingSec, setRemainingSec] = useState(0); + const timerRef = useRef | null>(null); + + useEffect(() => { + return () => { + if (timerRef.current) clearInterval(timerRef.current); + }; + }, []); + + async function startExam() { + setBusy(true); + setError(null); + try { + const e = await examPrepApi.startMockExam({ count, timeLimitMinutes: timeLimit }); + setExam(e); + setAnswers({}); + setResult(null); + setPhase("exam"); + setRemainingSec(timeLimit * 60); + timerRef.current = setInterval(() => { + setRemainingSec((s) => { + if (s <= 1) { + if (timerRef.current) clearInterval(timerRef.current); + void submitExam(e, true); + return 0; + } + return s - 1; + }); + }, 1000); + } catch (err) { + setError(err instanceof ApiError ? err.message : "无法发起模拟考试"); + } finally { + setBusy(false); + } + } + + async function submitExam(examData = exam, auto = false) { + if (!examData) return; + if (timerRef.current) clearInterval(timerRef.current); + setBusy(true); + setError(null); + try { + const payload = { + examId: examData.examId, + answers: examData.questions.map((q: any) => ({ + questionId: q.id, + answer: answers[q.id] ?? "", + })), + }; + const r = await examPrepApi.submitMockExam(payload); + setResult(r); + setPhase("result"); + if (auto) toast.warning("考试时间到,已自动交卷。"); + } catch (err) { + setError(err instanceof ApiError ? err.message : "交卷失败"); + } finally { + setBusy(false); + } + } + + function reset() { + if (timerRef.current) clearInterval(timerRef.current); + setPhase("config"); + setExam(null); + setAnswers({}); + setResult(null); + } + + const answeredCount = exam ? Object.keys(answers).filter((k) => answers[k]).length : 0; + + // ── 配置阶段 ── + if (phase === "config") { + return ( + + + + 模拟考试 + + + +

+ 限时批量答题,模拟真实考场环境。交卷后统一批阅并生成成绩单。 +

+ {error && } +
+
+ + +
+
+ + +
+ +
+
+
+ ); + } + + // ── 考试阶段 ── + if (phase === "exam" && exam) { + const mm = String(Math.floor(remainingSec / 60)).padStart(2, "0"); + const ss = String(remainingSec % 60).padStart(2, "0"); + const lowTime = remainingSec <= 60; + return ( + + +
+ 模拟考试进行中 +
+ + 已答 {answeredCount} / {exam.totalQuestions} + + + {mm}:{ss} + +
+
+
+ + {error && } + {exam.questions.map((q: any) => ( +
+
+ {q.index}. +
+

{q.content}

+ + {q.subjectLabel} + +
+
+ {q.options && ( +
+ {q.options.map((opt: string, i: number) => { + const letter = String.fromCharCode(65 + i); + const selected = answers[q.id] === letter; + return ( + + ); + })} +
+ )} +
+ ))} +
+ + +
+
+
+ ); + } + + // ── 成绩阶段 ── + if (phase === "result" && result) { + return ( + + + 考试成绩单 + + +
+
= 80 ? "text-success" : result.score >= 60 ? "text-warning" : "text-destructive" + }`} + > + {result.score} +
+
+ + {result.passed ? "及格" : "未及格"} + + {result.overtime && 超时} +
+

+ {result.correctCount}/{result.totalQuestions} 题正确 · 用时 {result.elapsedMinutes} 分钟 +

+

{result.comment}

+
+ + {/* 科目分布 */} +
+

各科目表现

+ {Object.entries(result.bySubject).map(([key, val]: [string, any]) => { + const label = SUBJECTS.find((s) => s.value === key)?.label ?? key; + return ( +
+ {label} + = 80 ? "text-success" : val.rate >= 60 ? "text-warning" : "text-destructive" + }`} + > + {val.correct}/{val.total}({val.rate}%) + +
+ ); + })} +
+ + {/* 逐题解析 */} +
+

逐题解析

+
+ {result.results.map((r: any, i: number) => ( +
+
+ {r.correct ? ( + + ) : ( + + )} + 第 {i + 1} 题 +
+

{r.content}

+
+ 你的答案:{r.userAnswer} + 正确答案:{r.correctAnswer} +
+ {r.explanation && ( +

{r.explanation}

+ )} +
+ ))} +
+
+ + +
+
+ ); + } + + return null; +} + +function ErrorText({ message }: { message: string }) { + return ( +
{message}
+ ); +} diff --git a/web/src/components/offline-indicator.tsx b/web/src/components/offline-indicator.tsx new file mode 100644 index 0000000..4b1368d --- /dev/null +++ b/web/src/components/offline-indicator.tsx @@ -0,0 +1,35 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { WifiOff } from "lucide-react"; + +/** + * 离线状态指示器:检测网络断开,提示用户当前展示的是缓存数据。 + */ +export function OfflineIndicator() { + const [offline, setOffline] = useState(false); + + useEffect(() => { + if (typeof navigator === "undefined") return; + setOffline(!navigator.onLine); + + const onOffline = () => setOffline(true); + const onOnline = () => setOffline(false); + window.addEventListener("offline", onOffline); + window.addEventListener("online", onOnline); + return () => { + window.removeEventListener("offline", onOffline); + window.removeEventListener("online", onOnline); + }; + }, []); + + if (!offline) return null; + + return ( +
+ + 离线模式(显示缓存数据) + 离线 +
+ ); +} diff --git a/医科高校智能学习中心-汇报.pptx b/医科高校智能学习中心-汇报.pptx new file mode 100644 index 0000000000000000000000000000000000000000..716aa85b8537cafc7cdf118179ab036c905ea99b GIT binary patch literal 704699 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