diff --git a/.env b/.env index a2578b9..fb3bd7c 100644 --- a/.env +++ b/.env @@ -10,8 +10,9 @@ SPRING_PROFILES_ACTIVE=prod JWT_SECRET=your-secret-key-change-in-production-environment # Dify配置 +# 请将下面的API Key替换为您实际的Dify API Key DIFY_API_URL=https://api.dify.ai/v1 -DIFY_API_KEY=your-dify-api-key-here +DIFY_API_KEY=app-croZF0SSV5fiyXbK3neGrOT6 # 大模型配置 LLM_API_URL=https://api.openai.com/v1 @@ -29,3 +30,5 @@ CNKI_PASSWORD= # 文献下载路径 LITERATURE_DOWNLOAD_PATH=./downloads + + diff --git a/DEPLOYMENT.md b/DEPLOYMENT.md deleted file mode 100644 index 0a7dc54..0000000 --- a/DEPLOYMENT.md +++ /dev/null @@ -1,406 +0,0 @@ -# 医学信息支持系统 - 部署说明 - -## 环境要求 - -### 开发环境 -- **JDK**: 17 或更高版本 -- **Node.js**: 18 或更高版本 -- **Maven**: 3.8 或更高版本 -- **MySQL**: 8.0 或更高版本 - -### 生产环境 -- **Docker**: 20.10 或更高版本 -- **Docker Compose**: 2.0 或更高版本 - -## 快速开始(使用Docker) - -### 1. 克隆项目 - -```bash -git clone -cd 文献流程 -``` - -### 2. 配置环境变量 - -复制环境变量示例文件并修改配置: - -```bash -cp .env.example .env -``` - -编辑 `.env` 文件,配置以下关键信息: - -```env -# 数据库密码 -MYSQL_ROOT_PASSWORD=your-secure-password -MYSQL_PASSWORD=your-secure-password - -# JWT密钥(生产环境必须修改) -JWT_SECRET=your-very-secure-jwt-secret-key - -# Dify API配置 -DIFY_API_KEY=your-dify-api-key - -# 大模型API配置 -LLM_API_KEY=your-llm-api-key -``` - -### 3. 启动服务 - -```bash -docker-compose up -d -``` - -### 4. 访问系统 - -- **前端地址**: http://localhost -- **后端API**: http://localhost:8080/api -- **默认账号**: admin / admin123 - -### 5. 查看日志 - -```bash -# 查看所有服务日志 -docker-compose logs -f - -# 查看特定服务日志 -docker-compose logs -f backend -docker-compose logs -f frontend -``` - -### 6. 停止服务 - -```bash -docker-compose down -``` - -## 本地开发部署 - -### 后端开发 - -#### 1. 准备MySQL数据库 - -```bash -# 启动MySQL -mysql -u root -p - -# 执行数据库脚本 -source database/schema.sql -source database/sample_data.sql -``` - -#### 2. 配置application.yml - -编辑 `backend/src/main/resources/application.yml`: - -```yaml -spring: - datasource: - url: jdbc:mysql://localhost:3306/medical_info_system - username: your-username - password: your-password -``` - -#### 3. 启动后端服务 - -```bash -cd backend -mvn clean install -mvn spring-boot:run -``` - -后端服务将在 http://localhost:8080 启动。 - -#### 4. API文档 - -启动后访问:http://localhost:8080/api/swagger-ui.html - -### 前端开发 - -#### 1. 安装依赖 - -```bash -cd frontend -npm install -``` - -#### 2. 启动开发服务器 - -```bash -npm run dev -``` - -前端开发服务器将在 http://localhost:3000 启动。 - -#### 3. 构建生产版本 - -```bash -npm run build -``` - -构建结果将输出到 `frontend/dist` 目录。 - -## 生产环境部署 - -### 方式一:使用Docker Compose(推荐) - -1. 按照"快速开始"部分的步骤操作 -2. 确保配置正确的环境变量 -3. 建议配置反向代理(如Nginx)并启用HTTPS - -### 方式二:手动部署 - -#### 后端部署 - -```bash -cd backend -mvn clean package -DskipTests -java -jar target/medical-info-system-1.0.0.jar --spring.profiles.active=prod -``` - -#### 前端部署 - -```bash -cd frontend -npm run build - -# 将dist目录内容部署到Nginx -cp -r dist/* /usr/share/nginx/html/ -``` - -#### Nginx配置示例 - -```nginx -server { - listen 80; - server_name your-domain.com; - - # 前端静态文件 - root /usr/share/nginx/html; - index index.html; - - # 前端路由 - location / { - try_files $uri $uri/ /index.html; - } - - # 后端API代理 - location /api/ { - proxy_pass http://localhost:8080/api/; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - } -} -``` - -## 配置说明 - -### 后端配置 - -主配置文件:`backend/src/main/resources/application.yml` - -```yaml -app: - # JWT配置 - jwt: - secret: ${JWT_SECRET} # JWT密钥 - expiration: 86400000 # 过期时间(毫秒) - - # Dify配置 - dify: - api-url: ${DIFY_API_URL} - api-key: ${DIFY_API_KEY} - - # 大模型配置 - llm: - api-url: ${LLM_API_URL} - api-key: ${LLM_API_KEY} - model: ${LLM_MODEL} - - # 文献下载配置 - literature: - download-path: ${LITERATURE_DOWNLOAD_PATH} - accounts: - pubmed: - username: ${PUBMED_USERNAME} - password: ${PUBMED_PASSWORD} - embase: - username: ${EMBASE_USERNAME} - password: ${EMBASE_PASSWORD} - cnki: - username: ${CNKI_USERNAME} - password: ${CNKI_PASSWORD} -``` - -### 前端配置 - -代理配置:`frontend/vite.config.js` - -```javascript -export default defineConfig({ - server: { - port: 3000, - proxy: { - '/api': { - target: 'http://localhost:8080', - changeOrigin: true - } - } - } -}) -``` - -## 数据库管理 - -### 备份数据库 - -```bash -# 使用Docker -docker exec medical-info-mysql mysqldump -u root -p medical_info_system > backup.sql - -# 本地MySQL -mysqldump -u root -p medical_info_system > backup.sql -``` - -### 恢复数据库 - -```bash -# 使用Docker -docker exec -i medical-info-mysql mysql -u root -p medical_info_system < backup.sql - -# 本地MySQL -mysql -u root -p medical_info_system < backup.sql -``` - -## 监控与日志 - -### 应用日志 - -后端日志位置: -- Docker部署:`./logs/` -- 本地开发:控制台输出 - -### Docker容器监控 - -```bash -# 查看容器状态 -docker-compose ps - -# 查看资源使用 -docker stats - -# 查看容器日志 -docker-compose logs -f [service-name] -``` - -## 常见问题 - -### 1. 数据库连接失败 - -**问题**: 后端无法连接数据库 - -**解决方案**: -- 检查MySQL服务是否启动 -- 确认数据库用户名和密码是否正确 -- 检查防火墙设置 -- 如使用Docker,确保容器在同一网络中 - -### 2. 前端无法访问后端API - -**问题**: 前端请求后端API时出现跨域错误 - -**解决方案**: -- 检查后端CORS配置 -- 确认API代理配置正确 -- 检查后端服务是否正常运行 - -### 3. 文献下载失败 - -**问题**: 系统无法下载文献 - -**解决方案**: -- 检查文献数据库账号配置是否正确 -- 确认下载路径是否有写入权限 -- 查看后端日志了解具体错误信息 - -### 4. Dify API调用失败 - -**问题**: AI功能无法正常使用 - -**解决方案**: -- 确认Dify API Key配置正确 -- 检查网络连接是否正常 -- 查看API配额是否用完 - -## 性能优化 - -### 数据库优化 - -```sql --- 创建必要的索引 -CREATE INDEX idx_inquiry_status ON inquiry_requests(status); -CREATE INDEX idx_inquiry_created_at ON inquiry_requests(created_at); -CREATE INDEX idx_literature_inquiry ON literatures(inquiry_request_id); -``` - -### 应用优化 - -1. **后端**: - - 启用数据库连接池 - - 配置适当的JVM参数 - - 使用Redis缓存热点数据 - -2. **前端**: - - 启用Gzip压缩 - - 使用CDN加速静态资源 - - 实现路由懒加载 - -### Docker优化 - -```yaml -# docker-compose.yml中添加资源限制 -services: - backend: - deploy: - resources: - limits: - cpus: '2' - memory: 2G -``` - -## 安全建议 - -1. **生产环境必须修改**: - - 数据库密码 - - JWT密钥 - - 默认管理员密码 - -2. **启用HTTPS**: - - 配置SSL证书 - - 强制HTTPS访问 - -3. **定期更新**: - - 及时更新依赖包 - - 修复安全漏洞 - -4. **访问控制**: - - 配置防火墙规则 - - 限制数据库访问 - - 实施IP白名单 - -## 技术支持 - -如遇到问题,请: -1. 查看日志文件获取详细错误信息 -2. 参考本文档的常见问题部分 -3. 联系技术支持团队 - -## 更新日志 - -### v1.0.0 (2024-10-26) -- 初始版本发布 -- 实现核心功能模块 -- 支持Docker部署 - diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index a35946d..0000000 --- a/Dockerfile +++ /dev/null @@ -1,77 +0,0 @@ -# syntax=docker/dockerfile:1 - -# Comments are provided throughout this file to help you get started. -# If you need more help, visit the Dockerfile reference guide at -# https://docs.docker.com/go/dockerfile-reference/ - -# Want to help us make this template better? Share your feedback here: https://forms.gle/ybq9Krt8jtBL3iCk7 - -################################################################################ - -# Create a stage for resolving and downloading dependencies. -FROM eclipse-temurin:8-jdk-jammy as deps - -WORKDIR /build - -# Copy the mvnw wrapper with executable permissions. -COPY --chmod=0755 mvnw mvnw -COPY .mvn/ .mvn/ - -# Download dependencies as a separate step to take advantage of Docker's caching. -# Leverage a cache mount to /root/.m2 so that subsequent builds don't have to -# re-download packages. -RUN --mount=type=bind,source=pom.xml,target=pom.xml \ - --mount=type=cache,target=/root/.m2 ./mvnw dependency:go-offline -DskipTests - -################################################################################ - -# Create a stage for building the application based on the stage with downloaded dependencies. -# This Dockerfile is optimized for Java applications that output an uber jar, which includes -# all the dependencies needed to run your app inside a JVM. If your app doesn't output an uber -# jar and instead relies on an application server like Apache Tomcat, you'll need to update this -# stage with the correct filename of your package and update the base image of the "final" stage -# use the relevant app server, e.g., using tomcat (https://hub.docker.com/_/tomcat/) as a base image. -FROM deps as package - -WORKDIR /build - -COPY ./ src/ -RUN --mount=type=bind,source=pom.xml,target=pom.xml \ - --mount=type=cache,target=/root/.m2 \ - ./mvnw package -DskipTests && \ - mv target/$(./mvnw help:evaluate -Dexpression=project.artifactId -q -DforceStdout)-$(./mvnw help:evaluate -Dexpression=project.version -q -DforceStdout).jar target/app.jar - - -################################################################################ - -# Create a new stage for running the application that contains the minimal -# runtime dependencies for the application. This often uses a different base -# image from the install or build stage where the necessary files are copied -# from the install stage. -# -# The example below uses eclipse-temurin's JRE image as the foundation for running the app. -# By specifying the "8-jre-jammy" tag, it will also use whatever happens to be the -# most recent version of that tag when you build your Dockerfile. -# If reproducibility is important, consider using a specific digest SHA, like -# eclipse-temurin@sha256:99cede493dfd88720b610eb8077c8688d3cca50003d76d1d539b0efc8cca72b4. -FROM eclipse-temurin:8-jre-jammy AS final - -# Create a non-privileged user that the app will run under. -# See https://docs.docker.com/go/dockerfile-user-best-practices/ -ARG UID=10001 -RUN adduser \ - --disabled-password \ - --gecos "" \ - --home "/nonexistent" \ - --shell "/sbin/nologin" \ - --no-create-home \ - --uid "${UID}" \ - appuser -USER appuser - -# Copy the executable from the "package" stage. -COPY --from=package build/target/app.jar app.jar - -EXPOSE 8080 - -ENTRYPOINT [ "java", "-jar", "app.jar" ] diff --git a/Readme.md b/Readme.md new file mode 100644 index 0000000..c480bbb --- /dev/null +++ b/Readme.md @@ -0,0 +1,280 @@ +# 益普生医学信息支持系统 - 软件开发需求文档 + +## 1. 项目概述 + +### 1.1 项目目的 +构建一个智能化的医学信息支持系统,解决企业在受到客户(主要是医生)的信息支持需求时,能够利用该网站快速处理信息,给到客户支持。 + +### 1.2 核心价值 +### 1.2.1 提高医学信息查询效率 + - 从信息申请的文本中智能化提取出用于后续检索的关键词 + - 首先在已建立的知识库中进行查询,而后在查询外部公开网络中的信息 + - 通过AI对查询的结果进行智能化分析 + - 自动化文献下载和管理 +### 1.2.2 标准化信息处理流程 + - 规范化回复内容生成 + +## 2. 详细业务流程 + +### 2.1 咨询需求创建阶段 +1. **客户需求提交**:客户通过邮件向益普生提出需求(如:请提供达菲林最新的临床试验数据) +2. **需求记录**:医学信息团队记录需求并指派专门人员处理 +3. **系统录入**:医学信息专员将查询内容录入系统 + - **必填字段**:查询内容描述 + - **可选字段**:邮箱(非必填)、姓名、联系方式等(非必填) + +### 2.2 智能关键词提取阶段 +4. **AI关键词提取**:系统通过AI从查询申请的具体描述中自动提取关键词 + - 提取范围:药品中文名称(阿司匹林)、英文名称(Aspirin)、查询请求事项(如,儿童用药中的安全性注意事项) + - 该提取动作通过调用Dify配置的AI流程来实现。Dify的API URL为:https://api.dify.ai/v1 ;API Key为app-croZF0SSV5fiyXbK3neGrOT6 + +5. **关键词展示与确认**:在页面显示提取的关键词,供用户确认和修改 +6. **关键词优化**:用户可添加、删除或修改关键词,形成检索条件,检查并确认检索条件的准确性 + +### 2.3 数据源选择阶段 +7. **检索范围选择**:用户通过按钮触发选择需要在以下范围进行查找,可以多选: + - **内部数据**:企业自有研究数据、历史回复、内部文献 + - **知识库**:已整理的企业知识库数据 + - **知网**:中国知网数据库 + - **ClinicalTrials**:临床试验数据库 + +### 2.4 信息检索与结果处理阶段 +8. **信息检索**:根据选择的数据源和构建好的检索条件进行检索 +9. **结果展示**:根据每一数据源分别在页面显示检索结果列表 +10. **结果筛选**:用户对每条记录进行判断和处理: + - **错误信息**:标记为错误并删除 + - **纳入回复参考资料**:选择是否纳入最终回复 + - **下载全文**:选择是否需要下载全文 + +### 2.5 文献下载管理阶段 +11. **下载任务生成**:对需要下载全文的文献生成下载链接 +12. **待下载页面**:在专门的待下载全文页面显示所有下载任务 +13. **自动化下载**:后续开发自动化工具(联合使用RPA工具影刀)读取待下载任务并执行下载 + +### 2.6 智能分析与回复生成阶段 +14. **资料整合**:将用户勾选的文件和资料进行整合 +15. **AI智能分析**:通过调用AI智能体阅读和分析所有选定资料(AI智能体的API地址与key的信息将写到代码中) +16. **回复生成**:形成与待回答问题相关的标准化回复,包含: + - **问题**:原始查询问题 + - **查询到的资料**:相关资料的简要说明 + - **总结**:基于资料的核心观点总结 + - **详细的资料列表**:完整的参考资料清单 + +### 2.7 审核与发送阶段 +17. **内容审核**:医学信息人员对生成的回复进行审核 +18. **最终确认**:确认无误后发送给客户 + +### 2.8 处理状态显示 +19. **任务状态**:查询任务的状态包括, + - 创建成功 (查询创建后则状态自动标记为创建成功) + - 关键词提取 + - 检索范围确定 + - 检索结果预览 + - 生成回复 + - 修改回复 + - 下载文献 + - 发送回复 + +### 2.9 状态机与流程编排(按钮驱动) +为保证流程可视、可控、可追踪,系统引入显式状态机。每个状态通过页面上的“主要动作按钮”触发进入下一状态;也支持必要的回退。 + +- **状态定义(State)**: + - 创建成功 (CREATED) + - 关键词提取 (KEYWORD_EXTRACTION) + - 检索范围确定 (SCOPE_CONFIRMED) + - 检索结果预览 (RESULTS_REVIEW) + - 下载文献 (DOWNLOAD_PENDING) + - 生成回复 (DRAFT_GENERATION) + - 修改回复 (REVISION) + - 发送回复 (SENT) + +- **核心转移(Transitions)及触发按钮(Action)**: + - CREATED → KEYWORD_EXTRACTION:按钮“开始提取关键词” + - KEYWORD_EXTRACTION → SCOPE_CONFIRMED:按钮“确认关键词并选择数据源” + - SCOPE_CONFIRMED → RESULTS_REVIEW:按钮“开始检索” + - RESULTS_REVIEW → DOWNLOAD_PENDING:按钮“生成下载任务”(当勾选需下载全文的文献) + - RESULTS_REVIEW → DRAFT_GENERATION:按钮“生成草稿回复”(当已选定参考资料) + - DOWNLOAD_PENDING → RESULTS_REVIEW:按钮“刷新下载结果并回到预览”(下载完成后复核) + - DRAFT_GENERATION → REVISION:按钮“进入编辑修改” + - REVISION → SENT:按钮“发送最终回复” + - 允许回退: + - RESULTS_REVIEW → SCOPE_CONFIRMED:按钮“返回调整检索范围” + - REVISION → RESULTS_REVIEW:按钮“返回结果预览” + +- **前端页面与按钮布局建议**: + - 咨询详情页(`InquiryDetail.vue`)顶部固定“状态徽标 + 主动作按钮”区,底部提供上下文次要操作。 + - 不同状态下展示不同的主要按钮,保持单一、明确的下一步。 + - 当必需输入或选择未完成时,主要按钮置灰并给出就绪条件提示。 + +- **后端状态管理约束**: + - 单一来源:后端持久化字段记录任务状态,前端仅展示并通过 API 触发转移。 + - 转移校验:后端校验当前状态是否允许进入目标状态;对缺失前置条件(如未选资料却生成草稿)返回 4xx 并附带原因。 + +### 2.10 文本流程图(Mermaid) +可在支持 Mermaid 的查看器中直接渲染: + +```mermaid +flowchart TD + CREATED[创建成功] -->|开始提取关键词| KEYWORD[关键词提取] + KEYWORD -->|确认关键词并选择数据源| SCOPE[检索范围确定] + SCOPE -->|开始检索| PREVIEW[检索结果预览] + PREVIEW -->|生成下载任务| DL[下载文献] + DL -->|刷新下载结果并回到预览| PREVIEW + PREVIEW -->|生成草稿回复| DRAFT[生成回复] + DRAFT -->|进入编辑修改| REV[修改回复] + REV -->|发送最终回复| SENT[发送回复] + + PREVIEW -->|返回调整检索范围| SCOPE + REV -->|返回结果预览| PREVIEW +``` + +### 2.11 API 与前端动作映射(概要) +- **开始提取关键词**: + - 前端:在 `InquiryDetail.vue` 调用 `POST /api/inquiry/{id}/keywords/extract` + - 成功:后端更新为 KEYWORD_EXTRACTION 或直接返回关键词,前端进入关键词确认 UI +- **确认关键词并选择数据源**: + - 前端:`POST /api/inquiry/{id}/scope` 提交关键词与数据源清单 + - 成功:后端更新为 SCOPE_CONFIRMED +- **开始检索**: + - 前端:`POST /api/inquiry/{id}/search`(异步) + - 成功:后端更新为 RESULTS_REVIEW,并可分页拉取结果 `GET /api/inquiry/{id}/results` +- **生成下载任务**: + - 前端:`POST /api/inquiry/{id}/downloads` 提交选中的条目 ID + - 成功:后端更新为 DOWNLOAD_PENDING;下载进度 `GET /api/download-tasks` +- **生成草稿回复**: + - 前端:`POST /api/inquiry/{id}/response/draft` 提交选定资料引用 + - 成功:后端更新为 DRAFT_GENERATION +- **进入编辑修改**: + - 前端:进入富文本/结构化编辑 UI,保存 `PUT /api/inquiry/{id}/response` + - 成功:后端更新为 REVISION +- **发送最终回复**: + - 前端:`POST /api/inquiry/{id}/send` + - 成功:后端更新为 SENT,记录审计日志 + + + +## 3. 功能模块详细设计 + +### 3.1 用户管理模块 +- **用户注册/登录**:支持多种角色(医学信息专员、管理员等) +- **权限管理**:基于角色的访问控制 +- **用户信息管理**:个人资料维护 + +### 3.2 咨询需求管理模块 +- **需求创建**:支持手动填写查询需求 +- **需求列表**:查看所有咨询需求 +- **需求详情**:查看具体需求信息 +- **需求状态管理**:跟踪需求处理状态 + +### 3.3 智能关键词提取模块 +- **AI关键词提取**:基于Dify API的智能提取 +- **关键词编辑**:用户可修改、添加、删除关键词 +- **关键词历史**:保存历史提取记录 +- **关键词模板**:常用关键词模板管理 + +### 3.4 数据源管理模块 +- **数据源配置**:配置各种数据源的连接信息 +- **检索策略**:定义检索顺序和策略 +- **数据源状态监控**:监控各数据源可用性 + +### 3.5 信息检索模块 +- **多源检索**:支持内部数据、知识库、知网、ClinicalTrials等 +- **检索结果管理**:结果展示、筛选、排序 +- **检索历史**:保存检索记录和结果 + +### 3.6 文献下载管理模块 +- **下载任务管理**:创建、查看、管理下载任务 +- **下载状态跟踪**:实时跟踪下载进度 +- **文件存储管理**:管理下载的文件 +- **下载配置**:配置下载账号和路径 + +### 3.7 AI智能分析模块 +- **文档阅读**:AI智能体阅读选定资料 +- **内容分析**:提取关键信息和观点 +- **回复生成**:生成标准化回复内容 +- **格式规范**:确保回复格式符合要求 + +### 3.8 知识库管理模块 +- **知识库配置**:管理各类知识库连接 +- **内容管理**:添加、编辑、删除知识库内容 +- **分类管理**:知识库内容分类和标签 +- **搜索功能**:知识库内容搜索 + +### 3.9 系统配置模块 +- **API配置**:配置Dify和其他API密钥(DIFY_API_URL=https://api.dify.ai/v1 +DIFY_API_KEY=your-dify-api-key-here) +- **系统参数**:配置系统运行参数 +- **数据源配置**:配置各种数据源连接 +- **下载配置**:配置文献下载相关参数 + +## 4. 技术架构要求 + +### 4.1 开发技术栈 +- **后端**:Java + Spring Boot +- **前端**:Vue3 +- **数据库**:MySQL 8.0+ +- **AI引擎**:Dify API + +### 4.2 系统架构 +- **架构模式**:前后端分离 +- **API设计**:RESTful API +- **安全认证**:JWT Token +- **文件存储**:本地存储 + +### 4.3 部署要求 +- **容器化**:支持Docker部署 +- **环境配置**:支持多环境配置 +- **监控日志**:集成日志和监控系统 + +## 5. 数据源配置要求 + +### 5.1 内部数据源 +- **企业研究数据**:内部研究项目数据 +- **历史回复数据**:过往咨询回复记录 +- **内部文献**:企业内部技术文档和报告 + +### 5.2 公开数据源 +- **监管机构网站**:FDA、EMA等监管数据 +- **知网**:中国知网学术文献 +- **PubMed**:医学文献数据库 +- **EMBASE**:生物医学文献数据库 +- **ClinicalTrials.gov**:临床试验数据库 + +### 5.3 扩展数据源 +- **疾病数据**:疾病相关信息数据库 +- **药物数据**:药物信息数据库 +- **关联数据**:疾病-药物关联数据 + +## 6. 界面设计要求 + +### 6.1 整体设计原则 +- **用户友好**:界面简洁直观,操作便捷 +- **响应式设计**:支持多种屏幕尺寸 +- **一致性**:保持界面风格统一 + +### 6.2 关键页面设计 +- **咨询需求创建页面**:简化表单,突出必填项 +- **关键词确认页面**:清晰展示提取结果,支持编辑 +- **数据源选择页面**:直观的复选框选择界面 +- **检索结果页面**:列表展示,支持批量操作 +- **待下载页面**:任务列表,支持批量下载 +- **回复生成页面**:分步骤展示生成过程 + +## 7. 非功能性需求 + +### 7.1 性能要求 +- **响应时间**:页面加载时间 < 3秒 +- **并发处理**:支持多用户同时操作 +- **数据处理**:支持大量文献数据处理 + +### 7.2 安全要求 +- **数据安全**:敏感数据加密存储 +- **访问控制**:基于角色的权限管理 +- **审计日志**:记录所有操作日志 + +### 7.3 可维护性 +- **代码规范**:遵循Java和Vue开发规范 +- **文档完整**:提供完整的API和用户文档 +- **模块化设计**:便于功能扩展和维护 + + diff --git a/backend/Dockerfile b/backend/Dockerfile deleted file mode 100644 index eaa7499..0000000 --- a/backend/Dockerfile +++ /dev/null @@ -1,31 +0,0 @@ -# 使用Maven构建 -FROM maven:3.9-eclipse-temurin-17 AS build -WORKDIR /app - -# 复制pom.xml并下载依赖(利用Docker缓存) -COPY pom.xml . -RUN mvn dependency:go-offline -B - -# 复制源代码并构建 -COPY src ./src -RUN mvn clean package -DskipTests - -# 运行阶段 -FROM eclipse-temurin:17-jre -WORKDIR /app - -# 复制构建好的jar包 -COPY --from=build /app/target/*.jar app.jar - -# 创建必要的目录 -RUN mkdir -p /app/downloads /app/logs - -# 暴露端口 -EXPOSE 8080 - -# 运行应用 -ENTRYPOINT ["java", "-jar", "-Dspring.profiles.active=${SPRING_PROFILES_ACTIVE:-prod}", "app.jar"] - - - - diff --git a/backend/package-lock.json b/backend/package-lock.json deleted file mode 100644 index dfb18f1..0000000 --- a/backend/package-lock.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "name": "backend", - "lockfileVersion": 3, - "requires": true, - "packages": {} -} diff --git a/backend/pom-backup.xml b/backend/pom-backup.xml deleted file mode 100644 index 30fc460..0000000 --- a/backend/pom-backup.xml +++ /dev/null @@ -1,129 +0,0 @@ - - - 4.0.0 - - - org.springframework.boot - spring-boot-starter-parent - 2.7.18 - - - - com.ipsen - medical-info-system - 1.0.0 - Medical Information Support System - Medical Information Support System for Ipsen - - - 8 - UTF-8 - - - - - - org.springframework.boot - spring-boot-starter-web - - - - - org.springframework.boot - spring-boot-starter-data-jpa - - - - - org.springframework.boot - spring-boot-starter-validation - - - - - org.springframework.boot - spring-boot-starter-security - - - - - mysql - mysql-connector-java - 8.0.33 - runtime - - - - - org.projectlombok - lombok - true - - - - - io.jsonwebtoken - jjwt-api - 0.11.5 - - - io.jsonwebtoken - jjwt-impl - 0.11.5 - runtime - - - io.jsonwebtoken - jjwt-jackson - 0.11.5 - runtime - - - - - org.apache.poi - poi-ooxml - 5.2.5 - - - - - org.springframework.boot - spring-boot-starter-webflux - - - - - com.fasterxml.jackson.core - jackson-databind - - - - - org.springframework.boot - spring-boot-starter-test - test - - - - - - - org.springframework.boot - spring-boot-maven-plugin - - - - org.projectlombok - lombok - - - - - - - - diff --git a/backend/pom-java8.xml b/backend/pom-java8.xml deleted file mode 100644 index c6ca1d4..0000000 --- a/backend/pom-java8.xml +++ /dev/null @@ -1,130 +0,0 @@ - - - 4.0.0 - - - org.springframework.boot - spring-boot-starter-parent - 2.7.18 - - - - com.ipsen - medical-info-system - 1.0.0 - Medical Information Support System - Medical Information Support System for Ipsen - - - 8 - UTF-8 - - - - - - org.springframework.boot - spring-boot-starter-web - - - - - org.springframework.boot - spring-boot-starter-data-jpa - - - - - org.springframework.boot - spring-boot-starter-validation - - - - - org.springframework.boot - spring-boot-starter-security - - - - - mysql - mysql-connector-java - 8.0.33 - runtime - - - - - org.projectlombok - lombok - true - - - - - io.jsonwebtoken - jjwt-api - 0.11.5 - - - io.jsonwebtoken - jjwt-impl - 0.11.5 - runtime - - - io.jsonwebtoken - jjwt-jackson - 0.11.5 - runtime - - - - - org.apache.poi - poi-ooxml - 5.2.5 - - - - - org.springframework.boot - spring-boot-starter-webflux - - - - - com.fasterxml.jackson.core - jackson-databind - - - - - org.springframework.boot - spring-boot-starter-test - test - - - - - - - org.springframework.boot - spring-boot-maven-plugin - - - - org.projectlombok - lombok - - - - - - - - - diff --git a/backend/src/main/java/com/ipsen/medical/controller/DownloadTaskController.java b/backend/src/main/java/com/ipsen/medical/controller/DownloadTaskController.java new file mode 100644 index 0000000..3dc61a1 --- /dev/null +++ b/backend/src/main/java/com/ipsen/medical/controller/DownloadTaskController.java @@ -0,0 +1,75 @@ +package com.ipsen.medical.controller; + +import com.ipsen.medical.dto.ApiResponse; +import com.ipsen.medical.dto.SearchResultItemDTO; +import com.ipsen.medical.service.SearchResultService; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +/** + * 下载任务控制器 + * 用于管理文献和资料的下载任务 + */ +@RestController +@RequestMapping("/download-tasks") +@RequiredArgsConstructor +@CrossOrigin(origins = "*") +public class DownloadTaskController { + + private final SearchResultService searchResultService; + + /** + * 获取所有待下载的任务 + */ + @GetMapping("/pending") + public ResponseEntity>> getAllPendingDownloads() { + List tasks = searchResultService.getPendingDownloads(); + return ResponseEntity.ok(ApiResponse.success(tasks)); + } + + /** + * 获取特定查询请求的待下载任务 + */ + @GetMapping("/inquiry/{inquiryId}/pending") + public ResponseEntity>> getPendingDownloadsByInquiry( + @PathVariable Long inquiryId) { + List tasks = searchResultService.getPendingDownloadsByInquiry(inquiryId); + return ResponseEntity.ok(ApiResponse.success(tasks)); + } + + /** + * 标记下载任务为完成 + */ + @PostMapping("/{id}/complete") + public ResponseEntity> markDownloadComplete( + @PathVariable Long id, + @RequestParam String filePath) { + SearchResultItemDTO dto = new SearchResultItemDTO(); + dto.setId(id); + dto.setDownloadStatus("COMPLETED"); + dto.setFilePath(filePath); + + SearchResultItemDTO result = searchResultService.updateSearchResult(id, dto); + return ResponseEntity.ok(ApiResponse.success(result)); + } + + /** + * 标记下载任务为失败 + */ + @PostMapping("/{id}/fail") + public ResponseEntity> markDownloadFailed( + @PathVariable Long id, + @RequestParam(required = false) String reason) { + SearchResultItemDTO dto = new SearchResultItemDTO(); + dto.setId(id); + dto.setDownloadStatus("FAILED"); + + SearchResultItemDTO result = searchResultService.updateSearchResult(id, dto); + return ResponseEntity.ok(ApiResponse.success(result)); + } +} + + diff --git a/backend/src/main/java/com/ipsen/medical/controller/InquiryController.java b/backend/src/main/java/com/ipsen/medical/controller/InquiryController.java index f8e3dea..6af3ff1 100644 --- a/backend/src/main/java/com/ipsen/medical/controller/InquiryController.java +++ b/backend/src/main/java/com/ipsen/medical/controller/InquiryController.java @@ -2,7 +2,9 @@ package com.ipsen.medical.controller; import com.ipsen.medical.dto.ApiResponse; import com.ipsen.medical.dto.ClinicalTrialDTO; +import com.ipsen.medical.dto.DataSourceSelectionDTO; import com.ipsen.medical.dto.InquiryRequestDTO; +import com.ipsen.medical.dto.KeywordConfirmationDTO; import com.ipsen.medical.service.AutoSearchService; import com.ipsen.medical.service.ClinicalTrialsService; import com.ipsen.medical.service.InquiryService; @@ -76,6 +78,28 @@ public class InquiryController { return ResponseEntity.ok(ApiResponse.success(result)); } + /** + * 确认和更新关键词 + */ + @PostMapping("/{id}/confirm-keywords") + public ResponseEntity> confirmKeywords( + @PathVariable Long id, + @RequestBody KeywordConfirmationDTO keywordDTO) { + InquiryRequestDTO result = inquiryService.confirmKeywords(id, keywordDTO); + return ResponseEntity.ok(ApiResponse.success(result)); + } + + /** + * 选择数据源 + */ + @PostMapping("/{id}/select-data-sources") + public ResponseEntity> selectDataSources( + @PathVariable Long id, + @RequestBody DataSourceSelectionDTO dataSourceDTO) { + InquiryRequestDTO result = inquiryService.selectDataSources(id, dataSourceDTO); + return ResponseEntity.ok(ApiResponse.success(result)); + } + /** * 执行信息检索 */ diff --git a/backend/src/main/java/com/ipsen/medical/controller/SearchResultController.java b/backend/src/main/java/com/ipsen/medical/controller/SearchResultController.java new file mode 100644 index 0000000..63d3996 --- /dev/null +++ b/backend/src/main/java/com/ipsen/medical/controller/SearchResultController.java @@ -0,0 +1,105 @@ +package com.ipsen.medical.controller; + +import com.ipsen.medical.dto.ApiResponse; +import com.ipsen.medical.dto.SearchResultItemDTO; +import com.ipsen.medical.service.SearchResultService; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +/** + * 检索结果控制器 + */ +@RestController +@RequestMapping("/search-results") +@RequiredArgsConstructor +@CrossOrigin(origins = "*") +public class SearchResultController { + + private final SearchResultService searchResultService; + + /** + * 获取查询请求的所有检索结果 + */ + @GetMapping("/inquiry/{inquiryId}") + public ResponseEntity>> getSearchResults( + @PathVariable Long inquiryId) { + List results = searchResultService.getActiveSearchResults(inquiryId); + return ResponseEntity.ok(ApiResponse.success(results)); + } + + /** + * 更新检索结果项 + */ + @PutMapping("/{id}") + public ResponseEntity> updateSearchResult( + @PathVariable Long id, + @RequestBody SearchResultItemDTO dto) { + SearchResultItemDTO result = searchResultService.updateSearchResult(id, dto); + return ResponseEntity.ok(ApiResponse.success(result)); + } + + /** + * 批量更新检索结果项 + */ + @PutMapping("/batch") + public ResponseEntity>> batchUpdateSearchResults( + @RequestBody List dtos) { + List results = searchResultService.batchUpdateSearchResults(dtos); + return ResponseEntity.ok(ApiResponse.success(results)); + } + + /** + * 标记为错误并删除 + */ + @DeleteMapping("/{id}") + public ResponseEntity> markAsDeleted(@PathVariable Long id) { + searchResultService.markAsDeleted(id); + return ResponseEntity.ok(ApiResponse.success(null)); + } + + /** + * 设置是否纳入回复参考资料 + */ + @PostMapping("/{id}/include") + public ResponseEntity> setIncludeInResponse( + @PathVariable Long id, + @RequestParam Boolean include) { + SearchResultItemDTO result = searchResultService.setIncludeInResponse(id, include); + return ResponseEntity.ok(ApiResponse.success(result)); + } + + /** + * 设置是否需要下载全文 + */ + @PostMapping("/{id}/download") + public ResponseEntity> setNeedDownload( + @PathVariable Long id, + @RequestParam Boolean need) { + SearchResultItemDTO result = searchResultService.setNeedDownload(id, need); + return ResponseEntity.ok(ApiResponse.success(result)); + } + + /** + * 获取所有待下载的结果项 + */ + @GetMapping("/pending-downloads") + public ResponseEntity>> getPendingDownloads() { + List results = searchResultService.getPendingDownloads(); + return ResponseEntity.ok(ApiResponse.success(results)); + } + + /** + * 获取查询请求的待下载结果项 + */ + @GetMapping("/inquiry/{inquiryId}/pending-downloads") + public ResponseEntity>> getPendingDownloadsByInquiry( + @PathVariable Long inquiryId) { + List results = searchResultService.getPendingDownloadsByInquiry(inquiryId); + return ResponseEntity.ok(ApiResponse.success(results)); + } +} + + diff --git a/backend/src/main/java/com/ipsen/medical/controller/TestController.java b/backend/src/main/java/com/ipsen/medical/controller/TestController.java index bbe48e6..09b7851 100644 --- a/backend/src/main/java/com/ipsen/medical/controller/TestController.java +++ b/backend/src/main/java/com/ipsen/medical/controller/TestController.java @@ -1,10 +1,11 @@ package com.ipsen.medical.controller; -import org.springframework.web.bind.annotation.CrossOrigin; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; +import com.ipsen.medical.dto.ApiResponse; +import com.ipsen.medical.service.DifyService; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.*; +import java.lang.reflect.Field; import java.util.HashMap; import java.util.Map; @@ -14,8 +15,11 @@ import java.util.Map; @RestController @RequestMapping("/test") @CrossOrigin(origins = "*") +@RequiredArgsConstructor public class TestController { + private final DifyService difyService; + @GetMapping("/health") public Map health() { Map response = new HashMap<>(); @@ -34,4 +38,43 @@ public class TestController { response.put("java.home", System.getProperty("java.home")); return response; } + + @GetMapping("/dify-connection") + public ApiResponse testDifyConnection() { + try { + String result = difyService.extractKeywords("测试连接"); + return ApiResponse.success("Dify API连接成功: " + result); + } catch (Exception e) { + return ApiResponse.error("Dify API连接失败: " + e.getMessage()); + } + } + + @GetMapping("/dify-config") + public ApiResponse> checkDifyConfig() { + Map config = new HashMap<>(); + + // 通过反射获取配置值 + try { + Field apiUrlField = difyService.getClass().getDeclaredField("difyApiUrl"); + Field apiKeyField = difyService.getClass().getDeclaredField("difyApiKey"); + + apiUrlField.setAccessible(true); + apiKeyField.setAccessible(true); + + String apiUrl = (String) apiUrlField.get(difyService); + String apiKey = (String) apiKeyField.get(difyService); + + config.put("apiUrl", apiUrl); + config.put("apiKeyConfigured", apiKey != null && !apiKey.equals("your-dify-api-key-here") && !apiKey.trim().isEmpty()); + config.put("apiKeyPreview", apiKey != null ? apiKey.substring(0, Math.min(10, apiKey.length())) + "..." : "null"); + boolean apiKeyConfigured = (Boolean) config.get("apiKeyConfigured"); + config.put("status", apiKeyConfigured ? "OK" : "NEEDS_CONFIGURATION"); + + } catch (Exception e) { + config.put("error", "无法获取配置信息: " + e.getMessage()); + config.put("status", "ERROR"); + } + + return ApiResponse.success(config); + } } diff --git a/backend/src/main/java/com/ipsen/medical/dto/ClinicalTrialDTO.java b/backend/src/main/java/com/ipsen/medical/dto/ClinicalTrialDTO.java index e66e290..587da0d 100644 --- a/backend/src/main/java/com/ipsen/medical/dto/ClinicalTrialDTO.java +++ b/backend/src/main/java/com/ipsen/medical/dto/ClinicalTrialDTO.java @@ -48,3 +48,5 @@ public class ClinicalTrialDTO { + + diff --git a/backend/src/main/java/com/ipsen/medical/dto/ClinicalTrialsSearchResult.java b/backend/src/main/java/com/ipsen/medical/dto/ClinicalTrialsSearchResult.java index 42673df..db27f02 100644 --- a/backend/src/main/java/com/ipsen/medical/dto/ClinicalTrialsSearchResult.java +++ b/backend/src/main/java/com/ipsen/medical/dto/ClinicalTrialsSearchResult.java @@ -27,3 +27,5 @@ public class ClinicalTrialsSearchResult { + + diff --git a/backend/src/main/java/com/ipsen/medical/dto/DataSourceSelectionDTO.java b/backend/src/main/java/com/ipsen/medical/dto/DataSourceSelectionDTO.java new file mode 100644 index 0000000..18a44ce --- /dev/null +++ b/backend/src/main/java/com/ipsen/medical/dto/DataSourceSelectionDTO.java @@ -0,0 +1,16 @@ +package com.ipsen.medical.dto; + +import lombok.Data; + +/** + * 数据源选择DTO + */ +@Data +public class DataSourceSelectionDTO { + private Boolean searchInternalData; // 是否在内部数据中检索 + private Boolean searchKnowledgeBase; // 是否在知识库中检索 + private Boolean searchCnki; // 是否在知网中检索 + private Boolean searchClinicalTrials; // 是否在ClinicalTrials中检索 +} + + diff --git a/backend/src/main/java/com/ipsen/medical/dto/InquiryRequestDTO.java b/backend/src/main/java/com/ipsen/medical/dto/InquiryRequestDTO.java index 716af24..327ee51 100644 --- a/backend/src/main/java/com/ipsen/medical/dto/InquiryRequestDTO.java +++ b/backend/src/main/java/com/ipsen/medical/dto/InquiryRequestDTO.java @@ -12,6 +12,11 @@ public class InquiryRequestDTO { private String customerTitle; private String inquiryContent; private String keywords; + private Boolean keywordsConfirmed; + private Boolean searchInternalData; + private Boolean searchKnowledgeBase; + private Boolean searchCnki; + private Boolean searchClinicalTrials; private String status; private String searchResults; private String responseContent; diff --git a/backend/src/main/java/com/ipsen/medical/dto/KeywordConfirmationDTO.java b/backend/src/main/java/com/ipsen/medical/dto/KeywordConfirmationDTO.java new file mode 100644 index 0000000..fa97466 --- /dev/null +++ b/backend/src/main/java/com/ipsen/medical/dto/KeywordConfirmationDTO.java @@ -0,0 +1,17 @@ +package com.ipsen.medical.dto; + +import lombok.Data; +import java.util.List; + +/** + * 关键词确认DTO + */ +@Data +public class KeywordConfirmationDTO { + private String drugNameChinese; + private String drugNameEnglish; + private String requestItem; + private List additionalKeywords; // 用户添加的其他关键词 +} + + diff --git a/backend/src/main/java/com/ipsen/medical/dto/ResponseGenerationDTO.java b/backend/src/main/java/com/ipsen/medical/dto/ResponseGenerationDTO.java new file mode 100644 index 0000000..093cbad --- /dev/null +++ b/backend/src/main/java/com/ipsen/medical/dto/ResponseGenerationDTO.java @@ -0,0 +1,32 @@ +package com.ipsen.medical.dto; + +import lombok.Data; +import java.util.List; + +/** + * 回复生成DTO + * 包含标准化的回复格式 + */ +@Data +public class ResponseGenerationDTO { + private String question; // 原始查询问题 + private String queriedMaterials; // 查询到的资料简要说明 + private String summary; // 基于资料的核心观点总结 + private List materialList; // 详细的资料列表 + + /** + * 资料引用 + */ + @Data + public static class MaterialReference { + private String title; // 标题 + private String authors; // 作者 + private String source; // 来源 + private String publicationDate; // 发表日期 + private String url; // 链接 + private String summary; // 摘要 + private String relevance; // 相关性说明 + } +} + + diff --git a/backend/src/main/java/com/ipsen/medical/dto/SearchResultItemDTO.java b/backend/src/main/java/com/ipsen/medical/dto/SearchResultItemDTO.java new file mode 100644 index 0000000..7f9fa8a --- /dev/null +++ b/backend/src/main/java/com/ipsen/medical/dto/SearchResultItemDTO.java @@ -0,0 +1,35 @@ +package com.ipsen.medical.dto; + +import lombok.Data; +import java.time.LocalDateTime; + +/** + * 检索结果项DTO + */ +@Data +public class SearchResultItemDTO { + private Long id; + private Long inquiryRequestId; + private String title; + private String summary; + private String content; + private String authors; + private String source; + private String sourceUrl; + private String publicationDate; + private String doi; + private String pmid; + private String nctId; + private String metadata; + private String status; + private Boolean includeInResponse; + private Boolean needDownload; + private Boolean isDeleted; + private String filePath; + private String downloadStatus; + private LocalDateTime createdAt; + private LocalDateTime updatedAt; + private LocalDateTime downloadedAt; +} + + diff --git a/backend/src/main/java/com/ipsen/medical/dto/UserDTO.java b/backend/src/main/java/com/ipsen/medical/dto/UserDTO.java new file mode 100644 index 0000000..5d5873d --- /dev/null +++ b/backend/src/main/java/com/ipsen/medical/dto/UserDTO.java @@ -0,0 +1,22 @@ +package com.ipsen.medical.dto; + +import lombok.Data; +import java.time.LocalDateTime; + +/** + * 用户DTO + */ +@Data +public class UserDTO { + private Long id; + private String username; + private String password; // 仅用于创建/更新,不返回 + private String fullName; + private String email; + private String role; + private Boolean enabled; + private LocalDateTime createdAt; + private LocalDateTime lastLoginAt; +} + + diff --git a/backend/src/main/java/com/ipsen/medical/entity/InquiryRequest.java b/backend/src/main/java/com/ipsen/medical/entity/InquiryRequest.java index ea7b0a9..d4f6179 100644 --- a/backend/src/main/java/com/ipsen/medical/entity/InquiryRequest.java +++ b/backend/src/main/java/com/ipsen/medical/entity/InquiryRequest.java @@ -19,19 +19,28 @@ public class InquiryRequest { @Column(nullable = false) private String requestNumber; // 请求编号 - @Column(nullable = false) - private String customerName; // 客户姓名 + private String customerName; // 客户姓名(非必填) - private String customerEmail; // 客户邮箱 + private String customerEmail; // 客户邮箱(非必填) private String customerTitle; // 客户职称 - @Column(columnDefinition = "TEXT") - private String inquiryContent; // 查询内容 + @Column(columnDefinition = "TEXT", nullable = false) + private String inquiryContent; // 查询内容(必填) @Column(columnDefinition = "TEXT") private String keywords; // 提取的关键词(JSON格式) + private Boolean keywordsConfirmed; // 用户是否确认关键词 + + private Boolean searchInternalData; // 是否在内部数据中检索 + + private Boolean searchKnowledgeBase; // 是否在知识库中检索 + + private Boolean searchCnki; // 是否在知网中检索 + + private Boolean searchClinicalTrials; // 是否在ClinicalTrials中检索 + @Enumerated(EnumType.STRING) @Column(nullable = false) private RequestStatus status; // 状态 diff --git a/backend/src/main/java/com/ipsen/medical/entity/SearchResultItem.java b/backend/src/main/java/com/ipsen/medical/entity/SearchResultItem.java new file mode 100644 index 0000000..3cf30e9 --- /dev/null +++ b/backend/src/main/java/com/ipsen/medical/entity/SearchResultItem.java @@ -0,0 +1,113 @@ +package com.ipsen.medical.entity; + +import javax.persistence.*; +import lombok.Data; +import java.time.LocalDateTime; + +/** + * 检索结果项实体 + * 用于存储单条检索结果,支持用户对每条结果进行标记和处理 + */ +@Data +@Entity +@Table(name = "search_result_items") +public class SearchResultItem { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne + @JoinColumn(name = "inquiry_request_id", nullable = false) + private InquiryRequest inquiryRequest; + + @Column(nullable = false) + private String title; // 标题 + + @Column(columnDefinition = "TEXT") + private String summary; // 摘要 + + @Column(columnDefinition = "TEXT") + private String content; // 内容 + + private String authors; // 作者 + + private String source; // 来源(知网、ClinicalTrials、知识库等) + + private String sourceUrl; // 来源URL + + private String publicationDate; // 发表日期 + + private String doi; // DOI + + private String pmid; // PubMed ID + + private String nctId; // ClinicalTrials NCT ID + + @Column(columnDefinition = "TEXT") + private String metadata; // 其他元数据(JSON格式) + + @Enumerated(EnumType.STRING) + @Column(nullable = false) + private ResultStatus status; // 结果状态 + + private Boolean includeInResponse; // 是否纳入回复参考资料 + + private Boolean needDownload; // 是否需要下载全文 + + private Boolean isDeleted; // 是否标记为错误并删除 + + private String filePath; // 下载后的文件路径 + + @Enumerated(EnumType.STRING) + private DownloadStatus downloadStatus; // 下载状态 + + @Column(nullable = false) + private LocalDateTime createdAt; + + private LocalDateTime updatedAt; + + private LocalDateTime downloadedAt; + + @PrePersist + protected void onCreate() { + createdAt = LocalDateTime.now(); + if (includeInResponse == null) { + includeInResponse = false; + } + if (needDownload == null) { + needDownload = false; + } + if (isDeleted == null) { + isDeleted = false; + } + if (status == null) { + status = ResultStatus.PENDING_REVIEW; + } + if (downloadStatus == null) { + downloadStatus = DownloadStatus.NOT_REQUIRED; + } + } + + @PreUpdate + protected void onUpdate() { + updatedAt = LocalDateTime.now(); + } + + public enum ResultStatus { + PENDING_REVIEW, // 待审核 + APPROVED, // 已批准 + REJECTED, // 已拒绝 + DELETED // 已删除(错误信息) + } + + public enum DownloadStatus { + NOT_REQUIRED, // 不需要下载 + PENDING, // 待下载 + DOWNLOADING, // 下载中 + COMPLETED, // 已完成 + FAILED // 失败 + } +} + + diff --git a/backend/src/main/java/com/ipsen/medical/repository/ClinicalTrialRepository.java b/backend/src/main/java/com/ipsen/medical/repository/ClinicalTrialRepository.java index d0e0dd5..d4d36b3 100644 --- a/backend/src/main/java/com/ipsen/medical/repository/ClinicalTrialRepository.java +++ b/backend/src/main/java/com/ipsen/medical/repository/ClinicalTrialRepository.java @@ -37,3 +37,5 @@ public interface ClinicalTrialRepository extends JpaRepository { + + /** + * 根据查询请求ID查找所有结果项 + */ + List findByInquiryRequestId(Long inquiryRequestId); + + /** + * 根据查询请求ID查找需要纳入回复的结果项 + */ + List findByInquiryRequestIdAndIncludeInResponseTrue(Long inquiryRequestId); + + /** + * 根据查询请求ID查找需要下载的结果项 + */ + List findByInquiryRequestIdAndNeedDownloadTrue(Long inquiryRequestId); + + /** + * 根据查询请求ID查找未删除的结果项 + */ + List findByInquiryRequestIdAndIsDeletedFalse(Long inquiryRequestId); + + /** + * 根据查询请求ID删除所有结果项 + */ + void deleteByInquiryRequestId(Long inquiryRequestId); + + /** + * 查找所有待下载的结果项 + */ + List findByDownloadStatus(SearchResultItem.DownloadStatus status); +} + + diff --git a/backend/src/main/java/com/ipsen/medical/service/ClinicalTrialsService.java b/backend/src/main/java/com/ipsen/medical/service/ClinicalTrialsService.java index d9f6753..3bac9ac 100644 --- a/backend/src/main/java/com/ipsen/medical/service/ClinicalTrialsService.java +++ b/backend/src/main/java/com/ipsen/medical/service/ClinicalTrialsService.java @@ -50,3 +50,5 @@ public interface ClinicalTrialsService { + + diff --git a/backend/src/main/java/com/ipsen/medical/service/DifyService.java b/backend/src/main/java/com/ipsen/medical/service/DifyService.java index 39678c5..a794b8c 100644 --- a/backend/src/main/java/com/ipsen/medical/service/DifyService.java +++ b/backend/src/main/java/com/ipsen/medical/service/DifyService.java @@ -1,6 +1,7 @@ package com.ipsen.medical.service; import com.ipsen.medical.dto.KeywordExtractionResult; +import com.ipsen.medical.dto.ResponseGenerationDTO; /** * Dify AI服务接口 @@ -23,9 +24,14 @@ public interface DifyService { String performSearch(String keywords); /** - * 生成回复 + * 生成回复(返回JSON字符串) */ String generateResponse(String inquiryContent, String searchResults); + + /** + * 生成结构化回复 + */ + ResponseGenerationDTO generateStructuredResponse(String inquiryContent, String selectedMaterials); } diff --git a/backend/src/main/java/com/ipsen/medical/service/InquiryService.java b/backend/src/main/java/com/ipsen/medical/service/InquiryService.java index 910bcb4..d6979aa 100644 --- a/backend/src/main/java/com/ipsen/medical/service/InquiryService.java +++ b/backend/src/main/java/com/ipsen/medical/service/InquiryService.java @@ -1,6 +1,8 @@ package com.ipsen.medical.service; +import com.ipsen.medical.dto.DataSourceSelectionDTO; import com.ipsen.medical.dto.InquiryRequestDTO; +import com.ipsen.medical.dto.KeywordConfirmationDTO; import org.springframework.web.multipart.MultipartFile; import java.util.List; @@ -36,7 +38,17 @@ public interface InquiryService { InquiryRequestDTO extractKeywords(Long id); /** - * 执行信息检索 + * 确认和更新关键词 + */ + InquiryRequestDTO confirmKeywords(Long id, KeywordConfirmationDTO keywordDTO); + + /** + * 选择数据源 + */ + InquiryRequestDTO selectDataSources(Long id, DataSourceSelectionDTO dataSourceDTO); + + /** + * 执行信息检索(基于选择的数据源) */ InquiryRequestDTO performSearch(Long id); diff --git a/backend/src/main/java/com/ipsen/medical/service/MultiSourceSearchService.java b/backend/src/main/java/com/ipsen/medical/service/MultiSourceSearchService.java new file mode 100644 index 0000000..c6bc18e --- /dev/null +++ b/backend/src/main/java/com/ipsen/medical/service/MultiSourceSearchService.java @@ -0,0 +1,41 @@ +package com.ipsen.medical.service; + +import com.ipsen.medical.dto.SearchResultItemDTO; + +import java.util.List; + +/** + * 多源检索服务接口 + * 支持内部数据、知识库、知网、ClinicalTrials等多个数据源的检索 + */ +public interface MultiSourceSearchService { + + /** + * 根据查询请求执行多源检索 + * @param inquiryId 查询请求ID + * @return 检索结果列表 + */ + List performMultiSourceSearch(Long inquiryId); + + /** + * 在内部数据中检索 + */ + List searchInternalData(Long inquiryId, String keywords); + + /** + * 在知识库中检索 + */ + List searchKnowledgeBase(Long inquiryId, String keywords); + + /** + * 在知网中检索 + */ + List searchCnki(Long inquiryId, String keywords); + + /** + * 在ClinicalTrials中检索 + */ + List searchClinicalTrials(Long inquiryId, String keywords); +} + + diff --git a/backend/src/main/java/com/ipsen/medical/service/SearchResultService.java b/backend/src/main/java/com/ipsen/medical/service/SearchResultService.java new file mode 100644 index 0000000..fa4d293 --- /dev/null +++ b/backend/src/main/java/com/ipsen/medical/service/SearchResultService.java @@ -0,0 +1,68 @@ +package com.ipsen.medical.service; + +import com.ipsen.medical.dto.SearchResultItemDTO; + +import java.util.List; + +/** + * 检索结果服务接口 + */ +public interface SearchResultService { + + /** + * 获取查询请求的所有检索结果 + */ + List getSearchResults(Long inquiryRequestId); + + /** + * 获取查询请求的未删除检索结果 + */ + List getActiveSearchResults(Long inquiryRequestId); + + /** + * 更新检索结果项 + */ + SearchResultItemDTO updateSearchResult(Long id, SearchResultItemDTO dto); + + /** + * 批量更新检索结果项 + */ + List batchUpdateSearchResults(List dtos); + + /** + * 标记为错误并删除 + */ + void markAsDeleted(Long id); + + /** + * 设置是否纳入回复参考资料 + */ + SearchResultItemDTO setIncludeInResponse(Long id, Boolean include); + + /** + * 设置是否需要下载全文 + */ + SearchResultItemDTO setNeedDownload(Long id, Boolean need); + + /** + * 获取所有待下载的结果项 + */ + List getPendingDownloads(); + + /** + * 获取查询请求的待下载结果项 + */ + List getPendingDownloadsByInquiry(Long inquiryRequestId); + + /** + * 创建检索结果项 + */ + SearchResultItemDTO createSearchResult(SearchResultItemDTO dto); + + /** + * 批量创建检索结果项 + */ + List batchCreateSearchResults(List dtos); +} + + diff --git a/backend/src/main/java/com/ipsen/medical/service/UserService.java b/backend/src/main/java/com/ipsen/medical/service/UserService.java new file mode 100644 index 0000000..603ebf1 --- /dev/null +++ b/backend/src/main/java/com/ipsen/medical/service/UserService.java @@ -0,0 +1,48 @@ +package com.ipsen.medical.service; + +import com.ipsen.medical.dto.UserDTO; + +import java.util.List; + +/** + * 用户服务接口 + */ +public interface UserService { + + /** + * 创建用户 + */ + UserDTO createUser(UserDTO userDTO); + + /** + * 获取用户 + */ + UserDTO getUser(Long id); + + /** + * 根据用户名获取用户 + */ + UserDTO getUserByUsername(String username); + + /** + * 获取所有用户 + */ + List getAllUsers(); + + /** + * 更新用户 + */ + UserDTO updateUser(Long id, UserDTO userDTO); + + /** + * 删除用户 + */ + void deleteUser(Long id); + + /** + * 启用/禁用用户 + */ + UserDTO toggleUserStatus(Long id, Boolean enabled); +} + + diff --git a/backend/src/main/java/com/ipsen/medical/service/impl/DifyServiceImpl.java b/backend/src/main/java/com/ipsen/medical/service/impl/DifyServiceImpl.java index c652beb..459ae76 100644 --- a/backend/src/main/java/com/ipsen/medical/service/impl/DifyServiceImpl.java +++ b/backend/src/main/java/com/ipsen/medical/service/impl/DifyServiceImpl.java @@ -3,8 +3,8 @@ package com.ipsen.medical.service.impl; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.ipsen.medical.dto.DifyRequest; -import com.ipsen.medical.dto.DifyResponse; import com.ipsen.medical.dto.KeywordExtractionResult; +import com.ipsen.medical.dto.ResponseGenerationDTO; import com.ipsen.medical.service.DifyService; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; @@ -13,6 +13,7 @@ import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; import org.springframework.stereotype.Service; import org.springframework.web.reactive.function.client.WebClient; +import reactor.core.publisher.Mono; import java.util.HashMap; import java.util.Map; @@ -45,6 +46,13 @@ public class DifyServiceImpl implements DifyService { public KeywordExtractionResult extractKeywordsStructured(String content) { log.info("Extracting keywords structured from content"); + // 检查API Key配置 + if (difyApiKey == null || difyApiKey.equals("your-dify-api-key-here") || difyApiKey.trim().isEmpty()) { + log.error("Dify API Key not configured properly. Current value: {}", + difyApiKey != null ? difyApiKey.substring(0, Math.min(20, difyApiKey.length())) + "..." : "null"); + throw new RuntimeException("Dify API Key未配置。请设置环境变量DIFY_API_KEY或修改application.yml中的配置。"); + } + try { String response = callDifyAPI(content, "extract_keywords"); @@ -132,12 +140,66 @@ public class DifyServiceImpl implements DifyService { } } + @Override + public ResponseGenerationDTO generateStructuredResponse(String inquiryContent, String selectedMaterials) { + log.info("Generating structured response"); + + try { + String prompt = String.format( + "请基于以下查询问题和检索到的资料生成标准化回复。\n\n" + + "查询问题:%s\n\n" + + "检索到的资料:%s\n\n" + + "请按照以下JSON格式返回:\n" + + "{\n" + + " \"question\": \"原始查询问题\",\n" + + " \"queriedMaterials\": \"查询到的资料简要说明\",\n" + + " \"summary\": \"基于资料的核心观点总结\",\n" + + " \"materialList\": [\n" + + " {\n" + + " \"title\": \"标题\",\n" + + " \"authors\": \"作者\",\n" + + " \"source\": \"来源\",\n" + + " \"publicationDate\": \"发表日期\",\n" + + " \"url\": \"链接\",\n" + + " \"summary\": \"摘要\",\n" + + " \"relevance\": \"相关性说明\"\n" + + " }\n" + + " ]\n" + + "}", + inquiryContent, selectedMaterials + ); + + String response = callDifyAPI(prompt, "generate_structured_response"); + + // 解析响应 + JsonNode rootNode = objectMapper.readTree(response); + JsonNode answerNode = rootNode.path("answer"); + + String answer; + if (answerNode.isTextual()) { + answer = answerNode.asText(); + } else { + answer = answerNode.toString(); + } + + // 解析结构化回复 + ResponseGenerationDTO result = objectMapper.readValue(answer, ResponseGenerationDTO.class); + + return result; + + } catch (Exception e) { + log.error("Error generating structured response: ", e); + throw new RuntimeException("Failed to generate structured response: " + e.getMessage(), e); + } + } + /** * 调用Dify API */ private String callDifyAPI(String content, String taskType) { try { log.info("Calling Dify API: url={}, taskType={}", difyApiUrl, taskType); + log.info("API Key: {}", difyApiKey != null ? difyApiKey.substring(0, Math.min(10, difyApiKey.length())) + "..." : "null"); // 构建请求 DifyRequest request = new DifyRequest(); @@ -151,6 +213,8 @@ public class DifyServiceImpl implements DifyService { inputs.put("content", content); request.setInputs(inputs); + log.debug("Dify API request: {}", objectMapper.writeValueAsString(request)); + // 调用API String response = webClient.post() .uri(difyApiUrl + "/chat-messages") @@ -158,6 +222,16 @@ public class DifyServiceImpl implements DifyService { .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) .bodyValue(request) .retrieve() + .onStatus(status -> status.is4xxClientError() || status.is5xxServerError(), + clientResponse -> { + log.error("Dify API error: status={}, headers={}", + clientResponse.statusCode(), clientResponse.headers().asHttpHeaders()); + return clientResponse.bodyToMono(String.class) + .flatMap(body -> { + log.error("Dify API error body: {}", body); + return Mono.error(new RuntimeException("Dify API error: " + clientResponse.statusCode() + " - " + body)); + }); + }) .bodyToMono(String.class) .block(); diff --git a/backend/src/main/java/com/ipsen/medical/service/impl/InquiryServiceImpl.java b/backend/src/main/java/com/ipsen/medical/service/impl/InquiryServiceImpl.java index 0303356..1122f6c 100644 --- a/backend/src/main/java/com/ipsen/medical/service/impl/InquiryServiceImpl.java +++ b/backend/src/main/java/com/ipsen/medical/service/impl/InquiryServiceImpl.java @@ -1,10 +1,13 @@ package com.ipsen.medical.service.impl; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.ipsen.medical.dto.DataSourceSelectionDTO; import com.ipsen.medical.dto.InquiryRequestDTO; +import com.ipsen.medical.dto.KeywordConfirmationDTO; +import com.ipsen.medical.dto.ResponseGenerationDTO; +import com.ipsen.medical.dto.SearchResultItemDTO; import com.ipsen.medical.entity.InquiryRequest; -import com.ipsen.medical.entity.User; import com.ipsen.medical.repository.InquiryRequestRepository; -import com.ipsen.medical.repository.UserRepository; import com.ipsen.medical.service.InquiryService; import com.ipsen.medical.service.ExcelParserService; import com.ipsen.medical.service.DifyService; @@ -27,15 +30,20 @@ public class InquiryServiceImpl implements InquiryService { @Autowired private InquiryRequestRepository inquiryRequestRepository; - @Autowired - private UserRepository userRepository; - @Autowired private ExcelParserService excelParserService; @Autowired private DifyService difyService; + @Autowired + private com.ipsen.medical.service.MultiSourceSearchService multiSourceSearchService; + + @Autowired + private com.ipsen.medical.service.SearchResultService searchResultService; + + private final ObjectMapper objectMapper = new ObjectMapper(); + @Override public InquiryRequestDTO uploadInquiry(MultipartFile file) { try { @@ -115,14 +123,14 @@ public class InquiryServiceImpl implements InquiryService { .orElseThrow(() -> new RuntimeException("查询请求不存在: " + id)); try { - // 使用Dify服务执行信息检索 - String searchResults = difyService.performSearch(inquiryRequest.getKeywords()); - inquiryRequest.setSearchResults(searchResults); - inquiryRequest.setStatus(InquiryRequest.RequestStatus.SEARCH_COMPLETED); - inquiryRequest.setUpdatedAt(LocalDateTime.now()); - - InquiryRequest savedRequest = inquiryRequestRepository.save(inquiryRequest); - return convertToDTO(savedRequest); + // 执行多源检索 + multiSourceSearchService.performMultiSourceSearch(id); + + // 重新加载更新后的请求 + inquiryRequest = inquiryRequestRepository.findById(id) + .orElseThrow(() -> new RuntimeException("查询请求不存在: " + id)); + + return convertToDTO(inquiryRequest); } catch (Exception e) { throw new RuntimeException("信息检索失败: " + e.getMessage(), e); } @@ -134,11 +142,27 @@ public class InquiryServiceImpl implements InquiryService { .orElseThrow(() -> new RuntimeException("查询请求不存在: " + id)); try { - // 使用Dify服务生成回复内容 - String responseContent = difyService.generateResponse( + // 获取用户选中的检索结果(includeInResponse = true) + List selectedResults = searchResultService.getSearchResults(id).stream() + .filter(r -> Boolean.TRUE.equals(r.getIncludeInResponse())) + .collect(Collectors.toList()); + + if (selectedResults.isEmpty()) { + throw new RuntimeException("未选择任何参考资料,无法生成回复"); + } + + // 将选中的资料转换为JSON字符串 + String selectedMaterials = objectMapper.writeValueAsString(selectedResults); + + // 使用Dify服务生成结构化回复 + ResponseGenerationDTO structuredResponse = difyService.generateStructuredResponse( inquiryRequest.getInquiryContent(), - inquiryRequest.getSearchResults() + selectedMaterials ); + + // 将结构化回复转换为JSON字符串保存 + String responseContent = objectMapper.writeValueAsString(structuredResponse); + inquiryRequest.setResponseContent(responseContent); inquiryRequest.setStatus(InquiryRequest.RequestStatus.UNDER_REVIEW); inquiryRequest.setUpdatedAt(LocalDateTime.now()); @@ -180,6 +204,41 @@ public class InquiryServiceImpl implements InquiryService { return convertToDTO(savedRequest); } + @Override + public InquiryRequestDTO confirmKeywords(Long id, KeywordConfirmationDTO keywordDTO) { + InquiryRequest inquiryRequest = inquiryRequestRepository.findById(id) + .orElseThrow(() -> new RuntimeException("查询请求不存在: " + id)); + + try { + // 将用户确认的关键词保存为JSON + String keywordsJson = objectMapper.writeValueAsString(keywordDTO); + inquiryRequest.setKeywords(keywordsJson); + inquiryRequest.setKeywordsConfirmed(true); + inquiryRequest.setUpdatedAt(LocalDateTime.now()); + + InquiryRequest savedRequest = inquiryRequestRepository.save(inquiryRequest); + return convertToDTO(savedRequest); + } catch (Exception e) { + throw new RuntimeException("关键词确认失败: " + e.getMessage(), e); + } + } + + @Override + public InquiryRequestDTO selectDataSources(Long id, DataSourceSelectionDTO dataSourceDTO) { + InquiryRequest inquiryRequest = inquiryRequestRepository.findById(id) + .orElseThrow(() -> new RuntimeException("查询请求不存在: " + id)); + + // 保存用户选择的数据源 + inquiryRequest.setSearchInternalData(dataSourceDTO.getSearchInternalData()); + inquiryRequest.setSearchKnowledgeBase(dataSourceDTO.getSearchKnowledgeBase()); + inquiryRequest.setSearchCnki(dataSourceDTO.getSearchCnki()); + inquiryRequest.setSearchClinicalTrials(dataSourceDTO.getSearchClinicalTrials()); + inquiryRequest.setUpdatedAt(LocalDateTime.now()); + + InquiryRequest savedRequest = inquiryRequestRepository.save(inquiryRequest); + return convertToDTO(savedRequest); + } + /** * 生成请求编号 */ @@ -199,6 +258,11 @@ public class InquiryServiceImpl implements InquiryService { dto.setCustomerTitle(inquiryRequest.getCustomerTitle()); dto.setInquiryContent(inquiryRequest.getInquiryContent()); dto.setKeywords(inquiryRequest.getKeywords()); + dto.setKeywordsConfirmed(inquiryRequest.getKeywordsConfirmed()); + dto.setSearchInternalData(inquiryRequest.getSearchInternalData()); + dto.setSearchKnowledgeBase(inquiryRequest.getSearchKnowledgeBase()); + dto.setSearchCnki(inquiryRequest.getSearchCnki()); + dto.setSearchClinicalTrials(inquiryRequest.getSearchClinicalTrials()); dto.setStatus(inquiryRequest.getStatus().name()); dto.setSearchResults(inquiryRequest.getSearchResults()); dto.setResponseContent(inquiryRequest.getResponseContent()); diff --git a/backend/src/main/java/com/ipsen/medical/service/impl/MultiSourceSearchServiceImpl.java b/backend/src/main/java/com/ipsen/medical/service/impl/MultiSourceSearchServiceImpl.java new file mode 100644 index 0000000..cff5ef2 --- /dev/null +++ b/backend/src/main/java/com/ipsen/medical/service/impl/MultiSourceSearchServiceImpl.java @@ -0,0 +1,223 @@ +package com.ipsen.medical.service.impl; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.ipsen.medical.dto.ClinicalTrialDTO; +import com.ipsen.medical.dto.KeywordConfirmationDTO; +import com.ipsen.medical.dto.SearchResultItemDTO; +import com.ipsen.medical.entity.InquiryRequest; +import com.ipsen.medical.repository.InquiryRequestRepository; +import com.ipsen.medical.service.ClinicalTrialsService; +import com.ipsen.medical.service.DifyService; +import com.ipsen.medical.service.KnowledgeBaseService; +import com.ipsen.medical.service.MultiSourceSearchService; +import com.ipsen.medical.service.SearchResultService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.ArrayList; +import java.util.List; + +/** + * 多源检索服务实现 + */ +@Slf4j +@Service +@Transactional +public class MultiSourceSearchServiceImpl implements MultiSourceSearchService { + + @Autowired + private InquiryRequestRepository inquiryRequestRepository; + + @Autowired + private SearchResultService searchResultService; + + @Autowired + private ClinicalTrialsService clinicalTrialsService; + + @Autowired + private KnowledgeBaseService knowledgeBaseService; + + @Autowired + private DifyService difyService; + + private final ObjectMapper objectMapper = new ObjectMapper(); + + @Override + public List performMultiSourceSearch(Long inquiryId) { + log.info("Performing multi-source search for inquiry: {}", inquiryId); + + InquiryRequest inquiry = inquiryRequestRepository.findById(inquiryId) + .orElseThrow(() -> new RuntimeException("查询请求不存在: " + inquiryId)); + + // 解析关键词 + String keywords = inquiry.getKeywords(); + if (keywords == null || keywords.isEmpty()) { + throw new RuntimeException("关键词未提取,无法执行检索"); + } + + List allResults = new ArrayList<>(); + + try { + // 根据用户选择的数据源进行检索 + if (Boolean.TRUE.equals(inquiry.getSearchInternalData())) { + log.info("Searching internal data"); + List internalResults = searchInternalData(inquiryId, keywords); + allResults.addAll(internalResults); + } + + if (Boolean.TRUE.equals(inquiry.getSearchKnowledgeBase())) { + log.info("Searching knowledge base"); + List kbResults = searchKnowledgeBase(inquiryId, keywords); + allResults.addAll(kbResults); + } + + if (Boolean.TRUE.equals(inquiry.getSearchCnki())) { + log.info("Searching CNKI"); + List cnkiResults = searchCnki(inquiryId, keywords); + allResults.addAll(cnkiResults); + } + + if (Boolean.TRUE.equals(inquiry.getSearchClinicalTrials())) { + log.info("Searching ClinicalTrials"); + List ctResults = searchClinicalTrials(inquiryId, keywords); + allResults.addAll(ctResults); + } + + // 更新查询请求状态 + inquiry.setStatus(InquiryRequest.RequestStatus.SEARCH_COMPLETED); + inquiryRequestRepository.save(inquiry); + + log.info("Multi-source search completed, found {} results", allResults.size()); + + return allResults; + + } catch (Exception e) { + log.error("Error performing multi-source search: ", e); + throw new RuntimeException("多源检索失败: " + e.getMessage(), e); + } + } + + @Override + public List searchInternalData(Long inquiryId, String keywords) { + log.info("Searching internal data for inquiry: {}", inquiryId); + + List results = new ArrayList<>(); + + // TODO: 实现内部数据检索逻辑 + // 这里应该查询企业内部研究数据、历史回复等 + log.warn("Internal data search not yet implemented"); + + return results; + } + + @Override + public List searchKnowledgeBase(Long inquiryId, String keywords) { + log.info("Searching knowledge base for inquiry: {}", inquiryId); + + List results = new ArrayList<>(); + + try { + // 使用Dify服务搜索知识库 + String searchResults = difyService.performSearch(keywords); + + // 解析搜索结果并转换为SearchResultItemDTO + // TODO: 根据实际返回格式解析 + SearchResultItemDTO result = new SearchResultItemDTO(); + result.setInquiryRequestId(inquiryId); + result.setTitle("知识库检索结果"); + result.setContent(searchResults); + result.setSource("知识库"); + + SearchResultItemDTO created = searchResultService.createSearchResult(result); + results.add(created); + + } catch (Exception e) { + log.error("Error searching knowledge base: ", e); + } + + return results; + } + + @Override + public List searchCnki(Long inquiryId, String keywords) { + log.info("Searching CNKI for inquiry: {}", inquiryId); + + List results = new ArrayList<>(); + + // TODO: 实现知网检索逻辑 + // 这里应该调用知网API或爬虫 + log.warn("CNKI search not yet implemented"); + + return results; + } + + @Override + public List searchClinicalTrials(Long inquiryId, String keywords) { + log.info("Searching ClinicalTrials for inquiry: {}", inquiryId); + + List results = new ArrayList<>(); + + try { + // 解析关键词 + KeywordConfirmationDTO keywordDTO = objectMapper.readValue(keywords, KeywordConfirmationDTO.class); + String searchKeyword = buildSearchKeyword(keywordDTO); + + // 使用ClinicalTrialsService搜索 + List trials = clinicalTrialsService.searchAndSaveForInquiry(inquiryId, searchKeyword); + + // 转换为SearchResultItemDTO + for (ClinicalTrialDTO trial : trials) { + SearchResultItemDTO result = new SearchResultItemDTO(); + result.setInquiryRequestId(inquiryId); + result.setTitle(trial.getStudyTitle() != null ? trial.getStudyTitle() : trial.getBriefTitle()); + result.setSummary(trial.getBriefSummary()); + result.setNctId(trial.getNctId()); + result.setSource("ClinicalTrials.gov"); + result.setSourceUrl("https://clinicaltrials.gov/study/" + trial.getNctId()); + result.setPublicationDate(trial.getStartDate()); + + SearchResultItemDTO created = searchResultService.createSearchResult(result); + results.add(created); + } + + } catch (Exception e) { + log.error("Error searching ClinicalTrials: ", e); + } + + return results; + } + + /** + * 构建搜索关键词 + */ + private String buildSearchKeyword(KeywordConfirmationDTO keywordDTO) { + StringBuilder sb = new StringBuilder(); + + if (keywordDTO.getDrugNameEnglish() != null && !keywordDTO.getDrugNameEnglish().isEmpty()) { + sb.append(keywordDTO.getDrugNameEnglish()); + } else if (keywordDTO.getDrugNameChinese() != null && !keywordDTO.getDrugNameChinese().isEmpty()) { + sb.append(keywordDTO.getDrugNameChinese()); + } + + if (keywordDTO.getRequestItem() != null && !keywordDTO.getRequestItem().isEmpty()) { + if (sb.length() > 0) { + sb.append(" "); + } + sb.append(keywordDTO.getRequestItem()); + } + + if (keywordDTO.getAdditionalKeywords() != null && !keywordDTO.getAdditionalKeywords().isEmpty()) { + for (String keyword : keywordDTO.getAdditionalKeywords()) { + if (sb.length() > 0) { + sb.append(" "); + } + sb.append(keyword); + } + } + + return sb.toString(); + } +} + diff --git a/backend/src/main/java/com/ipsen/medical/service/impl/SearchResultServiceImpl.java b/backend/src/main/java/com/ipsen/medical/service/impl/SearchResultServiceImpl.java new file mode 100644 index 0000000..3f8b6d4 --- /dev/null +++ b/backend/src/main/java/com/ipsen/medical/service/impl/SearchResultServiceImpl.java @@ -0,0 +1,201 @@ +package com.ipsen.medical.service.impl; + +import com.ipsen.medical.dto.SearchResultItemDTO; +import com.ipsen.medical.entity.InquiryRequest; +import com.ipsen.medical.entity.SearchResultItem; +import com.ipsen.medical.repository.InquiryRequestRepository; +import com.ipsen.medical.repository.SearchResultItemRepository; +import com.ipsen.medical.service.SearchResultService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.stream.Collectors; + +/** + * 检索结果服务实现 + */ +@Slf4j +@Service +@Transactional +public class SearchResultServiceImpl implements SearchResultService { + + @Autowired + private SearchResultItemRepository searchResultItemRepository; + + @Autowired + private InquiryRequestRepository inquiryRequestRepository; + + @Override + public List getSearchResults(Long inquiryRequestId) { + List items = searchResultItemRepository.findByInquiryRequestId(inquiryRequestId); + return items.stream() + .map(this::convertToDTO) + .collect(Collectors.toList()); + } + + @Override + public List getActiveSearchResults(Long inquiryRequestId) { + List items = searchResultItemRepository.findByInquiryRequestIdAndIsDeletedFalse(inquiryRequestId); + return items.stream() + .map(this::convertToDTO) + .collect(Collectors.toList()); + } + + @Override + public SearchResultItemDTO updateSearchResult(Long id, SearchResultItemDTO dto) { + SearchResultItem item = searchResultItemRepository.findById(id) + .orElseThrow(() -> new RuntimeException("检索结果不存在: " + id)); + + if (dto.getIncludeInResponse() != null) { + item.setIncludeInResponse(dto.getIncludeInResponse()); + } + if (dto.getNeedDownload() != null) { + item.setNeedDownload(dto.getNeedDownload()); + if (dto.getNeedDownload() && item.getDownloadStatus() == SearchResultItem.DownloadStatus.NOT_REQUIRED) { + item.setDownloadStatus(SearchResultItem.DownloadStatus.PENDING); + } else if (!dto.getNeedDownload()) { + item.setDownloadStatus(SearchResultItem.DownloadStatus.NOT_REQUIRED); + } + } + if (dto.getIsDeleted() != null) { + item.setIsDeleted(dto.getIsDeleted()); + if (dto.getIsDeleted()) { + item.setStatus(SearchResultItem.ResultStatus.DELETED); + } + } + if (dto.getStatus() != null) { + item.setStatus(SearchResultItem.ResultStatus.valueOf(dto.getStatus())); + } + + item.setUpdatedAt(LocalDateTime.now()); + SearchResultItem saved = searchResultItemRepository.save(item); + return convertToDTO(saved); + } + + @Override + public List batchUpdateSearchResults(List dtos) { + return dtos.stream() + .map(dto -> updateSearchResult(dto.getId(), dto)) + .collect(Collectors.toList()); + } + + @Override + public void markAsDeleted(Long id) { + SearchResultItem item = searchResultItemRepository.findById(id) + .orElseThrow(() -> new RuntimeException("检索结果不存在: " + id)); + item.setIsDeleted(true); + item.setStatus(SearchResultItem.ResultStatus.DELETED); + item.setUpdatedAt(LocalDateTime.now()); + searchResultItemRepository.save(item); + } + + @Override + public SearchResultItemDTO setIncludeInResponse(Long id, Boolean include) { + SearchResultItem item = searchResultItemRepository.findById(id) + .orElseThrow(() -> new RuntimeException("检索结果不存在: " + id)); + item.setIncludeInResponse(include); + item.setUpdatedAt(LocalDateTime.now()); + SearchResultItem saved = searchResultItemRepository.save(item); + return convertToDTO(saved); + } + + @Override + public SearchResultItemDTO setNeedDownload(Long id, Boolean need) { + SearchResultItem item = searchResultItemRepository.findById(id) + .orElseThrow(() -> new RuntimeException("检索结果不存在: " + id)); + item.setNeedDownload(need); + if (need && item.getDownloadStatus() == SearchResultItem.DownloadStatus.NOT_REQUIRED) { + item.setDownloadStatus(SearchResultItem.DownloadStatus.PENDING); + } else if (!need) { + item.setDownloadStatus(SearchResultItem.DownloadStatus.NOT_REQUIRED); + } + item.setUpdatedAt(LocalDateTime.now()); + SearchResultItem saved = searchResultItemRepository.save(item); + return convertToDTO(saved); + } + + @Override + public List getPendingDownloads() { + List items = searchResultItemRepository.findByDownloadStatus( + SearchResultItem.DownloadStatus.PENDING); + return items.stream() + .map(this::convertToDTO) + .collect(Collectors.toList()); + } + + @Override + public List getPendingDownloadsByInquiry(Long inquiryRequestId) { + List items = searchResultItemRepository.findByInquiryRequestIdAndNeedDownloadTrue(inquiryRequestId); + return items.stream() + .filter(item -> item.getDownloadStatus() == SearchResultItem.DownloadStatus.PENDING || + item.getDownloadStatus() == SearchResultItem.DownloadStatus.FAILED) + .map(this::convertToDTO) + .collect(Collectors.toList()); + } + + @Override + public SearchResultItemDTO createSearchResult(SearchResultItemDTO dto) { + InquiryRequest inquiry = inquiryRequestRepository.findById(dto.getInquiryRequestId()) + .orElseThrow(() -> new RuntimeException("查询请求不存在: " + dto.getInquiryRequestId())); + + SearchResultItem item = new SearchResultItem(); + item.setInquiryRequest(inquiry); + item.setTitle(dto.getTitle()); + item.setSummary(dto.getSummary()); + item.setContent(dto.getContent()); + item.setAuthors(dto.getAuthors()); + item.setSource(dto.getSource()); + item.setSourceUrl(dto.getSourceUrl()); + item.setPublicationDate(dto.getPublicationDate()); + item.setDoi(dto.getDoi()); + item.setPmid(dto.getPmid()); + item.setNctId(dto.getNctId()); + item.setMetadata(dto.getMetadata()); + + SearchResultItem saved = searchResultItemRepository.save(item); + return convertToDTO(saved); + } + + @Override + public List batchCreateSearchResults(List dtos) { + return dtos.stream() + .map(this::createSearchResult) + .collect(Collectors.toList()); + } + + /** + * 转换为DTO + */ + private SearchResultItemDTO convertToDTO(SearchResultItem item) { + SearchResultItemDTO dto = new SearchResultItemDTO(); + dto.setId(item.getId()); + dto.setInquiryRequestId(item.getInquiryRequest().getId()); + dto.setTitle(item.getTitle()); + dto.setSummary(item.getSummary()); + dto.setContent(item.getContent()); + dto.setAuthors(item.getAuthors()); + dto.setSource(item.getSource()); + dto.setSourceUrl(item.getSourceUrl()); + dto.setPublicationDate(item.getPublicationDate()); + dto.setDoi(item.getDoi()); + dto.setPmid(item.getPmid()); + dto.setNctId(item.getNctId()); + dto.setMetadata(item.getMetadata()); + dto.setStatus(item.getStatus().name()); + dto.setIncludeInResponse(item.getIncludeInResponse()); + dto.setNeedDownload(item.getNeedDownload()); + dto.setIsDeleted(item.getIsDeleted()); + dto.setFilePath(item.getFilePath()); + dto.setDownloadStatus(item.getDownloadStatus().name()); + dto.setCreatedAt(item.getCreatedAt()); + dto.setUpdatedAt(item.getUpdatedAt()); + dto.setDownloadedAt(item.getDownloadedAt()); + return dto; + } +} + + diff --git a/backend/src/main/resources/application.yml b/backend/src/main/resources/application.yml index 9de5c47..1202907 100644 --- a/backend/src/main/resources/application.yml +++ b/backend/src/main/resources/application.yml @@ -38,7 +38,7 @@ app: # Dify配置 dify: api-url: ${DIFY_API_URL:https://api.dify.ai/v1} - api-key: ${DIFY_API_KEY:app-croZF0SSV5fiyXbK3neGrOT6} + api-key: ${DIFY_API_KEY:your-dify-api-key-here} # 大模型配置 llm: diff --git a/database/init_drug_module.sql b/database/init_drug_module.sql index 84d5118..6a010a5 100644 --- a/database/init_drug_module.sql +++ b/database/init_drug_module.sql @@ -213,3 +213,5 @@ SELECT '药物模块初始化完成!' AS message, + + diff --git a/database/migration_search_result_items.sql b/database/migration_search_result_items.sql new file mode 100644 index 0000000..cb2073e --- /dev/null +++ b/database/migration_search_result_items.sql @@ -0,0 +1,140 @@ +-- 添加检索结果项表和更新查询请求表 +-- Migration script for search result items and inquiry request enhancements + +USE medical_info_system; + +-- 1. 更新查询请求表,添加新字段 +-- 检查并添加 keywords_confirmed 字段 +SET @sql = (SELECT IF( + (SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS + WHERE table_name = 'inquiry_requests' + AND table_schema = DATABASE() + AND column_name = 'keywords_confirmed') = 0, + 'ALTER TABLE inquiry_requests ADD COLUMN keywords_confirmed BOOLEAN DEFAULT FALSE COMMENT ''用户是否确认关键词''', + 'SELECT ''Column keywords_confirmed already exists'' as message' +)); +PREPARE stmt FROM @sql; +EXECUTE stmt; +DEALLOCATE PREPARE stmt; + +-- 检查并添加 search_internal_data 字段 +SET @sql = (SELECT IF( + (SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS + WHERE table_name = 'inquiry_requests' + AND table_schema = DATABASE() + AND column_name = 'search_internal_data') = 0, + 'ALTER TABLE inquiry_requests ADD COLUMN search_internal_data BOOLEAN DEFAULT FALSE COMMENT ''是否在内部数据中检索''', + 'SELECT ''Column search_internal_data already exists'' as message' +)); +PREPARE stmt FROM @sql; +EXECUTE stmt; +DEALLOCATE PREPARE stmt; + +-- 检查并添加 search_knowledge_base 字段 +SET @sql = (SELECT IF( + (SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS + WHERE table_name = 'inquiry_requests' + AND table_schema = DATABASE() + AND column_name = 'search_knowledge_base') = 0, + 'ALTER TABLE inquiry_requests ADD COLUMN search_knowledge_base BOOLEAN DEFAULT FALSE COMMENT ''是否在知识库中检索''', + 'SELECT ''Column search_knowledge_base already exists'' as message' +)); +PREPARE stmt FROM @sql; +EXECUTE stmt; +DEALLOCATE PREPARE stmt; + +-- 检查并添加 search_cnki 字段 +SET @sql = (SELECT IF( + (SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS + WHERE table_name = 'inquiry_requests' + AND table_schema = DATABASE() + AND column_name = 'search_cnki') = 0, + 'ALTER TABLE inquiry_requests ADD COLUMN search_cnki BOOLEAN DEFAULT FALSE COMMENT ''是否在知网中检索''', + 'SELECT ''Column search_cnki already exists'' as message' +)); +PREPARE stmt FROM @sql; +EXECUTE stmt; +DEALLOCATE PREPARE stmt; + +-- 检查并添加 search_clinical_trials 字段 +SET @sql = (SELECT IF( + (SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS + WHERE table_name = 'inquiry_requests' + AND table_schema = DATABASE() + AND column_name = 'search_clinical_trials') = 0, + 'ALTER TABLE inquiry_requests ADD COLUMN search_clinical_trials BOOLEAN DEFAULT FALSE COMMENT ''是否在ClinicalTrials中检索''', + 'SELECT ''Column search_clinical_trials already exists'' as message' +)); +PREPARE stmt FROM @sql; +EXECUTE stmt; +DEALLOCATE PREPARE stmt; + +-- 2. 修改查询请求表,使customer_name为可选 +ALTER TABLE inquiry_requests +MODIFY COLUMN customer_name VARCHAR(100) NULL COMMENT '客户姓名(非必填)'; + +-- 3. 修改查询请求表,使inquiry_content为必填 +ALTER TABLE inquiry_requests +MODIFY COLUMN inquiry_content TEXT NOT NULL COMMENT '查询内容(必填)'; + +-- 4. 创建检索结果项表 +CREATE TABLE IF NOT EXISTS search_result_items ( + id BIGINT PRIMARY KEY AUTO_INCREMENT, + inquiry_request_id BIGINT NOT NULL COMMENT '查询请求ID', + title VARCHAR(500) NOT NULL COMMENT '标题', + summary TEXT COMMENT '摘要', + content TEXT COMMENT '内容', + authors VARCHAR(500) COMMENT '作者', + source VARCHAR(100) COMMENT '来源(知网、ClinicalTrials、知识库等)', + source_url VARCHAR(500) COMMENT '来源URL', + publication_date VARCHAR(50) COMMENT '发表日期', + doi VARCHAR(100) COMMENT 'DOI', + pmid VARCHAR(50) COMMENT 'PubMed ID', + nct_id VARCHAR(50) COMMENT 'ClinicalTrials NCT ID', + metadata TEXT COMMENT '其他元数据(JSON格式)', + status VARCHAR(20) NOT NULL DEFAULT 'PENDING_REVIEW' COMMENT '结果状态: PENDING_REVIEW, APPROVED, REJECTED, DELETED', + include_in_response BOOLEAN DEFAULT FALSE COMMENT '是否纳入回复参考资料', + need_download BOOLEAN DEFAULT FALSE COMMENT '是否需要下载全文', + is_deleted BOOLEAN DEFAULT FALSE COMMENT '是否标记为错误并删除', + file_path VARCHAR(500) COMMENT '下载后的文件路径', + download_status VARCHAR(20) DEFAULT 'NOT_REQUIRED' COMMENT '下载状态: NOT_REQUIRED, PENDING, DOWNLOADING, COMPLETED, FAILED', + created_at DATETIME NOT NULL, + updated_at DATETIME, + downloaded_at DATETIME, + INDEX idx_inquiry_request_id (inquiry_request_id), + INDEX idx_source (source), + INDEX idx_status (status), + INDEX idx_include_in_response (include_in_response), + INDEX idx_need_download (need_download), + INDEX idx_is_deleted (is_deleted), + INDEX idx_download_status (download_status), + FOREIGN KEY (inquiry_request_id) REFERENCES inquiry_requests(id) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='检索结果项表'; + +-- 5. 添加索引以提高查询性能 +-- 检查并创建 idx_keywords_confirmed 索引 +SET @sql = (SELECT IF( + (SELECT COUNT(*) FROM INFORMATION_SCHEMA.STATISTICS + WHERE table_name = 'inquiry_requests' + AND table_schema = DATABASE() + AND index_name = 'idx_keywords_confirmed') = 0, + 'CREATE INDEX idx_keywords_confirmed ON inquiry_requests(keywords_confirmed)', + 'SELECT ''Index idx_keywords_confirmed already exists'' as message' +)); +PREPARE stmt FROM @sql; +EXECUTE stmt; +DEALLOCATE PREPARE stmt; + +-- 检查并创建 idx_search_flags 索引 +SET @sql = (SELECT IF( + (SELECT COUNT(*) FROM INFORMATION_SCHEMA.STATISTICS + WHERE table_name = 'inquiry_requests' + AND table_schema = DATABASE() + AND index_name = 'idx_search_flags') = 0, + 'CREATE INDEX idx_search_flags ON inquiry_requests(search_internal_data, search_knowledge_base, search_cnki, search_clinical_trials)', + 'SELECT ''Index idx_search_flags already exists'' as message' +)); +PREPARE stmt FROM @sql; +EXECUTE stmt; +DEALLOCATE PREPARE stmt; + diff --git a/docker-compose.yml b/docker-compose.yml deleted file mode 100644 index 9eeb4a9..0000000 --- a/docker-compose.yml +++ /dev/null @@ -1,49 +0,0 @@ -# Comments are provided throughout this file to help you get started. -# If you need more help, visit the Docker Compose reference guide at -# https://docs.docker.com/go/compose-spec-reference/ - -# Here the instructions define your application as a service called "server". -# This service is built from the Dockerfile in the current directory. -# You can add other services your application may depend on here, such as a -# database or a cache. For examples, see the Awesome Compose repository: -# https://github.com/docker/awesome-compose -services: - server: - build: - context: . - ports: - - 8080:8080 - -# The commented out section below is an example of how to define a PostgreSQL -# database that your application can use. `depends_on` tells Docker Compose to -# start the database before your application. The `db-data` volume persists the -# database data between container restarts. The `db-password` secret is used -# to set the database password. You must create `db/password.txt` and add -# a password of your choosing to it before running `docker-compose up`. -# depends_on: -# db: -# condition: service_healthy -# db: -# image: postgres -# restart: always -# user: postgres -# secrets: -# - db-password -# volumes: -# - db-data:/var/lib/postgresql/data -# environment: -# - POSTGRES_DB=example -# - POSTGRES_PASSWORD_FILE=/run/secrets/db-password -# expose: -# - 5432 -# healthcheck: -# test: [ "CMD", "pg_isready" ] -# interval: 10s -# timeout: 5s -# retries: 5 -# volumes: -# db-data: -# secrets: -# db-password: -# file: db/password.txt - diff --git a/env.example b/env.example index 0eb2882..f461197 100644 --- a/env.example +++ b/env.example @@ -10,6 +10,7 @@ SPRING_PROFILES_ACTIVE=prod JWT_SECRET=your-secret-key-change-in-production-environment # Dify配置 +# 请将下面的API Key替换为您实际的Dify API Key DIFY_API_URL=https://api.dify.ai/v1 DIFY_API_KEY=your-dify-api-key-here diff --git a/frontend/Dockerfile b/frontend/Dockerfile deleted file mode 100644 index 5a8b05d..0000000 --- a/frontend/Dockerfile +++ /dev/null @@ -1,34 +0,0 @@ -# 构建阶段 -FROM node:18-alpine AS build -WORKDIR /app - -# 复制package.json和package-lock.json -COPY package*.json ./ -RUN npm install - -# 复制源代码并构建 -COPY . . -RUN npm run build - -# 运行阶段 -FROM nginx:alpine -WORKDIR /usr/share/nginx/html - -# 删除默认的nginx静态资源 -RUN rm -rf ./* - -# 从构建阶段复制构建结果 -COPY --from=build /app/dist . - -# 复制nginx配置 -COPY nginx.conf /etc/nginx/conf.d/default.conf - -# 暴露端口 -EXPOSE 80 - -# 启动nginx -CMD ["nginx", "-g", "daemon off;"] - - - - diff --git a/frontend/nginx.conf b/frontend/nginx.conf deleted file mode 100644 index 2a13bec..0000000 --- a/frontend/nginx.conf +++ /dev/null @@ -1,41 +0,0 @@ -server { - listen 80; - server_name localhost; - root /usr/share/nginx/html; - index index.html; - - # Gzip压缩 - gzip on; - gzip_vary on; - gzip_min_length 1024; - gzip_types text/plain text/css text/xml text/javascript application/x-javascript application/xml+rss application/json application/javascript; - - # 前端路由支持 - location / { - try_files $uri $uri/ /index.html; - } - - # API代理 - location /api/ { - proxy_pass http://backend:8080/api/; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - - # 超时设置 - proxy_connect_timeout 300s; - proxy_send_timeout 300s; - proxy_read_timeout 300s; - } - - # 缓存静态资源 - location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ { - expires 1y; - add_header Cache-Control "public, immutable"; - } -} - - - - diff --git a/frontend/src/api/inquiry.js b/frontend/src/api/inquiry.js index 75053c7..6ce632d 100644 --- a/frontend/src/api/inquiry.js +++ b/frontend/src/api/inquiry.js @@ -160,6 +160,28 @@ export function executeFullWorkflow(id) { }) } +/** + * 确认和更新关键词 + */ +export function confirmKeywords(id, data) { + return request({ + url: `/inquiries/${id}/confirm-keywords`, + method: 'post', + data + }) +} + +/** + * 选择数据源 + */ +export function selectDataSources(id, data) { + return request({ + url: `/inquiries/${id}/select-data-sources`, + method: 'post', + data + }) +} + diff --git a/frontend/src/api/searchResult.js b/frontend/src/api/searchResult.js new file mode 100644 index 0000000..5c2e360 --- /dev/null +++ b/frontend/src/api/searchResult.js @@ -0,0 +1,109 @@ +import request from '@/utils/request' + +/** + * 获取查询请求的所有检索结果 + */ +export function getSearchResults(inquiryId) { + return request({ + url: `/search-results/inquiry/${inquiryId}`, + method: 'get' + }) +} + +/** + * 更新检索结果项 + */ +export function updateSearchResult(id, data) { + return request({ + url: `/search-results/${id}`, + method: 'put', + data + }) +} + +/** + * 批量更新检索结果 + */ +export function batchUpdateSearchResults(data) { + return request({ + url: '/search-results/batch', + method: 'put', + data + }) +} + +/** + * 标记为错误并删除 + */ +export function deleteSearchResult(id) { + return request({ + url: `/search-results/${id}`, + method: 'delete' + }) +} + +/** + * 设置是否纳入回复参考资料 + */ +export function setIncludeInResponse(id, include) { + return request({ + url: `/search-results/${id}/include`, + method: 'post', + params: { include } + }) +} + +/** + * 设置是否需要下载全文 + */ +export function setNeedDownload(id, need) { + return request({ + url: `/search-results/${id}/download`, + method: 'post', + params: { need } + }) +} + +/** + * 获取所有待下载的结果项 + */ +export function getPendingDownloads() { + return request({ + url: '/download-tasks/pending', + method: 'get' + }) +} + +/** + * 获取查询请求的待下载结果项 + */ +export function getPendingDownloadsByInquiry(inquiryId) { + return request({ + url: `/download-tasks/inquiry/${inquiryId}/pending`, + method: 'get' + }) +} + +/** + * 标记下载完成 + */ +export function markDownloadComplete(id, filePath) { + return request({ + url: `/download-tasks/${id}/complete`, + method: 'post', + params: { filePath } + }) +} + +/** + * 标记下载失败 + */ +export function markDownloadFailed(id, reason) { + return request({ + url: `/download-tasks/${id}/fail`, + method: 'post', + params: { reason } + }) +} + + diff --git a/frontend/src/router/index.js b/frontend/src/router/index.js index ab8a9c9..f9612ee 100644 --- a/frontend/src/router/index.js +++ b/frontend/src/router/index.js @@ -45,6 +45,40 @@ const routes = [ component: () => import('@/views/inquiry/InquiryDetail.vue'), meta: { title: '查询详情', icon: 'View' }, hidden: true + }, + { + path: ':id/keyword-confirmation', + name: 'KeywordConfirmation', + component: () => import('@/views/inquiry/KeywordConfirmation.vue'), + meta: { title: '关键词确认', icon: 'Key' }, + hidden: true + }, + { + path: ':id/data-source-selection', + name: 'DataSourceSelection', + component: () => import('@/views/inquiry/DataSourceSelection.vue'), + meta: { title: '数据源选择', icon: 'DataBoard' }, + hidden: true + }, + { + path: ':id/search-results', + name: 'SearchResults', + component: () => import('@/views/inquiry/SearchResults.vue'), + meta: { title: '检索结果管理', icon: 'Search' }, + hidden: true + }, + { + path: ':id/response-view', + name: 'ResponseView', + component: () => import('@/views/inquiry/ResponseView.vue'), + meta: { title: '回复查看', icon: 'Document' }, + hidden: true + }, + { + path: 'download-tasks', + name: 'DownloadTasks', + component: () => import('@/views/inquiry/DownloadTasks.vue'), + meta: { title: '待下载任务', icon: 'Download' } } ] }, diff --git a/frontend/src/views/inquiry/DataSourceSelection.vue b/frontend/src/views/inquiry/DataSourceSelection.vue new file mode 100644 index 0000000..c770164 --- /dev/null +++ b/frontend/src/views/inquiry/DataSourceSelection.vue @@ -0,0 +1,437 @@ + + + + + + + diff --git a/frontend/src/views/inquiry/DownloadTasks.vue b/frontend/src/views/inquiry/DownloadTasks.vue new file mode 100644 index 0000000..076c294 --- /dev/null +++ b/frontend/src/views/inquiry/DownloadTasks.vue @@ -0,0 +1,484 @@ + + + + + + + diff --git a/frontend/src/views/inquiry/InquiryDetail.vue b/frontend/src/views/inquiry/InquiryDetail.vue index 78bb792..c8b5b68 100644 --- a/frontend/src/views/inquiry/InquiryDetail.vue +++ b/frontend/src/views/inquiry/InquiryDetail.vue @@ -14,7 +14,14 @@ {{ inquiry.requestNumber }} - + + + + + + + + {{ inquiry.customerName }} @@ -36,16 +43,50 @@ - - - - - - - - +
+ + + 开始处理(关键词提取) + + + + 确认关键词 + + + + 选择数据源并检索 + + + + 查看检索结果 + + + + 查看生成的回复 + + { return '' } } + +const goToKeywordConfirmation = () => { + router.push(`/inquiry/${inquiry.value.id}/keyword-confirmation`) +} + +const goToDataSourceSelection = () => { + router.push(`/inquiry/${inquiry.value.id}/data-source-selection`) +} + +const goToSearchResults = () => { + router.push(`/inquiry/${inquiry.value.id}/search-results`) +} + +const goToResponseView = () => { + router.push(`/inquiry/${inquiry.value.id}/response-view`) +} + + diff --git a/frontend/src/views/inquiry/ResponseView.vue b/frontend/src/views/inquiry/ResponseView.vue new file mode 100644 index 0000000..ff24dc7 --- /dev/null +++ b/frontend/src/views/inquiry/ResponseView.vue @@ -0,0 +1,494 @@ + + + + + + + diff --git a/frontend/src/views/inquiry/SearchResults.vue b/frontend/src/views/inquiry/SearchResults.vue new file mode 100644 index 0000000..db30dd9 --- /dev/null +++ b/frontend/src/views/inquiry/SearchResults.vue @@ -0,0 +1,500 @@ + + + + + + + diff --git a/presentation.html b/presentation.html deleted file mode 100644 index 07f4d2a..0000000 --- a/presentation.html +++ /dev/null @@ -1,1073 +0,0 @@ - - - - - - 医学信息支持系统 - 系统介绍 - - - - - -
-
- - -
-
- -

医学信息支持系统

-

Medical Information Support System

-
-

为益普生医学信息团队打造的智能化信息支持平台

-

日期:2025年

-
-
- - -
-

📋 演示内容

-
-
- 1. 系统背景与目的 - 为什么需要这个系统? -
-
- 2. 业务流程详解 - 系统如何工作? -
-
- 3. 核心功能模块 - 系统能做什么? -
-
- 4. 技术架构 - 系统如何构建? -
-
- 5. 使用指南 - 如何使用系统? -
-
- 6. 价值与展望 - 系统带来的价值 -
-
-
- - -
-
-

🎯 系统背景与目的

-
-

业务挑战

-
    -
  • 客户(医生)经常通过邮件向益普生提出专业的医学信息需求
  • -
  • 医学信息团队需要快速、准确地回复这些咨询
  • -
  • 信息检索涉及多个数据源:企业内部数据、公开数据库、文献等
  • -
  • 人工处理效率低、耗时长、容易遗漏关键信息
  • -
-
-
- -
-

💡 解决方案

-
- 构建智能化医学信息支持系统
- 整合AI技术,实现自动化信息检索与处理 -
-
-

核心价值

-
- ⚡ 提升效率 - 自动化关键词提取和信息检索 -
-
- 🎯 精准回复 - 多源数据整合,信息全面准确 -
-
- 📚 规范管理 - 标准化流程,历史记录可追溯 -
-
- 🤖 AI赋能 - 集成Dify AI引擎,智能生成回复 -
-
-
- -
-

🎯 目标用户

-
-
-

👨‍⚕️ 医学信息专员

-

创建查询请求、审核回复内容、管理文献下载

-
-
-

✅ 审核人员

-

审核系统生成的回复内容,确保准确性

-
-
-

👤 管理员

-

系统配置、用户管理、知识库维护

-
-
-

🔬 医生(客户)

-

最终接收专业、准确的医学信息回复

-
-
-
-
- - -
-
-

🔄 完整业务流程

-
- - - - - - - - - - - 1. 客户提出需求 - - - - - - - 2. 上传标准化表格 - 或手动创建查询 - - - - - - - 3. AI提取关键词 - (药物、疾病、问题) - - - - - - - 4. 多源信息检索 - ①企业自有数据 - ②公开数据库 - ③扩展关联数据 - - - - - - - 5. 生成回复草稿 - (AI整理信息) - - - - - - - 6. 人工审核 - (审核人员确认) - - - - - - - 7. 下载文献 - (自动下载到本地) - - - - - - - 8. 发送给客户 - (回复+文献) - - - - ⏱️ 处理时效 - • 关键词提取: 秒级 - • 信息检索: 1-3分钟 - • 生成回复: 1-2分钟 - • 人工审核: 按需 - • 文献下载: 5-10分钟 - - 传统方式: 数小时至数天 - 系统处理: 10-30分钟 - -
-
- -
-

📊 知识库层级检索

-
-
-

🔹 第一优先级:企业自有数据

-
    -
  • 企业开展的研究数据
  • -
  • 历史回复记录
  • -
  • 内部文献和资料
  • -
  • 特点:权威性最高,最符合企业需求
  • -
-
- -
-

🔹 第二优先级:公开数据

-
- PubMed - EMBASE - 知网 - ClinicalTrials.gov - 监管机构 -
-

特点:公开可靠,已人工整理

-
- -
-

🔹 第三优先级:扩展关联数据

-
    -
  • 疾病数据库
  • -
  • 药物数据库
  • -
  • 疾病-药物关联数据
  • -
  • 特点:提供更广泛的参考信息
  • -
-
-
-
-
- - -
-
-

⚙️ 核心功能模块

-
-
-

📝 查询请求管理

-
    -
  • 上传Excel表格
  • -
  • 手动创建查询
  • -
  • 关键词自动提取
  • -
  • 状态流转跟踪
  • -
  • 审核流程管理
  • -
-
- -
-

📚 知识库管理

-
    -
  • 三类知识库配置
  • -
  • 优先级设置
  • -
  • 动态启用/禁用
  • -
  • 知识库内容维护
  • -
  • 检索历史记录
  • -
-
- -
-

📄 文献管理

-
    -
  • 文献检索结果展示
  • -
  • 文献选择标记
  • -
  • 批量下载
  • -
  • 多数据库账号配置
  • -
  • 下载状态追踪
  • -
-
- -
-

💊 药物信息管理

-
    -
  • 药物基本信息
  • -
  • 多源安全性信息
  • -
  • 5类信息来源
  • -
  • 14种安全分类
  • -
  • 验证机制
  • -
-
-
-
- -
-

⚙️ 核心功能模块(续)

-
-
-

🔬 临床试验管理

-
    -
  • ClinicalTrials.gov集成
  • -
  • 关键词搜索
  • -
  • 数据展示与详情
  • -
  • CSV导出
  • -
  • 数据持久化
  • -
-
- -
-

⚙️ 系统配置

-
    -
  • Dify AI引擎配置
  • -
  • 大模型API设置
  • -
  • 文献下载账号
  • -
  • 用户权限管理
  • -
  • 系统参数调整
  • -
-
- -
-

📊 工作台

-
    -
  • 待处理任务统计
  • -
  • 最近查询记录
  • -
  • 快速操作入口
  • -
  • 系统状态监控
  • -
  • 数据统计图表
  • -
-
- -
-

📋 审核日志

-
    -
  • 操作追踪记录
  • -
  • 审核历史查询
  • -
  • 用户操作统计
  • -
  • 异常操作提醒
  • -
  • 数据审计
  • -
-
-
-
- -
-

🔍 药物安全信息管理 - 亮点功能

-
-
-

📊 多源信息整合

-

整合5类信息来源,全面展示药物安全性信息:

-
-
-
📁
-
内部数据
-
-
-
📚
-
文献数据
-
-
-
🏛️
-
监管信息
-
-
-
💬
-
自媒体
-
-
-
🔬
-
临床试验
-
-
-
- -
-

🎯 14种安全信息分类

-
-
✓ 不良反应
-
✓ 药物相互作用
-
✓ 禁忌症
-
✓ 特殊人群用药
-
✓ 儿童用药
-
✓ 老年用药
-
✓ 妊娠哺乳期
-
✓ 肝功能不全
-
✓ 肾功能不全
-
✓ 长期使用影响
-
✓ 注意事项
-
✓ 警告信息
-
✓ 过量
-
✓ 停药症状
-
-
-
-
- -
-

🤖 AI智能功能

-
-
-

🧠 集成Dify AI引擎

-
    -
  • 关键词智能提取 - 自动识别药物名称、疾病、问题类型
  • -
  • 信息智能整合 - 从多个数据源整合相关信息
  • -
  • 回复智能生成 - AI辅助生成专业的回复内容
  • -
  • 内容质量把控 - 人工审核确保准确性
  • -
-
- -
- - - 查询内容 - - - - - Dify AI - 关键词提取 - 内容整理 - - - - - 回复草稿 - + 文献列表 - -
-
-
-
- - -
-
-

🏗️ 技术架构

-
-
-

☕ 后端技术

-
    -
  • Java 17
  • -
  • Spring Boot 3.2
  • -
  • Spring Data JPA
  • -
  • Spring Security
  • -
  • Maven
  • -
-
- -
-

🎨 前端技术

-
    -
  • Vue 3
  • -
  • Vite
  • -
  • Element Plus
  • -
  • Pinia (状态管理)
  • -
  • Axios
  • -
-
- -
-

💾 数据存储

-
    -
  • MySQL 8.0
  • -
  • JPA / Hibernate
  • -
  • 数据库连接池
  • -
  • 事务管理
  • -
-
- -
-

🚀 部署方案

-
    -
  • Docker
  • -
  • Docker Compose
  • -
  • Nginx
  • -
  • 一键启动脚本
  • -
-
-
-
- -
-

🎨 系统架构图

-
- - - - 👤 用户层 (Vue 3 前端) - - - - - - - 🚪 API Gateway (RESTful) - - - - - - - 🔧 业务逻辑层 (Spring Boot) - - - - 查询管理 - Service - - - 知识库 - Service - - - 药物管理 - Service - - - 临床试验 - Service - - - - - - - 💾 数据持久层 (JPA + MySQL) - - - - 🤖 Dify AI - 引擎 - - - 🔬 Clinical - Trials API - - - 📚 文献数据库 - - - - - - - - 前后端分离架构 | RESTful API | 微服务设计 - -
-
- -
-

🔐 安全性设计

-
-
-

🔒 用户认证与授权

-
    -
  • Spring Security 安全框架
  • -
  • 基于角色的权限控制(RBAC)
  • -
  • JWT Token认证
  • -
  • 密码加密存储
  • -
-
- -
-

📋 操作审计

-
    -
  • 完整的操作日志记录
  • -
  • 用户行为追踪
  • -
  • 审核流程记录
  • -
  • 异常操作告警
  • -
-
- -
-

🛡️ 数据安全

-
    -
  • 敏感信息加密存储
  • -
  • API密钥安全管理
  • -
  • 数据库访问控制
  • -
  • 定期备份机制
  • -
-
-
-
-
- - -
-
-

📖 使用指南 - 快速开始

-
-

方式一:Docker部署(推荐)

-
- 步骤 1: 确保Docker Desktop已安装并运行 -
-
- 步骤 2: 配置环境变量(.env文件) -
-
- 步骤 3: 运行启动脚本 -
docker-compose up -d
-
-
- 步骤 4: 访问系统 -
    -
  • 前端: http://localhost
  • -
  • 后端API: http://localhost:8080/api
  • -
-
-
- 步骤 5: 使用默认账号登录 -
    -
  • 用户名: admin
  • -
  • 密码: admin123
  • -
-
-
-
- -
-

📝 核心操作流程

-
-
-

1️⃣ 创建查询请求

-
    -
  • 方式A:上传标准化Excel表格(批量处理)
  • -
  • 方式B:手动填写查询内容(单个处理)
  • -
  • 系统自动分配查询编号
  • -
  • 设置查询优先级
  • -
-
- -
-

2️⃣ AI处理与检索

-
    -
  • 点击"提取关键词"按钮,AI自动提取
  • -
  • 确认关键词后,点击"开始检索"
  • -
  • 系统自动在多个知识库中检索
  • -
  • 实时查看检索进度
  • -
-
- -
-

3️⃣ 审核与发送

-
    -
  • 查看AI生成的回复草稿
  • -
  • 人工审核修改(如需要)
  • -
  • 选择需要下载的文献
  • -
  • 点击"完成"发送给客户
  • -
-
-
-
- -
-

🔍 查询药物信息

-
-
- 步骤 1: 点击左侧菜单"药物信息" -
-
- 步骤 2: 在列表中搜索或浏览药物 -
-
- 步骤 3: 点击任意药物查看详情 -
-
- 步骤 4: 切换标签页查看不同来源的安全信息 -
- 内部数据 - 文献数据 - 监管信息 - 自媒体 - 临床试验 -
-
-
- 步骤 5: 查看详细的安全信息 -
    -
  • 严重程度标识(轻度/中度/重度/严重)
  • -
  • 验证状态(已验证/未验证)
  • -
  • 参考来源链接
  • -
-
-
-
- -
-

🔬 检索临床试验

-
-
- 步骤 1: 在查询详情页面找到"临床试验信息"模块 -
-
- 步骤 2: 输入搜索关键词(药物名、疾病名等) -
-
- 步骤 3: 点击"搜索",等待3-10秒 -
-
- 步骤 4: 查看搜索结果 -
    -
  • NCT ID可点击跳转到官网
  • -
  • 点击展开按钮查看详细信息
  • -
  • 查看研究状态、阶段、入组人数等
  • -
-
-
- 步骤 5: 导出数据 -
    -
  • 点击"导出CSV"按钮
  • -
  • 获得包含完整信息的Excel文件
  • -
  • 用于进一步分析或报告
  • -
-
-
-
- -
-

⚙️ 系统配置

-
-
-

🤖 AI引擎配置

-
    -
  • 配置Dify API密钥
  • -
  • 设置API访问地址
  • -
  • 选择使用的大模型
  • -
  • 调整AI参数(温度、长度等)
  • -
-
- -
-

📚 知识库配置

-
    -
  • 添加新的知识库
  • -
  • 设置知识库优先级
  • -
  • 配置知识库访问地址和API
  • -
  • 启用/禁用特定知识库
  • -
  • 上传企业内部文档
  • -
-
- -
-

👥 用户管理

-
    -
  • 添加新用户
  • -
  • 分配用户角色(管理员/专员/审核员)
  • -
  • 设置权限
  • -
  • 重置密码
  • -
-
-
-
-
- - -
-
-

💎 系统价值

-
-
-

⚡ 效率提升

-

传统方式:数小时至数天 → 系统处理:10-30分钟

-

效率提升80%以上

-
- -
-

🎯 质量保证

-
    -
  • 多源数据整合,信息更全面
  • -
  • AI辅助+人工审核,双重保障
  • -
  • 标准化流程,减少人为错误
  • -
  • 历史记录可追溯,持续优化
  • -
-
- -
-

💰 成本节约

-
    -
  • 减少人工检索时间
  • -
  • 降低信息遗漏风险
  • -
  • 提高团队生产力
  • -
  • 支持更多客户咨询
  • -
-
- -
-

📈 知识积累

-
    -
  • 建立企业知识库
  • -
  • 历史查询可复用
  • -
  • 持续优化回复质量
  • -
  • 新员工快速上手
  • -
-
-
-
- -
-

📊 实际应用场景

-
-
-

场景1:医生咨询达菲林最新临床数据

-
    -
  • 传统方式:人工搜索多个数据库,整理资料,2-3天
  • -
  • 使用系统:输入关键词,AI自动检索ClinicalTrials.gov,生成报告,30分钟
  • -
  • 效果:快速响应客户,提升满意度
  • -
-
- -
-

场景2:查询药物安全性信息

-
    -
  • 传统方式:查阅说明书、文献、监管网站,信息分散,半天
  • -
  • 使用系统:一键查看多源安全信息,分类清晰,5分钟
  • -
  • 效果:信息全面,不会遗漏重要内容
  • -
-
- -
-

场景3:处理批量咨询

-
    -
  • 传统方式:逐个处理,重复劳动,1周
  • -
  • 使用系统:上传Excel批量处理,AI并行检索,1天
  • -
  • 效果:大幅提高处理能力
  • -
-
-
-
- -
-

🚀 未来展望

-
-
-

📊 数据分析增强

-
    -
  • 咨询趋势分析
  • -
  • 热点问题识别
  • -
  • 客户需求预测
  • -
  • 智能推荐系统
  • -
-
- -
-

🤖 AI能力提升

-
    -
  • 更智能的关键词提取
  • -
  • 自动生成专业报告
  • -
  • 多语言支持
  • -
  • 语音交互
  • -
-
- -
-

🔗 集成扩展

-
    -
  • 更多文献数据库
  • -
  • 监管机构实时对接
  • -
  • 邮件系统集成
  • -
  • 移动端应用
  • -
-
-
-
- -
-

📞 支持与培训

-
-
-

📚 培训计划

-
    -
  • 基础培训:系统登录、基本操作(1小时)
  • -
  • 进阶培训:查询创建、AI使用、审核流程(2小时)
  • -
  • 管理培训:系统配置、知识库管理、用户管理(2小时)
  • -
  • 持续支持:线上答疑、定期回访
  • -
-
- -
-

📖 文档资源

-
    -
  • 用户操作手册
  • -
  • 管理员配置指南
  • -
  • 常见问题解答(FAQ)
  • -
  • 视频教程
  • -
  • 最佳实践案例
  • -
-
- -
-

🛠️ 技术支持

-
    -
  • 邮件支持
  • -
  • 远程协助
  • -
  • 系统更新维护
  • -
  • 定制化开发
  • -
-
-
-
-
- - -
-
-

感谢观看!

-
- 医学信息支持系统
- 让专业服务更高效、更智能 -
-
-

🚀 开始使用:访问 http://localhost

-

📧 技术支持:technical-support@ipsen.com

-

📚 文档中心:查看项目根目录的MD文档

-
-
-

为医学信息团队赋能 | 为客户提供更优质的服务

-
-
- -
-
- - - - - - - - diff --git a/screenshots/README.md b/screenshots/README.md deleted file mode 100644 index 0a138bb..0000000 --- a/screenshots/README.md +++ /dev/null @@ -1,152 +0,0 @@ -# 系统截图文件夹 - -本文件夹用于存放医学信息支持系统的界面截图,供演示文稿使用。 - -## 建议的截图清单 - -### 核心功能截图 - -1. **login.png** - 登录页面 - - 展示系统登录界面 - -2. **dashboard.png** - 工作台首页 - - 展示系统主界面和数据统计 - -3. **inquiry-list.png** - 查询列表页面 - - 展示查询请求列表 - -4. **inquiry-create.png** - 创建查询页面 - - 展示如何创建新查询 - -5. **inquiry-detail.png** - 查询详情页面 - - 展示查询处理流程 - -6. **drug-list.png** - 药物列表页面 - - 展示药物信息列表 - -7. **drug-detail.png** - 药物详情页面 - - 展示药物详细信息和安全性数据 - -8. **clinical-trials.png** - 临床试验页面 - - 展示临床试验搜索和结果 - -9. **knowledge-base.png** - 知识库管理页面 - - 展示知识库配置 - -10. **system-config.png** - 系统配置页面 - - 展示系统设置界面 - -## 截图要求 - -### 技术要求 -- **格式**:PNG(推荐)或 JPG -- **分辨率**:建议 1920x1080 或更高 -- **文件大小**:每张不超过 2MB - -### 内容要求 -- 使用**测试数据**或**脱敏数据**,不要包含真实敏感信息 -- 界面要**完整清晰**,避免截图不全 -- 确保**中文显示正常**,没有乱码 -- 尽量展示**有数据的状态**,避免空白页面 - -### 美观建议 -- 关闭不必要的浏览器工具栏和书签栏 -- 使用**全屏模式**截图(F11) -- 确保界面**布局整齐**,没有错位 -- 选择**有代表性的数据**进行展示 - -## 如何在演示文稿中使用截图 - -### 方法一:直接嵌入 -编辑 `presentation.html`,在需要的位置添加: - -```html -
-

系统界面展示

- 工作台界面 -

系统工作台提供一站式的信息概览

-
-``` - -### 方法二:创建对比展示 -```html -
-

操作流程

-
-
- -

步骤1:创建查询

-
-
- -

步骤2:处理查询

-
-
-
-``` - -### 方法三:全屏展示 -```html -
-
-

工作台界面

-

清晰的数据展示,一目了然

-
-
-``` - -## 快速截图工具推荐 - -### Windows -- **Snipping Tool** (系统自带) -- **Snagit** (专业工具) -- **ShareX** (免费开源) - -### Mac -- **Command + Shift + 4** (系统快捷键) -- **Command + Shift + 5** (截屏工具) - -### 浏览器扩展 -- **Awesome Screenshot** -- **Nimbus Screenshot** -- **Full Page Screen Capture** - -## 截图技巧 - -### 1. 准备测试数据 -在截图前,确保系统中有: -- 几条示例查询记录 -- 药物信息(如达菲林) -- 临床试验搜索结果 -- 知识库配置示例 - -### 2. 统一浏览器设置 -- 缩放比例:100% -- 窗口大小:最大化或1920x1080 -- 主题:浅色(更适合演示) - -### 3. 批量截图流程 -1. 登录系统 -2. 按照功能模块依次截图 -3. 立即重命名保存(避免混淆) -4. 检查所有截图质量 -5. 选择最佳的添加到演示文稿 - -## 注意事项 - -⚠️ **隐私保护** -- 不要截取包含真实患者信息的页面 -- 不要截取包含真实医生信息的页面 -- 使用测试账号进行截图 -- 模糊处理任何敏感信息 - -⚠️ **版权说明** -- 系统界面截图仅供内部演示使用 -- 对外发布前需获得授权 - ---- - -**截图完成后,删除本说明文件即可。** - - -