对项目进行了简化
This commit is contained in:
parent
f4af080b04
commit
442e3a2e57
5
.env
5
.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
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
406
DEPLOYMENT.md
406
DEPLOYMENT.md
|
|
@ -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 <repository-url>
|
||||
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部署
|
||||
|
||||
77
Dockerfile
77
Dockerfile
|
|
@ -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" ]
|
||||
|
|
@ -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和用户文档
|
||||
- **模块化设计**:便于功能扩展和维护
|
||||
|
||||
|
||||
|
|
@ -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"]
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
{
|
||||
"name": "backend",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {}
|
||||
}
|
||||
|
|
@ -1,129 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
|
||||
http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<parent>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-parent</artifactId>
|
||||
<version>2.7.18</version>
|
||||
<relativePath/>
|
||||
</parent>
|
||||
|
||||
<groupId>com.ipsen</groupId>
|
||||
<artifactId>medical-info-system</artifactId>
|
||||
<version>1.0.0</version>
|
||||
<name>Medical Information Support System</name>
|
||||
<description>Medical Information Support System for Ipsen</description>
|
||||
|
||||
<properties>
|
||||
<java.version>8</java.version>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<!-- Spring Boot Web -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Spring Boot Data JPA -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-data-jpa</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Spring Boot Validation -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-validation</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Spring Boot Security -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-security</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- MySQL Driver -->
|
||||
<dependency>
|
||||
<groupId>mysql</groupId>
|
||||
<artifactId>mysql-connector-java</artifactId>
|
||||
<version>8.0.33</version>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
|
||||
<!-- Lombok -->
|
||||
<dependency>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
|
||||
<!-- JWT -->
|
||||
<dependency>
|
||||
<groupId>io.jsonwebtoken</groupId>
|
||||
<artifactId>jjwt-api</artifactId>
|
||||
<version>0.11.5</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.jsonwebtoken</groupId>
|
||||
<artifactId>jjwt-impl</artifactId>
|
||||
<version>0.11.5</version>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.jsonwebtoken</groupId>
|
||||
<artifactId>jjwt-jackson</artifactId>
|
||||
<version>0.11.5</version>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
|
||||
<!-- Apache POI for Excel -->
|
||||
<dependency>
|
||||
<groupId>org.apache.poi</groupId>
|
||||
<artifactId>poi-ooxml</artifactId>
|
||||
<version>5.2.5</version>
|
||||
</dependency>
|
||||
|
||||
<!-- HTTP Client for Dify API -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-webflux</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- JSON Processing -->
|
||||
<dependency>
|
||||
<groupId>com.fasterxml.jackson.core</groupId>
|
||||
<artifactId>jackson-databind</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Testing -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||
<configuration>
|
||||
<excludes>
|
||||
<exclude>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
</exclude>
|
||||
</excludes>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
||||
|
||||
|
|
@ -1,130 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
|
||||
http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<parent>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-parent</artifactId>
|
||||
<version>2.7.18</version>
|
||||
<relativePath/>
|
||||
</parent>
|
||||
|
||||
<groupId>com.ipsen</groupId>
|
||||
<artifactId>medical-info-system</artifactId>
|
||||
<version>1.0.0</version>
|
||||
<name>Medical Information Support System</name>
|
||||
<description>Medical Information Support System for Ipsen</description>
|
||||
|
||||
<properties>
|
||||
<java.version>8</java.version>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<!-- Spring Boot Web -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Spring Boot Data JPA -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-data-jpa</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Spring Boot Validation -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-validation</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Spring Boot Security -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-security</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- MySQL Driver -->
|
||||
<dependency>
|
||||
<groupId>mysql</groupId>
|
||||
<artifactId>mysql-connector-java</artifactId>
|
||||
<version>8.0.33</version>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
|
||||
<!-- Lombok -->
|
||||
<dependency>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
|
||||
<!-- JWT -->
|
||||
<dependency>
|
||||
<groupId>io.jsonwebtoken</groupId>
|
||||
<artifactId>jjwt-api</artifactId>
|
||||
<version>0.11.5</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.jsonwebtoken</groupId>
|
||||
<artifactId>jjwt-impl</artifactId>
|
||||
<version>0.11.5</version>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.jsonwebtoken</groupId>
|
||||
<artifactId>jjwt-jackson</artifactId>
|
||||
<version>0.11.5</version>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
|
||||
<!-- Apache POI for Excel -->
|
||||
<dependency>
|
||||
<groupId>org.apache.poi</groupId>
|
||||
<artifactId>poi-ooxml</artifactId>
|
||||
<version>5.2.5</version>
|
||||
</dependency>
|
||||
|
||||
<!-- HTTP Client for Dify API -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-webflux</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- JSON Processing -->
|
||||
<dependency>
|
||||
<groupId>com.fasterxml.jackson.core</groupId>
|
||||
<artifactId>jackson-databind</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Testing -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||
<configuration>
|
||||
<excludes>
|
||||
<exclude>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
</exclude>
|
||||
</excludes>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
||||
|
||||
|
||||
|
|
@ -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<ApiResponse<List<SearchResultItemDTO>>> getAllPendingDownloads() {
|
||||
List<SearchResultItemDTO> tasks = searchResultService.getPendingDownloads();
|
||||
return ResponseEntity.ok(ApiResponse.success(tasks));
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取特定查询请求的待下载任务
|
||||
*/
|
||||
@GetMapping("/inquiry/{inquiryId}/pending")
|
||||
public ResponseEntity<ApiResponse<List<SearchResultItemDTO>>> getPendingDownloadsByInquiry(
|
||||
@PathVariable Long inquiryId) {
|
||||
List<SearchResultItemDTO> tasks = searchResultService.getPendingDownloadsByInquiry(inquiryId);
|
||||
return ResponseEntity.ok(ApiResponse.success(tasks));
|
||||
}
|
||||
|
||||
/**
|
||||
* 标记下载任务为完成
|
||||
*/
|
||||
@PostMapping("/{id}/complete")
|
||||
public ResponseEntity<ApiResponse<SearchResultItemDTO>> 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<ApiResponse<SearchResultItemDTO>> 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));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -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<ApiResponse<InquiryRequestDTO>> 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<ApiResponse<InquiryRequestDTO>> selectDataSources(
|
||||
@PathVariable Long id,
|
||||
@RequestBody DataSourceSelectionDTO dataSourceDTO) {
|
||||
InquiryRequestDTO result = inquiryService.selectDataSources(id, dataSourceDTO);
|
||||
return ResponseEntity.ok(ApiResponse.success(result));
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行信息检索
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -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<ApiResponse<List<SearchResultItemDTO>>> getSearchResults(
|
||||
@PathVariable Long inquiryId) {
|
||||
List<SearchResultItemDTO> results = searchResultService.getActiveSearchResults(inquiryId);
|
||||
return ResponseEntity.ok(ApiResponse.success(results));
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新检索结果项
|
||||
*/
|
||||
@PutMapping("/{id}")
|
||||
public ResponseEntity<ApiResponse<SearchResultItemDTO>> updateSearchResult(
|
||||
@PathVariable Long id,
|
||||
@RequestBody SearchResultItemDTO dto) {
|
||||
SearchResultItemDTO result = searchResultService.updateSearchResult(id, dto);
|
||||
return ResponseEntity.ok(ApiResponse.success(result));
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量更新检索结果项
|
||||
*/
|
||||
@PutMapping("/batch")
|
||||
public ResponseEntity<ApiResponse<List<SearchResultItemDTO>>> batchUpdateSearchResults(
|
||||
@RequestBody List<SearchResultItemDTO> dtos) {
|
||||
List<SearchResultItemDTO> results = searchResultService.batchUpdateSearchResults(dtos);
|
||||
return ResponseEntity.ok(ApiResponse.success(results));
|
||||
}
|
||||
|
||||
/**
|
||||
* 标记为错误并删除
|
||||
*/
|
||||
@DeleteMapping("/{id}")
|
||||
public ResponseEntity<ApiResponse<Void>> markAsDeleted(@PathVariable Long id) {
|
||||
searchResultService.markAsDeleted(id);
|
||||
return ResponseEntity.ok(ApiResponse.success(null));
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置是否纳入回复参考资料
|
||||
*/
|
||||
@PostMapping("/{id}/include")
|
||||
public ResponseEntity<ApiResponse<SearchResultItemDTO>> setIncludeInResponse(
|
||||
@PathVariable Long id,
|
||||
@RequestParam Boolean include) {
|
||||
SearchResultItemDTO result = searchResultService.setIncludeInResponse(id, include);
|
||||
return ResponseEntity.ok(ApiResponse.success(result));
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置是否需要下载全文
|
||||
*/
|
||||
@PostMapping("/{id}/download")
|
||||
public ResponseEntity<ApiResponse<SearchResultItemDTO>> setNeedDownload(
|
||||
@PathVariable Long id,
|
||||
@RequestParam Boolean need) {
|
||||
SearchResultItemDTO result = searchResultService.setNeedDownload(id, need);
|
||||
return ResponseEntity.ok(ApiResponse.success(result));
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有待下载的结果项
|
||||
*/
|
||||
@GetMapping("/pending-downloads")
|
||||
public ResponseEntity<ApiResponse<List<SearchResultItemDTO>>> getPendingDownloads() {
|
||||
List<SearchResultItemDTO> results = searchResultService.getPendingDownloads();
|
||||
return ResponseEntity.ok(ApiResponse.success(results));
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取查询请求的待下载结果项
|
||||
*/
|
||||
@GetMapping("/inquiry/{inquiryId}/pending-downloads")
|
||||
public ResponseEntity<ApiResponse<List<SearchResultItemDTO>>> getPendingDownloadsByInquiry(
|
||||
@PathVariable Long inquiryId) {
|
||||
List<SearchResultItemDTO> results = searchResultService.getPendingDownloadsByInquiry(inquiryId);
|
||||
return ResponseEntity.ok(ApiResponse.success(results));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -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<String, Object> health() {
|
||||
Map<String, Object> 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<String> 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<Map<String, Object>> checkDifyConfig() {
|
||||
Map<String, Object> 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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -48,3 +48,5 @@ public class ClinicalTrialDTO {
|
|||
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -27,3 +27,5 @@ public class ClinicalTrialsSearchResult {
|
|||
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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中检索
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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<String> additionalKeywords; // 用户添加的其他关键词
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -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<MaterialReference> 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; // 相关性说明
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -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; // 状态
|
||||
|
|
|
|||
|
|
@ -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 // 失败
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -37,3 +37,5 @@ public interface ClinicalTrialRepository extends JpaRepository<ClinicalTrial, Lo
|
|||
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,46 @@
|
|||
package com.ipsen.medical.repository;
|
||||
|
||||
import com.ipsen.medical.entity.SearchResultItem;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 检索结果项存储库
|
||||
*/
|
||||
@Repository
|
||||
public interface SearchResultItemRepository extends JpaRepository<SearchResultItem, Long> {
|
||||
|
||||
/**
|
||||
* 根据查询请求ID查找所有结果项
|
||||
*/
|
||||
List<SearchResultItem> findByInquiryRequestId(Long inquiryRequestId);
|
||||
|
||||
/**
|
||||
* 根据查询请求ID查找需要纳入回复的结果项
|
||||
*/
|
||||
List<SearchResultItem> findByInquiryRequestIdAndIncludeInResponseTrue(Long inquiryRequestId);
|
||||
|
||||
/**
|
||||
* 根据查询请求ID查找需要下载的结果项
|
||||
*/
|
||||
List<SearchResultItem> findByInquiryRequestIdAndNeedDownloadTrue(Long inquiryRequestId);
|
||||
|
||||
/**
|
||||
* 根据查询请求ID查找未删除的结果项
|
||||
*/
|
||||
List<SearchResultItem> findByInquiryRequestIdAndIsDeletedFalse(Long inquiryRequestId);
|
||||
|
||||
/**
|
||||
* 根据查询请求ID删除所有结果项
|
||||
*/
|
||||
void deleteByInquiryRequestId(Long inquiryRequestId);
|
||||
|
||||
/**
|
||||
* 查找所有待下载的结果项
|
||||
*/
|
||||
List<SearchResultItem> findByDownloadStatus(SearchResultItem.DownloadStatus status);
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -50,3 +50,5 @@ public interface ClinicalTrialsService {
|
|||
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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<SearchResultItemDTO> performMultiSourceSearch(Long inquiryId);
|
||||
|
||||
/**
|
||||
* 在内部数据中检索
|
||||
*/
|
||||
List<SearchResultItemDTO> searchInternalData(Long inquiryId, String keywords);
|
||||
|
||||
/**
|
||||
* 在知识库中检索
|
||||
*/
|
||||
List<SearchResultItemDTO> searchKnowledgeBase(Long inquiryId, String keywords);
|
||||
|
||||
/**
|
||||
* 在知网中检索
|
||||
*/
|
||||
List<SearchResultItemDTO> searchCnki(Long inquiryId, String keywords);
|
||||
|
||||
/**
|
||||
* 在ClinicalTrials中检索
|
||||
*/
|
||||
List<SearchResultItemDTO> searchClinicalTrials(Long inquiryId, String keywords);
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -0,0 +1,68 @@
|
|||
package com.ipsen.medical.service;
|
||||
|
||||
import com.ipsen.medical.dto.SearchResultItemDTO;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 检索结果服务接口
|
||||
*/
|
||||
public interface SearchResultService {
|
||||
|
||||
/**
|
||||
* 获取查询请求的所有检索结果
|
||||
*/
|
||||
List<SearchResultItemDTO> getSearchResults(Long inquiryRequestId);
|
||||
|
||||
/**
|
||||
* 获取查询请求的未删除检索结果
|
||||
*/
|
||||
List<SearchResultItemDTO> getActiveSearchResults(Long inquiryRequestId);
|
||||
|
||||
/**
|
||||
* 更新检索结果项
|
||||
*/
|
||||
SearchResultItemDTO updateSearchResult(Long id, SearchResultItemDTO dto);
|
||||
|
||||
/**
|
||||
* 批量更新检索结果项
|
||||
*/
|
||||
List<SearchResultItemDTO> batchUpdateSearchResults(List<SearchResultItemDTO> dtos);
|
||||
|
||||
/**
|
||||
* 标记为错误并删除
|
||||
*/
|
||||
void markAsDeleted(Long id);
|
||||
|
||||
/**
|
||||
* 设置是否纳入回复参考资料
|
||||
*/
|
||||
SearchResultItemDTO setIncludeInResponse(Long id, Boolean include);
|
||||
|
||||
/**
|
||||
* 设置是否需要下载全文
|
||||
*/
|
||||
SearchResultItemDTO setNeedDownload(Long id, Boolean need);
|
||||
|
||||
/**
|
||||
* 获取所有待下载的结果项
|
||||
*/
|
||||
List<SearchResultItemDTO> getPendingDownloads();
|
||||
|
||||
/**
|
||||
* 获取查询请求的待下载结果项
|
||||
*/
|
||||
List<SearchResultItemDTO> getPendingDownloadsByInquiry(Long inquiryRequestId);
|
||||
|
||||
/**
|
||||
* 创建检索结果项
|
||||
*/
|
||||
SearchResultItemDTO createSearchResult(SearchResultItemDTO dto);
|
||||
|
||||
/**
|
||||
* 批量创建检索结果项
|
||||
*/
|
||||
List<SearchResultItemDTO> batchCreateSearchResults(List<SearchResultItemDTO> dtos);
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -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<UserDTO> getAllUsers();
|
||||
|
||||
/**
|
||||
* 更新用户
|
||||
*/
|
||||
UserDTO updateUser(Long id, UserDTO userDTO);
|
||||
|
||||
/**
|
||||
* 删除用户
|
||||
*/
|
||||
void deleteUser(Long id);
|
||||
|
||||
/**
|
||||
* 启用/禁用用户
|
||||
*/
|
||||
UserDTO toggleUserStatus(Long id, Boolean enabled);
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -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();
|
||||
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
// 执行多源检索
|
||||
multiSourceSearchService.performMultiSourceSearch(id);
|
||||
|
||||
InquiryRequest savedRequest = inquiryRequestRepository.save(inquiryRequest);
|
||||
return convertToDTO(savedRequest);
|
||||
// 重新加载更新后的请求
|
||||
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<SearchResultItemDTO> 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());
|
||||
|
|
|
|||
|
|
@ -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<SearchResultItemDTO> 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<SearchResultItemDTO> allResults = new ArrayList<>();
|
||||
|
||||
try {
|
||||
// 根据用户选择的数据源进行检索
|
||||
if (Boolean.TRUE.equals(inquiry.getSearchInternalData())) {
|
||||
log.info("Searching internal data");
|
||||
List<SearchResultItemDTO> internalResults = searchInternalData(inquiryId, keywords);
|
||||
allResults.addAll(internalResults);
|
||||
}
|
||||
|
||||
if (Boolean.TRUE.equals(inquiry.getSearchKnowledgeBase())) {
|
||||
log.info("Searching knowledge base");
|
||||
List<SearchResultItemDTO> kbResults = searchKnowledgeBase(inquiryId, keywords);
|
||||
allResults.addAll(kbResults);
|
||||
}
|
||||
|
||||
if (Boolean.TRUE.equals(inquiry.getSearchCnki())) {
|
||||
log.info("Searching CNKI");
|
||||
List<SearchResultItemDTO> cnkiResults = searchCnki(inquiryId, keywords);
|
||||
allResults.addAll(cnkiResults);
|
||||
}
|
||||
|
||||
if (Boolean.TRUE.equals(inquiry.getSearchClinicalTrials())) {
|
||||
log.info("Searching ClinicalTrials");
|
||||
List<SearchResultItemDTO> 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<SearchResultItemDTO> searchInternalData(Long inquiryId, String keywords) {
|
||||
log.info("Searching internal data for inquiry: {}", inquiryId);
|
||||
|
||||
List<SearchResultItemDTO> results = new ArrayList<>();
|
||||
|
||||
// TODO: 实现内部数据检索逻辑
|
||||
// 这里应该查询企业内部研究数据、历史回复等
|
||||
log.warn("Internal data search not yet implemented");
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<SearchResultItemDTO> searchKnowledgeBase(Long inquiryId, String keywords) {
|
||||
log.info("Searching knowledge base for inquiry: {}", inquiryId);
|
||||
|
||||
List<SearchResultItemDTO> 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<SearchResultItemDTO> searchCnki(Long inquiryId, String keywords) {
|
||||
log.info("Searching CNKI for inquiry: {}", inquiryId);
|
||||
|
||||
List<SearchResultItemDTO> results = new ArrayList<>();
|
||||
|
||||
// TODO: 实现知网检索逻辑
|
||||
// 这里应该调用知网API或爬虫
|
||||
log.warn("CNKI search not yet implemented");
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<SearchResultItemDTO> searchClinicalTrials(Long inquiryId, String keywords) {
|
||||
log.info("Searching ClinicalTrials for inquiry: {}", inquiryId);
|
||||
|
||||
List<SearchResultItemDTO> results = new ArrayList<>();
|
||||
|
||||
try {
|
||||
// 解析关键词
|
||||
KeywordConfirmationDTO keywordDTO = objectMapper.readValue(keywords, KeywordConfirmationDTO.class);
|
||||
String searchKeyword = buildSearchKeyword(keywordDTO);
|
||||
|
||||
// 使用ClinicalTrialsService搜索
|
||||
List<ClinicalTrialDTO> 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();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -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<SearchResultItemDTO> getSearchResults(Long inquiryRequestId) {
|
||||
List<SearchResultItem> items = searchResultItemRepository.findByInquiryRequestId(inquiryRequestId);
|
||||
return items.stream()
|
||||
.map(this::convertToDTO)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<SearchResultItemDTO> getActiveSearchResults(Long inquiryRequestId) {
|
||||
List<SearchResultItem> 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<SearchResultItemDTO> batchUpdateSearchResults(List<SearchResultItemDTO> 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<SearchResultItemDTO> getPendingDownloads() {
|
||||
List<SearchResultItem> items = searchResultItemRepository.findByDownloadStatus(
|
||||
SearchResultItem.DownloadStatus.PENDING);
|
||||
return items.stream()
|
||||
.map(this::convertToDTO)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<SearchResultItemDTO> getPendingDownloadsByInquiry(Long inquiryRequestId) {
|
||||
List<SearchResultItem> 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<SearchResultItemDTO> batchCreateSearchResults(List<SearchResultItemDTO> 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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -213,3 +213,5 @@ SELECT '药物模块初始化完成!' AS message,
|
|||
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
@ -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
|
||||
|
||||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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;"]
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
@ -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";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
@ -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
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -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' }
|
||||
}
|
||||
]
|
||||
},
|
||||
|
|
|
|||
|
|
@ -0,0 +1,437 @@
|
|||
<template>
|
||||
<div class="data-source-selection">
|
||||
<el-card v-loading="loading">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>选择数据源 - {{ inquiry.requestNumber }}</span>
|
||||
<el-button @click="goBack" text>返回</el-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 关键词展示 -->
|
||||
<el-alert
|
||||
title="已确认的关键词"
|
||||
type="success"
|
||||
:closable="false"
|
||||
style="margin-bottom: 20px;"
|
||||
>
|
||||
<div class="keywords-display">
|
||||
<el-tag
|
||||
v-if="keywords.drugNameChinese"
|
||||
type="success"
|
||||
size="large"
|
||||
style="margin-right: 10px;"
|
||||
>
|
||||
{{ keywords.drugNameChinese }}
|
||||
</el-tag>
|
||||
<el-tag
|
||||
v-if="keywords.drugNameEnglish"
|
||||
type="primary"
|
||||
size="large"
|
||||
style="margin-right: 10px;"
|
||||
>
|
||||
{{ keywords.drugNameEnglish }}
|
||||
</el-tag>
|
||||
<el-tag
|
||||
v-if="keywords.requestItem"
|
||||
type="warning"
|
||||
size="large"
|
||||
style="margin-right: 10px;"
|
||||
>
|
||||
{{ keywords.requestItem }}
|
||||
</el-tag>
|
||||
<el-tag
|
||||
v-for="(tag, index) in keywords.additionalKeywords"
|
||||
:key="index"
|
||||
type="info"
|
||||
size="large"
|
||||
style="margin-right: 10px;"
|
||||
>
|
||||
{{ tag }}
|
||||
</el-tag>
|
||||
</div>
|
||||
</el-alert>
|
||||
|
||||
<!-- 数据源选择 -->
|
||||
<div class="source-selection">
|
||||
<h3 style="margin-bottom: 20px;">请选择需要检索的数据源:</h3>
|
||||
|
||||
<el-form :model="form" label-position="top">
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-card
|
||||
shadow="hover"
|
||||
:class="{ 'source-card': true, 'selected': form.searchInternalData }"
|
||||
@click="toggleSource('searchInternalData')"
|
||||
>
|
||||
<div class="source-content">
|
||||
<el-checkbox
|
||||
v-model="form.searchInternalData"
|
||||
size="large"
|
||||
@click.stop
|
||||
/>
|
||||
<div class="source-info">
|
||||
<div class="source-title">
|
||||
<el-icon :size="24" color="#409EFF"><Document /></el-icon>
|
||||
<span>内部数据</span>
|
||||
</div>
|
||||
<div class="source-desc">
|
||||
企业自有研究数据、历史回复记录、内部文献等
|
||||
</div>
|
||||
<el-tag size="small" type="primary">企业数据</el-tag>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
|
||||
<el-col :span="12">
|
||||
<el-card
|
||||
shadow="hover"
|
||||
:class="{ 'source-card': true, 'selected': form.searchKnowledgeBase }"
|
||||
@click="toggleSource('searchKnowledgeBase')"
|
||||
>
|
||||
<div class="source-content">
|
||||
<el-checkbox
|
||||
v-model="form.searchKnowledgeBase"
|
||||
size="large"
|
||||
@click.stop
|
||||
/>
|
||||
<div class="source-info">
|
||||
<div class="source-title">
|
||||
<el-icon :size="24" color="#67C23A"><FolderOpened /></el-icon>
|
||||
<span>知识库</span>
|
||||
</div>
|
||||
<div class="source-desc">
|
||||
已整理的企业知识库数据、专家意见等
|
||||
</div>
|
||||
<el-tag size="small" type="success">推荐</el-tag>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
|
||||
<el-col :span="12">
|
||||
<el-card
|
||||
shadow="hover"
|
||||
:class="{ 'source-card': true, 'selected': form.searchCnki }"
|
||||
@click="toggleSource('searchCnki')"
|
||||
>
|
||||
<div class="source-content">
|
||||
<el-checkbox
|
||||
v-model="form.searchCnki"
|
||||
size="large"
|
||||
@click.stop
|
||||
/>
|
||||
<div class="source-info">
|
||||
<div class="source-title">
|
||||
<el-icon :size="24" color="#E6A23C"><Reading /></el-icon>
|
||||
<span>知网 (CNKI)</span>
|
||||
</div>
|
||||
<div class="source-desc">
|
||||
中国知网学术文献数据库
|
||||
</div>
|
||||
<el-tag size="small" type="warning">中文文献</el-tag>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
|
||||
<el-col :span="12">
|
||||
<el-card
|
||||
shadow="hover"
|
||||
:class="{ 'source-card': true, 'selected': form.searchClinicalTrials }"
|
||||
@click="toggleSource('searchClinicalTrials')"
|
||||
>
|
||||
<div class="source-content">
|
||||
<el-checkbox
|
||||
v-model="form.searchClinicalTrials"
|
||||
size="large"
|
||||
@click.stop
|
||||
/>
|
||||
<div class="source-info">
|
||||
<div class="source-title">
|
||||
<el-icon :size="24" color="#F56C6C"><Experiment /></el-icon>
|
||||
<span>ClinicalTrials.gov</span>
|
||||
</div>
|
||||
<div class="source-desc">
|
||||
美国国家医学图书馆临床试验数据库
|
||||
</div>
|
||||
<el-tag size="small" type="danger">临床试验</el-tag>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-form>
|
||||
|
||||
<!-- 选择提示 -->
|
||||
<el-alert
|
||||
v-if="!hasSelectedSource"
|
||||
title="请至少选择一个数据源"
|
||||
type="warning"
|
||||
:closable="false"
|
||||
style="margin-top: 20px;"
|
||||
/>
|
||||
|
||||
<el-alert
|
||||
v-else
|
||||
:title="`已选择 ${selectedCount} 个数据源`"
|
||||
type="info"
|
||||
:closable="false"
|
||||
style="margin-top: 20px;"
|
||||
>
|
||||
<div>将在以下数据源中进行检索:{{ selectedSourcesText }}</div>
|
||||
</el-alert>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<div class="action-buttons">
|
||||
<el-button
|
||||
type="primary"
|
||||
size="large"
|
||||
@click="handleConfirm"
|
||||
:loading="confirming"
|
||||
:disabled="!hasSelectedSource"
|
||||
>
|
||||
确认并开始检索
|
||||
</el-button>
|
||||
<el-button size="large" @click="handleSelectAll">
|
||||
全选
|
||||
</el-button>
|
||||
<el-button size="large" @click="handleClearAll">
|
||||
清空
|
||||
</el-button>
|
||||
<el-button size="large" @click="goBack">
|
||||
返回
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, computed, onMounted } from 'vue'
|
||||
import { useRouter, useRoute } from 'vue-router'
|
||||
import { getInquiryDetail, selectDataSources, performSearch } from '@/api/inquiry'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { Document, FolderOpened, Reading, Experiment } from '@element-plus/icons-vue'
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
|
||||
const loading = ref(false)
|
||||
const confirming = ref(false)
|
||||
const inquiry = ref({})
|
||||
const keywords = ref({})
|
||||
|
||||
const form = reactive({
|
||||
searchInternalData: false,
|
||||
searchKnowledgeBase: true, // 默认选中推荐项
|
||||
searchCnki: false,
|
||||
searchClinicalTrials: true // 默认选中推荐项
|
||||
})
|
||||
|
||||
const hasSelectedSource = computed(() => {
|
||||
return form.searchInternalData ||
|
||||
form.searchKnowledgeBase ||
|
||||
form.searchCnki ||
|
||||
form.searchClinicalTrials
|
||||
})
|
||||
|
||||
const selectedCount = computed(() => {
|
||||
let count = 0
|
||||
if (form.searchInternalData) count++
|
||||
if (form.searchKnowledgeBase) count++
|
||||
if (form.searchCnki) count++
|
||||
if (form.searchClinicalTrials) count++
|
||||
return count
|
||||
})
|
||||
|
||||
const selectedSourcesText = computed(() => {
|
||||
const sources = []
|
||||
if (form.searchInternalData) sources.push('内部数据')
|
||||
if (form.searchKnowledgeBase) sources.push('知识库')
|
||||
if (form.searchCnki) sources.push('知网')
|
||||
if (form.searchClinicalTrials) sources.push('ClinicalTrials.gov')
|
||||
return sources.join('、')
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
loadDetail()
|
||||
})
|
||||
|
||||
const loadDetail = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const id = route.params.id
|
||||
const data = await getInquiryDetail(id)
|
||||
inquiry.value = data
|
||||
|
||||
// 解析关键词
|
||||
if (data.keywords) {
|
||||
try {
|
||||
keywords.value = JSON.parse(data.keywords)
|
||||
} catch (error) {
|
||||
console.error('解析关键词失败', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 如果已有数据源选择,恢复状态
|
||||
if (data.searchInternalData !== null) {
|
||||
form.searchInternalData = data.searchInternalData || false
|
||||
}
|
||||
if (data.searchKnowledgeBase !== null) {
|
||||
form.searchKnowledgeBase = data.searchKnowledgeBase || false
|
||||
}
|
||||
if (data.searchCnki !== null) {
|
||||
form.searchCnki = data.searchCnki || false
|
||||
}
|
||||
if (data.searchClinicalTrials !== null) {
|
||||
form.searchClinicalTrials = data.searchClinicalTrials || false
|
||||
}
|
||||
} catch (error) {
|
||||
ElMessage.error('加载详情失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const toggleSource = (source) => {
|
||||
form[source] = !form[source]
|
||||
}
|
||||
|
||||
const handleSelectAll = () => {
|
||||
form.searchInternalData = true
|
||||
form.searchKnowledgeBase = true
|
||||
form.searchCnki = true
|
||||
form.searchClinicalTrials = true
|
||||
}
|
||||
|
||||
const handleClearAll = () => {
|
||||
form.searchInternalData = false
|
||||
form.searchKnowledgeBase = false
|
||||
form.searchCnki = false
|
||||
form.searchClinicalTrials = false
|
||||
}
|
||||
|
||||
const handleConfirm = async () => {
|
||||
if (!hasSelectedSource.value) {
|
||||
ElMessage.warning('请至少选择一个数据源')
|
||||
return
|
||||
}
|
||||
|
||||
ElMessageBox.confirm(
|
||||
`确认在以下数据源中检索:${selectedSourcesText.value}?`,
|
||||
'确认检索',
|
||||
{
|
||||
confirmButtonText: '开始检索',
|
||||
cancelButtonText: '取消',
|
||||
type: 'info'
|
||||
}
|
||||
).then(async () => {
|
||||
confirming.value = true
|
||||
try {
|
||||
// 保存数据源选择
|
||||
await selectDataSources(inquiry.value.id, form)
|
||||
ElMessage.success('数据源选择已保存')
|
||||
|
||||
// 开始检索
|
||||
await performSearch(inquiry.value.id)
|
||||
ElMessage.success('检索已完成,正在跳转到结果页面...')
|
||||
|
||||
// 跳转到检索结果页面
|
||||
router.push(`/inquiry/${inquiry.value.id}/search-results`)
|
||||
} catch (error) {
|
||||
ElMessage.error('操作失败: ' + (error.message || '未知错误'))
|
||||
} finally {
|
||||
confirming.value = false
|
||||
}
|
||||
}).catch(() => {
|
||||
// 用户取消
|
||||
})
|
||||
}
|
||||
|
||||
const goBack = () => {
|
||||
router.back()
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.data-source-selection {
|
||||
width: 100%;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.keywords-display {
|
||||
padding: 10px 0;
|
||||
}
|
||||
|
||||
.source-selection {
|
||||
padding: 20px 0;
|
||||
}
|
||||
|
||||
.source-card {
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.source-card:hover {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.source-card.selected {
|
||||
border: 2px solid #409EFF;
|
||||
background-color: #ecf5ff;
|
||||
}
|
||||
|
||||
.source-content {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.source-info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.source-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 10px;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.source-desc {
|
||||
color: #606266;
|
||||
line-height: 1.6;
|
||||
margin-bottom: 10px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
margin-top: 30px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.action-buttons .el-button {
|
||||
margin: 0 10px;
|
||||
}
|
||||
|
||||
:deep(.el-checkbox__label) {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
|
|
@ -0,0 +1,484 @@
|
|||
<template>
|
||||
<div class="download-tasks">
|
||||
<el-card v-loading="loading">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>待下载任务</span>
|
||||
<div>
|
||||
<el-button @click="handleRefresh" icon="Refresh" :loading="loading">
|
||||
刷新
|
||||
</el-button>
|
||||
<el-button @click="goBack" text>返回</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 统计信息 -->
|
||||
<el-row :gutter="20" style="margin-bottom: 20px;">
|
||||
<el-col :span="6">
|
||||
<el-statistic title="待下载任务" :value="pendingTasks.length" value-style="color: #E6A23C">
|
||||
<template #suffix>个</template>
|
||||
</el-statistic>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-statistic title="下载中" :value="downloadingCount" value-style="color: #409EFF">
|
||||
<template #suffix>个</template>
|
||||
</el-statistic>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-statistic title="已完成" :value="completedCount" value-style="color: #67C23A">
|
||||
<template #suffix>个</template>
|
||||
</el-statistic>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-statistic title="失败" :value="failedCount" value-style="color: #F56C6C">
|
||||
<template #suffix>个</template>
|
||||
</el-statistic>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<!-- 说明信息 -->
|
||||
<el-alert
|
||||
title="关于自动化下载"
|
||||
type="info"
|
||||
:closable="false"
|
||||
style="margin-bottom: 20px;"
|
||||
>
|
||||
<div>
|
||||
此页面显示所有待下载的文献列表。可供自动化下载工具读取并执行下载任务。
|
||||
<br />
|
||||
下载完成后,请更新任务状态。
|
||||
</div>
|
||||
</el-alert>
|
||||
|
||||
<!-- 筛选器 -->
|
||||
<div class="filters">
|
||||
<el-radio-group v-model="filterStatus" @change="loadTasks">
|
||||
<el-radio-button label="all">全部任务</el-radio-button>
|
||||
<el-radio-button label="PENDING">待下载</el-radio-button>
|
||||
<el-radio-button label="DOWNLOADING">下载中</el-radio-button>
|
||||
<el-radio-button label="COMPLETED">已完成</el-radio-button>
|
||||
<el-radio-button label="FAILED">失败</el-radio-button>
|
||||
</el-radio-group>
|
||||
<el-select
|
||||
v-model="selectedInquiryId"
|
||||
placeholder="筛选查询请求"
|
||||
clearable
|
||||
style="width: 300px; margin-left: 15px;"
|
||||
@change="loadTasks"
|
||||
>
|
||||
<el-option
|
||||
v-for="inquiry in uniqueInquiries"
|
||||
:key="inquiry.id"
|
||||
:label="`${inquiry.requestNumber} - ${inquiry.title}`"
|
||||
:value="inquiry.id"
|
||||
/>
|
||||
</el-select>
|
||||
</div>
|
||||
|
||||
<!-- 任务列表 -->
|
||||
<el-table :data="displayTasks" border stripe style="margin-top: 20px;">
|
||||
<el-table-column type="expand">
|
||||
<template #default="{ row }">
|
||||
<div class="expand-content">
|
||||
<el-descriptions :column="2" border>
|
||||
<el-descriptions-item label="任务ID">
|
||||
{{ row.id }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="查询请求ID">
|
||||
<el-link
|
||||
type="primary"
|
||||
@click="goToInquiry(row.inquiryRequestId)"
|
||||
>
|
||||
#{{ row.inquiryRequestId }}
|
||||
</el-link>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="标题" :span="2">
|
||||
{{ row.title }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="作者" :span="2">
|
||||
{{ row.authors || 'N/A' }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="摘要" :span="2">
|
||||
<div style="white-space: pre-wrap; max-height: 150px; overflow-y: auto;">
|
||||
{{ row.summary || 'N/A' }}
|
||||
</div>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="DOI" v-if="row.doi">
|
||||
<a :href="`https://doi.org/${row.doi}`" target="_blank" style="color: #409EFF;">
|
||||
{{ row.doi }}
|
||||
</a>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="PMID" v-if="row.pmid">
|
||||
<a :href="`https://pubmed.ncbi.nlm.nih.gov/${row.pmid}`" target="_blank" style="color: #409EFF;">
|
||||
{{ row.pmid }}
|
||||
</a>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="NCT ID" v-if="row.nctId">
|
||||
<a :href="`https://clinicaltrials.gov/study/${row.nctId}`" target="_blank" style="color: #409EFF;">
|
||||
{{ row.nctId }}
|
||||
</a>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="来源链接" :span="2" v-if="row.sourceUrl">
|
||||
<a :href="row.sourceUrl" target="_blank" style="color: #409EFF; word-break: break-all;">
|
||||
{{ row.sourceUrl }}
|
||||
</a>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="文件路径" :span="2" v-if="row.filePath">
|
||||
{{ row.filePath }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="创建时间">
|
||||
{{ formatDateTime(row.createdAt) }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="下载时间" v-if="row.downloadedAt">
|
||||
{{ formatDateTime(row.downloadedAt) }}
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="id" label="ID" width="80" />
|
||||
<el-table-column prop="title" label="标题" min-width="300" show-overflow-tooltip />
|
||||
<el-table-column prop="source" label="来源" width="150">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="getSourceTagType(row.source)" size="small">
|
||||
{{ row.source }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="downloadStatus" label="状态" width="120">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="getStatusTagType(row.downloadStatus)" size="small">
|
||||
{{ getStatusText(row.downloadStatus) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="下载链接" width="150">
|
||||
<template #default="{ row }">
|
||||
<el-button
|
||||
v-if="row.sourceUrl"
|
||||
type="primary"
|
||||
size="small"
|
||||
link
|
||||
@click="openLink(row.sourceUrl)"
|
||||
>
|
||||
打开链接
|
||||
</el-button>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="250" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<div style="display: flex; flex-direction: column; gap: 5px;">
|
||||
<el-button-group>
|
||||
<el-button
|
||||
size="small"
|
||||
type="success"
|
||||
@click="handleMarkComplete(row)"
|
||||
:disabled="row.downloadStatus === 'COMPLETED'"
|
||||
>
|
||||
标记完成
|
||||
</el-button>
|
||||
<el-button
|
||||
size="small"
|
||||
type="danger"
|
||||
@click="handleMarkFailed(row)"
|
||||
:disabled="row.downloadStatus === 'COMPLETED'"
|
||||
>
|
||||
标记失败
|
||||
</el-button>
|
||||
</el-button-group>
|
||||
<el-button
|
||||
size="small"
|
||||
@click="copyDownloadInfo(row)"
|
||||
>
|
||||
复制信息
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<el-empty
|
||||
v-if="displayTasks.length === 0"
|
||||
description="暂无下载任务"
|
||||
style="margin-top: 40px;"
|
||||
/>
|
||||
|
||||
<!-- 批量导出按钮 -->
|
||||
<div style="margin-top: 20px; text-align: right;">
|
||||
<el-button
|
||||
type="primary"
|
||||
@click="handleExportJson"
|
||||
:disabled="displayTasks.length === 0"
|
||||
>
|
||||
导出为JSON(供自动化工具使用)
|
||||
</el-button>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<!-- 标记完成对话框 -->
|
||||
<el-dialog v-model="completeDialogVisible" title="标记下载完成" width="500px">
|
||||
<el-form :model="completeForm" label-width="100px">
|
||||
<el-form-item label="文件路径">
|
||||
<el-input
|
||||
v-model="completeForm.filePath"
|
||||
placeholder="请输入下载后的文件路径"
|
||||
clearable
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="completeDialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="submitComplete">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { getPendingDownloads, markDownloadComplete, markDownloadFailed } from '@/api/searchResult'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
const loading = ref(false)
|
||||
const tasks = ref([])
|
||||
const filterStatus = ref('all')
|
||||
const selectedInquiryId = ref(null)
|
||||
const completeDialogVisible = ref(false)
|
||||
const currentTask = ref(null)
|
||||
const completeForm = ref({
|
||||
filePath: ''
|
||||
})
|
||||
|
||||
const pendingTasks = computed(() => {
|
||||
return tasks.value.filter(t => t.downloadStatus === 'PENDING')
|
||||
})
|
||||
|
||||
const downloadingCount = computed(() => {
|
||||
return tasks.value.filter(t => t.downloadStatus === 'DOWNLOADING').length
|
||||
})
|
||||
|
||||
const completedCount = computed(() => {
|
||||
return tasks.value.filter(t => t.downloadStatus === 'COMPLETED').length
|
||||
})
|
||||
|
||||
const failedCount = computed(() => {
|
||||
return tasks.value.filter(t => t.downloadStatus === 'FAILED').length
|
||||
})
|
||||
|
||||
const displayTasks = computed(() => {
|
||||
let filtered = tasks.value
|
||||
|
||||
// 按状态筛选
|
||||
if (filterStatus.value !== 'all') {
|
||||
filtered = filtered.filter(t => t.downloadStatus === filterStatus.value)
|
||||
}
|
||||
|
||||
// 按查询请求筛选
|
||||
if (selectedInquiryId.value) {
|
||||
filtered = filtered.filter(t => t.inquiryRequestId === selectedInquiryId.value)
|
||||
}
|
||||
|
||||
return filtered
|
||||
})
|
||||
|
||||
const uniqueInquiries = computed(() => {
|
||||
const inquiryMap = new Map()
|
||||
tasks.value.forEach(task => {
|
||||
if (!inquiryMap.has(task.inquiryRequestId)) {
|
||||
inquiryMap.set(task.inquiryRequestId, {
|
||||
id: task.inquiryRequestId,
|
||||
requestNumber: `REQ${task.inquiryRequestId}`,
|
||||
title: task.title?.substring(0, 30) || '未命名'
|
||||
})
|
||||
}
|
||||
})
|
||||
return Array.from(inquiryMap.values())
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
loadTasks()
|
||||
})
|
||||
|
||||
const loadTasks = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const data = await getPendingDownloads()
|
||||
tasks.value = data || []
|
||||
} catch (error) {
|
||||
ElMessage.error('加载下载任务失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleRefresh = () => {
|
||||
loadTasks()
|
||||
}
|
||||
|
||||
const handleMarkComplete = (row) => {
|
||||
currentTask.value = row
|
||||
completeForm.value.filePath = row.filePath || ''
|
||||
completeDialogVisible.value = true
|
||||
}
|
||||
|
||||
const submitComplete = async () => {
|
||||
if (!completeForm.value.filePath) {
|
||||
ElMessage.warning('请输入文件路径')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
await markDownloadComplete(currentTask.value.id, completeForm.value.filePath)
|
||||
ElMessage.success('已标记为完成')
|
||||
completeDialogVisible.value = false
|
||||
loadTasks()
|
||||
} catch (error) {
|
||||
ElMessage.error('操作失败')
|
||||
}
|
||||
}
|
||||
|
||||
const handleMarkFailed = (row) => {
|
||||
ElMessageBox.prompt('请输入失败原因(可选)', '标记失败', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
inputPlaceholder: '失败原因'
|
||||
}).then(async ({ value }) => {
|
||||
try {
|
||||
await markDownloadFailed(row.id, value || '')
|
||||
ElMessage.success('已标记为失败')
|
||||
loadTasks()
|
||||
} catch (error) {
|
||||
ElMessage.error('操作失败')
|
||||
}
|
||||
}).catch(() => {})
|
||||
}
|
||||
|
||||
const openLink = (url) => {
|
||||
window.open(url, '_blank')
|
||||
}
|
||||
|
||||
const copyDownloadInfo = (row) => {
|
||||
const info = {
|
||||
id: row.id,
|
||||
title: row.title,
|
||||
source: row.source,
|
||||
sourceUrl: row.sourceUrl,
|
||||
doi: row.doi,
|
||||
pmid: row.pmid,
|
||||
nctId: row.nctId
|
||||
}
|
||||
|
||||
const text = JSON.stringify(info, null, 2)
|
||||
navigator.clipboard.writeText(text).then(() => {
|
||||
ElMessage.success('已复制到剪贴板')
|
||||
}).catch(() => {
|
||||
ElMessage.error('复制失败')
|
||||
})
|
||||
}
|
||||
|
||||
const handleExportJson = () => {
|
||||
const exportData = displayTasks.value.map(task => ({
|
||||
id: task.id,
|
||||
inquiryRequestId: task.inquiryRequestId,
|
||||
title: task.title,
|
||||
authors: task.authors,
|
||||
source: task.source,
|
||||
sourceUrl: task.sourceUrl,
|
||||
doi: task.doi,
|
||||
pmid: task.pmid,
|
||||
nctId: task.nctId,
|
||||
downloadStatus: task.downloadStatus,
|
||||
createdAt: task.createdAt
|
||||
}))
|
||||
|
||||
const blob = new Blob([JSON.stringify(exportData, null, 2)], { type: 'application/json' })
|
||||
const url = URL.createObjectURL(blob)
|
||||
const a = document.createElement('a')
|
||||
a.href = url
|
||||
a.download = `download-tasks-${Date.now()}.json`
|
||||
a.click()
|
||||
URL.revokeObjectURL(url)
|
||||
ElMessage.success('导出成功')
|
||||
}
|
||||
|
||||
const goToInquiry = (inquiryId) => {
|
||||
router.push(`/inquiry/${inquiryId}`)
|
||||
}
|
||||
|
||||
const formatDateTime = (dateTime) => {
|
||||
if (!dateTime) return '-'
|
||||
return new Date(dateTime).toLocaleString('zh-CN')
|
||||
}
|
||||
|
||||
const getStatusTagType = (status) => {
|
||||
const typeMap = {
|
||||
'PENDING': 'warning',
|
||||
'DOWNLOADING': 'primary',
|
||||
'COMPLETED': 'success',
|
||||
'FAILED': 'danger',
|
||||
'NOT_REQUIRED': 'info'
|
||||
}
|
||||
return typeMap[status] || 'info'
|
||||
}
|
||||
|
||||
const getStatusText = (status) => {
|
||||
const textMap = {
|
||||
'PENDING': '待下载',
|
||||
'DOWNLOADING': '下载中',
|
||||
'COMPLETED': '已完成',
|
||||
'FAILED': '失败',
|
||||
'NOT_REQUIRED': '不需要'
|
||||
}
|
||||
return textMap[status] || status
|
||||
}
|
||||
|
||||
const getSourceTagType = (source) => {
|
||||
const typeMap = {
|
||||
'内部数据': 'primary',
|
||||
'知识库': 'success',
|
||||
'知网': 'warning',
|
||||
'ClinicalTrials.gov': 'danger',
|
||||
'CNKI': 'warning'
|
||||
}
|
||||
return typeMap[source] || 'info'
|
||||
}
|
||||
|
||||
const goBack = () => {
|
||||
router.back()
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.download-tasks {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.filters {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
.expand-content {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
:deep(.el-statistic__content) {
|
||||
font-size: 28px;
|
||||
font-weight: 600;
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
|
|
@ -14,7 +14,14 @@
|
|||
<el-descriptions-item label="请求编号">
|
||||
{{ inquiry.requestNumber }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="客户姓名">
|
||||
<el-descriptions-item label="客户姓名"> <el-steps :active="currentStep" align-center>
|
||||
<el-step title="提取关键词" description="使用AI提取查询关键词" />
|
||||
<el-step title="信息检索" description="检索相关文献和数据" />
|
||||
<el-step title="生成回复" description="整理信息生成回复" />
|
||||
<el-step title="审核回复" description="人工审核回复内容" />
|
||||
<el-step title="下载文献" description="下载相关文献" />
|
||||
<el-step title="完成" description="处理完成" />
|
||||
</el-steps>
|
||||
{{ inquiry.customerName }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="客户邮箱">
|
||||
|
|
@ -36,16 +43,50 @@
|
|||
|
||||
<el-divider />
|
||||
|
||||
<el-steps :active="currentStep" align-center>
|
||||
<el-step title="提取关键词" description="使用AI提取查询关键词" />
|
||||
<el-step title="信息检索" description="检索相关文献和数据" />
|
||||
<el-step title="生成回复" description="整理信息生成回复" />
|
||||
<el-step title="审核回复" description="人工审核回复内容" />
|
||||
<el-step title="下载文献" description="下载相关文献" />
|
||||
<el-step title="完成" description="处理完成" />
|
||||
</el-steps>
|
||||
|
||||
|
||||
<div class="action-section">
|
||||
<!-- 快捷导航按钮 -->
|
||||
<el-button
|
||||
v-if="inquiry.status === 'PENDING'"
|
||||
type="primary"
|
||||
@click="goToKeywordConfirmation"
|
||||
>
|
||||
开始处理(关键词提取)
|
||||
</el-button>
|
||||
|
||||
<el-button
|
||||
v-if="inquiry.status === 'KEYWORD_EXTRACTED' && !inquiry.keywordsConfirmed"
|
||||
type="primary"
|
||||
@click="goToKeywordConfirmation"
|
||||
>
|
||||
确认关键词
|
||||
</el-button>
|
||||
|
||||
<el-button
|
||||
v-if="inquiry.keywordsConfirmed && inquiry.status === 'KEYWORD_EXTRACTED'"
|
||||
type="success"
|
||||
@click="goToDataSourceSelection"
|
||||
>
|
||||
选择数据源并检索
|
||||
</el-button>
|
||||
|
||||
<el-button
|
||||
v-if="inquiry.status === 'SEARCH_COMPLETED'"
|
||||
type="warning"
|
||||
@click="goToSearchResults"
|
||||
>
|
||||
查看检索结果
|
||||
</el-button>
|
||||
|
||||
<el-button
|
||||
v-if="inquiry.responseContent"
|
||||
type="info"
|
||||
@click="goToResponseView"
|
||||
>
|
||||
查看生成的回复
|
||||
</el-button>
|
||||
|
||||
<!-- AI智能流程按钮 -->
|
||||
<el-button
|
||||
v-if="inquiry.status === 'PENDING' || inquiry.status === 'KEYWORD_EXTRACTED'"
|
||||
|
|
@ -622,6 +663,22 @@ const getKeywordField = (keywords, field) => {
|
|||
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`)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,338 @@
|
|||
<template>
|
||||
<div class="keyword-confirmation">
|
||||
<el-card v-loading="loading">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>关键词确认 - {{ inquiry.requestNumber }}</span>
|
||||
<el-button @click="goBack" text>返回</el-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 查询内容展示 -->
|
||||
<el-alert
|
||||
title="查询内容"
|
||||
type="info"
|
||||
:closable="false"
|
||||
style="margin-bottom: 20px;"
|
||||
>
|
||||
<div style="white-space: pre-wrap; line-height: 1.8;">
|
||||
{{ inquiry.inquiryContent }}
|
||||
</div>
|
||||
</el-alert>
|
||||
|
||||
<!-- AI提取结果 -->
|
||||
<div v-if="!inquiry.keywords" class="extract-section">
|
||||
<el-empty description="尚未提取关键词">
|
||||
<el-button type="primary" @click="handleExtract" :loading="extracting">
|
||||
开始提取关键词
|
||||
</el-button>
|
||||
</el-empty>
|
||||
</div>
|
||||
|
||||
<!-- 关键词编辑表单 -->
|
||||
<div v-else class="keyword-form">
|
||||
<el-alert
|
||||
title="AI已自动提取以下关键词,请确认或修改"
|
||||
type="success"
|
||||
:closable="false"
|
||||
style="margin-bottom: 20px;"
|
||||
/>
|
||||
|
||||
<el-form ref="formRef" :model="form" :rules="rules" label-width="120px">
|
||||
<el-form-item label="药物中文名" prop="drugNameChinese">
|
||||
<el-input
|
||||
v-model="form.drugNameChinese"
|
||||
placeholder="请输入药物中文名称"
|
||||
clearable
|
||||
>
|
||||
<template #prepend>
|
||||
<el-icon><Pills /></el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="药物英文名" prop="drugNameEnglish">
|
||||
<el-input
|
||||
v-model="form.drugNameEnglish"
|
||||
placeholder="请输入药物英文名称"
|
||||
clearable
|
||||
>
|
||||
<template #prepend>
|
||||
<el-icon><Pills /></el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="查询项目" prop="requestItem">
|
||||
<el-input
|
||||
v-model="form.requestItem"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
placeholder="请输入查询的具体项目或问题"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="额外关键词">
|
||||
<el-tag
|
||||
v-for="(tag, index) in form.additionalKeywords"
|
||||
:key="index"
|
||||
closable
|
||||
@close="removeKeyword(index)"
|
||||
style="margin-right: 10px; margin-bottom: 10px;"
|
||||
>
|
||||
{{ tag }}
|
||||
</el-tag>
|
||||
<el-input
|
||||
v-if="inputVisible"
|
||||
ref="inputRef"
|
||||
v-model="inputValue"
|
||||
size="small"
|
||||
style="width: 150px;"
|
||||
@keyup.enter="handleInputConfirm"
|
||||
@blur="handleInputConfirm"
|
||||
/>
|
||||
<el-button
|
||||
v-else
|
||||
size="small"
|
||||
@click="showInput"
|
||||
>
|
||||
+ 添加关键词
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
|
||||
<!-- 关键词预览 -->
|
||||
<el-form-item label="关键词预览">
|
||||
<div class="keyword-preview">
|
||||
<el-tag
|
||||
v-if="form.drugNameChinese"
|
||||
type="success"
|
||||
size="large"
|
||||
style="margin-right: 10px; margin-bottom: 10px;"
|
||||
>
|
||||
{{ form.drugNameChinese }}
|
||||
</el-tag>
|
||||
<el-tag
|
||||
v-if="form.drugNameEnglish"
|
||||
type="primary"
|
||||
size="large"
|
||||
style="margin-right: 10px; margin-bottom: 10px;"
|
||||
>
|
||||
{{ form.drugNameEnglish }}
|
||||
</el-tag>
|
||||
<el-tag
|
||||
v-if="form.requestItem"
|
||||
type="warning"
|
||||
size="large"
|
||||
style="margin-right: 10px; margin-bottom: 10px;"
|
||||
>
|
||||
{{ form.requestItem }}
|
||||
</el-tag>
|
||||
<el-tag
|
||||
v-for="(tag, index) in form.additionalKeywords"
|
||||
:key="'preview-' + index"
|
||||
type="info"
|
||||
size="large"
|
||||
style="margin-right: 10px; margin-bottom: 10px;"
|
||||
>
|
||||
{{ tag }}
|
||||
</el-tag>
|
||||
</div>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item>
|
||||
<el-button
|
||||
type="primary"
|
||||
@click="handleConfirm"
|
||||
:loading="confirming"
|
||||
size="large"
|
||||
>
|
||||
确认关键词并继续
|
||||
</el-button>
|
||||
<el-button @click="handleReExtract" :loading="extracting">
|
||||
重新提取
|
||||
</el-button>
|
||||
<el-button @click="goBack">取消</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, nextTick, onMounted } from 'vue'
|
||||
import { useRouter, useRoute } from 'vue-router'
|
||||
import { getInquiryDetail, extractKeywords, confirmKeywords } from '@/api/inquiry'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { Pills } from '@element-plus/icons-vue'
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
|
||||
const loading = ref(false)
|
||||
const extracting = ref(false)
|
||||
const confirming = ref(false)
|
||||
const inquiry = ref({})
|
||||
const formRef = ref(null)
|
||||
const inputRef = ref(null)
|
||||
const inputVisible = ref(false)
|
||||
const inputValue = ref('')
|
||||
|
||||
const form = reactive({
|
||||
drugNameChinese: '',
|
||||
drugNameEnglish: '',
|
||||
requestItem: '',
|
||||
additionalKeywords: []
|
||||
})
|
||||
|
||||
const rules = {
|
||||
drugNameChinese: [
|
||||
{ required: false, message: '请输入药物中文名称', trigger: 'blur' }
|
||||
],
|
||||
drugNameEnglish: [
|
||||
{ required: false, message: '请输入药物英文名称', trigger: 'blur' }
|
||||
],
|
||||
requestItem: [
|
||||
{ required: false, message: '请输入查询项目', trigger: 'blur' }
|
||||
]
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadDetail()
|
||||
})
|
||||
|
||||
const loadDetail = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const id = route.params.id
|
||||
const data = await getInquiryDetail(id)
|
||||
inquiry.value = data
|
||||
|
||||
// 如果已有关键词,解析并填充表单
|
||||
if (data.keywords) {
|
||||
parseKeywords(data.keywords)
|
||||
}
|
||||
} catch (error) {
|
||||
ElMessage.error('加载详情失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const parseKeywords = (keywordsStr) => {
|
||||
try {
|
||||
const keywords = JSON.parse(keywordsStr)
|
||||
form.drugNameChinese = keywords.drugNameChinese || ''
|
||||
form.drugNameEnglish = keywords.drugNameEnglish || ''
|
||||
form.requestItem = keywords.requestItem || ''
|
||||
form.additionalKeywords = keywords.additionalKeywords || []
|
||||
} catch (error) {
|
||||
console.error('解析关键词失败', error)
|
||||
}
|
||||
}
|
||||
|
||||
const handleExtract = async () => {
|
||||
extracting.value = true
|
||||
try {
|
||||
await extractKeywords(inquiry.value.id)
|
||||
ElMessage.success('关键词提取成功')
|
||||
await loadDetail()
|
||||
} catch (error) {
|
||||
ElMessage.error('关键词提取失败: ' + (error.message || '未知错误'))
|
||||
} finally {
|
||||
extracting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleReExtract = async () => {
|
||||
extracting.value = true
|
||||
try {
|
||||
await extractKeywords(inquiry.value.id)
|
||||
ElMessage.success('关键词重新提取成功')
|
||||
await loadDetail()
|
||||
} catch (error) {
|
||||
ElMessage.error('关键词提取失败')
|
||||
} finally {
|
||||
extracting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleConfirm = async () => {
|
||||
if (!formRef.value) return
|
||||
|
||||
await formRef.value.validate(async (valid) => {
|
||||
if (valid) {
|
||||
confirming.value = true
|
||||
try {
|
||||
await confirmKeywords(inquiry.value.id, form)
|
||||
ElMessage.success('关键词确认成功')
|
||||
// 跳转到数据源选择页面
|
||||
router.push(`/inquiry/${inquiry.value.id}/data-source-selection`)
|
||||
} catch (error) {
|
||||
ElMessage.error('确认失败: ' + (error.message || '未知错误'))
|
||||
} finally {
|
||||
confirming.value = false
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const showInput = () => {
|
||||
inputVisible.value = true
|
||||
nextTick(() => {
|
||||
inputRef.value?.focus()
|
||||
})
|
||||
}
|
||||
|
||||
const handleInputConfirm = () => {
|
||||
if (inputValue.value && !form.additionalKeywords.includes(inputValue.value)) {
|
||||
form.additionalKeywords.push(inputValue.value)
|
||||
}
|
||||
inputVisible.value = false
|
||||
inputValue.value = ''
|
||||
}
|
||||
|
||||
const removeKeyword = (index) => {
|
||||
form.additionalKeywords.splice(index, 1)
|
||||
}
|
||||
|
||||
const goBack = () => {
|
||||
router.back()
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.keyword-confirmation {
|
||||
width: 100%;
|
||||
max-width: 900px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.extract-section {
|
||||
padding: 40px 0;
|
||||
}
|
||||
|
||||
.keyword-form {
|
||||
padding: 20px 0;
|
||||
}
|
||||
|
||||
.keyword-preview {
|
||||
min-height: 50px;
|
||||
padding: 15px;
|
||||
background-color: #f5f7fa;
|
||||
border-radius: 4px;
|
||||
border: 1px dashed #dcdfe6;
|
||||
}
|
||||
|
||||
:deep(.el-form-item__label) {
|
||||
font-weight: 600;
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
|
|
@ -0,0 +1,494 @@
|
|||
<template>
|
||||
<div class="response-view">
|
||||
<el-card v-loading="loading">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>查看回复 - {{ inquiry.requestNumber }}</span>
|
||||
<div>
|
||||
<el-button @click="handlePrint" icon="Printer">打印</el-button>
|
||||
<el-button @click="handleExport" icon="Download">导出</el-button>
|
||||
<el-button @click="goBack" text>返回</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 查询信息 -->
|
||||
<el-descriptions :column="2" border style="margin-bottom: 20px;">
|
||||
<el-descriptions-item label="请求编号">
|
||||
{{ inquiry.requestNumber }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="状态">
|
||||
<el-tag :type="getStatusType(inquiry.status)">
|
||||
{{ getStatusText(inquiry.status) }}
|
||||
</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="客户姓名">
|
||||
{{ inquiry.customerName || '-' }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="客户邮箱">
|
||||
{{ inquiry.customerEmail || '-' }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="创建时间" :span="2">
|
||||
{{ formatDateTime(inquiry.createdAt) }}
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
|
||||
<!-- 回复内容展示区 -->
|
||||
<div v-if="!responseData" class="empty-response">
|
||||
<el-empty description="尚未生成回复">
|
||||
<el-button type="primary" @click="goToSearchResults">
|
||||
前往管理检索结果
|
||||
</el-button>
|
||||
</el-empty>
|
||||
</div>
|
||||
|
||||
<div v-else class="response-content" id="printArea">
|
||||
<!-- 问题部分 -->
|
||||
<section class="response-section">
|
||||
<h2 class="section-title">
|
||||
<el-icon><QuestionFilled /></el-icon>
|
||||
查询问题
|
||||
</h2>
|
||||
<div class="section-content">
|
||||
{{ responseData.question || inquiry.inquiryContent }}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 查询到的资料部分 -->
|
||||
<section class="response-section">
|
||||
<h2 class="section-title">
|
||||
<el-icon><Search /></el-icon>
|
||||
查询到的资料
|
||||
</h2>
|
||||
<div class="section-content">
|
||||
{{ responseData.queriedMaterials }}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 总结部分 -->
|
||||
<section class="response-section summary-section">
|
||||
<h2 class="section-title">
|
||||
<el-icon><Document /></el-icon>
|
||||
总结
|
||||
</h2>
|
||||
<div class="section-content summary-content">
|
||||
{{ responseData.summary }}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 详细资料列表部分 -->
|
||||
<section class="response-section">
|
||||
<h2 class="section-title">
|
||||
<el-icon><Collection /></el-icon>
|
||||
详细资料列表
|
||||
<el-tag type="info" style="margin-left: 10px;">
|
||||
共 {{ responseData.materialList?.length || 0 }} 条
|
||||
</el-tag>
|
||||
</h2>
|
||||
<div class="materials-list">
|
||||
<el-collapse v-if="responseData.materialList && responseData.materialList.length > 0">
|
||||
<el-collapse-item
|
||||
v-for="(material, index) in responseData.materialList"
|
||||
:key="index"
|
||||
:name="index"
|
||||
>
|
||||
<template #title>
|
||||
<div class="material-title">
|
||||
<span class="material-number">{{ index + 1 }}.</span>
|
||||
<span class="material-name">{{ material.title }}</span>
|
||||
</div>
|
||||
</template>
|
||||
<el-descriptions :column="1" border>
|
||||
<el-descriptions-item label="作者">
|
||||
{{ material.authors || 'N/A' }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="来源">
|
||||
<el-tag type="primary" size="small">{{ material.source }}</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="发表日期">
|
||||
{{ material.publicationDate || 'N/A' }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="链接" v-if="material.url">
|
||||
<a :href="material.url" target="_blank" style="color: #409EFF;">
|
||||
{{ material.url }}
|
||||
</a>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="摘要">
|
||||
<div style="white-space: pre-wrap; line-height: 1.8;">
|
||||
{{ material.summary }}
|
||||
</div>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="相关性说明">
|
||||
<div style="white-space: pre-wrap; line-height: 1.8; color: #67C23A;">
|
||||
{{ material.relevance }}
|
||||
</div>
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</el-collapse-item>
|
||||
</el-collapse>
|
||||
<el-empty v-else description="暂无资料" />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 生成时间 -->
|
||||
<div class="response-footer">
|
||||
<el-text type="info">
|
||||
生成时间:{{ formatDateTime(inquiry.updatedAt) }}
|
||||
</el-text>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<el-divider />
|
||||
<div class="action-buttons" v-if="responseData">
|
||||
<template v-if="inquiry.status === 'UNDER_REVIEW'">
|
||||
<el-button
|
||||
type="success"
|
||||
size="large"
|
||||
@click="handleApprove"
|
||||
:loading="processing"
|
||||
>
|
||||
批准回复
|
||||
</el-button>
|
||||
<el-button
|
||||
type="warning"
|
||||
size="large"
|
||||
@click="handleReject"
|
||||
:loading="processing"
|
||||
>
|
||||
要求修改
|
||||
</el-button>
|
||||
<el-button
|
||||
size="large"
|
||||
@click="handleRegenerate"
|
||||
:loading="processing"
|
||||
>
|
||||
重新生成
|
||||
</el-button>
|
||||
</template>
|
||||
<template v-else-if="inquiry.status === 'COMPLETED'">
|
||||
<el-alert
|
||||
title="此回复已批准并完成"
|
||||
type="success"
|
||||
:closable="false"
|
||||
show-icon
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { useRouter, useRoute } from 'vue-router'
|
||||
import { getInquiryDetail, reviewResponse, generateResponse } from '@/api/inquiry'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { QuestionFilled, Search, Document, Collection } from '@element-plus/icons-vue'
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
|
||||
const loading = ref(false)
|
||||
const processing = ref(false)
|
||||
const inquiry = ref({})
|
||||
const responseData = ref(null)
|
||||
|
||||
onMounted(() => {
|
||||
loadDetail()
|
||||
})
|
||||
|
||||
const loadDetail = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const id = route.params.id
|
||||
const data = await getInquiryDetail(id)
|
||||
inquiry.value = data
|
||||
|
||||
// 解析回复内容
|
||||
if (data.responseContent) {
|
||||
try {
|
||||
responseData.value = JSON.parse(data.responseContent)
|
||||
} catch (error) {
|
||||
// 如果不是JSON格式,作为纯文本处理
|
||||
responseData.value = {
|
||||
question: data.inquiryContent,
|
||||
queriedMaterials: '已查询相关资料',
|
||||
summary: data.responseContent,
|
||||
materialList: []
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
ElMessage.error('加载详情失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleApprove = async () => {
|
||||
ElMessageBox.confirm(
|
||||
'确认批准此回复?批准后将标记为已完成。',
|
||||
'批准回复',
|
||||
{
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'success'
|
||||
}
|
||||
).then(async () => {
|
||||
processing.value = true
|
||||
try {
|
||||
await reviewResponse(inquiry.value.id, { approved: true })
|
||||
ElMessage.success('回复已批准')
|
||||
loadDetail()
|
||||
} catch (error) {
|
||||
ElMessage.error('操作失败')
|
||||
} finally {
|
||||
processing.value = false
|
||||
}
|
||||
}).catch(() => {})
|
||||
}
|
||||
|
||||
const handleReject = async () => {
|
||||
ElMessageBox.prompt('请输入修改意见', '要求修改', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
inputType: 'textarea',
|
||||
inputPlaceholder: '请说明需要修改的内容'
|
||||
}).then(async ({ value }) => {
|
||||
if (!value) {
|
||||
ElMessage.warning('请输入修改意见')
|
||||
return
|
||||
}
|
||||
|
||||
processing.value = true
|
||||
try {
|
||||
await reviewResponse(inquiry.value.id, { approved: false, comments: value })
|
||||
ElMessage.success('已提交修改要求')
|
||||
loadDetail()
|
||||
} catch (error) {
|
||||
ElMessage.error('操作失败')
|
||||
} finally {
|
||||
processing.value = false
|
||||
}
|
||||
}).catch(() => {})
|
||||
}
|
||||
|
||||
const handleRegenerate = async () => {
|
||||
ElMessageBox.confirm(
|
||||
'确认重新生成回复?这将覆盖当前的回复内容。',
|
||||
'重新生成',
|
||||
{
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}
|
||||
).then(async () => {
|
||||
processing.value = true
|
||||
try {
|
||||
await generateResponse(inquiry.value.id)
|
||||
ElMessage.success('回复已重新生成')
|
||||
loadDetail()
|
||||
} catch (error) {
|
||||
ElMessage.error('生成失败: ' + (error.message || '未知错误'))
|
||||
} finally {
|
||||
processing.value = false
|
||||
}
|
||||
}).catch(() => {})
|
||||
}
|
||||
|
||||
const handlePrint = () => {
|
||||
window.print()
|
||||
}
|
||||
|
||||
const handleExport = () => {
|
||||
if (!responseData.value) {
|
||||
ElMessage.warning('暂无回复内容可导出')
|
||||
return
|
||||
}
|
||||
|
||||
// 导出为JSON
|
||||
const exportData = {
|
||||
requestNumber: inquiry.value.requestNumber,
|
||||
question: responseData.value.question,
|
||||
queriedMaterials: responseData.value.queriedMaterials,
|
||||
summary: responseData.value.summary,
|
||||
materialList: responseData.value.materialList,
|
||||
generatedAt: inquiry.value.updatedAt
|
||||
}
|
||||
|
||||
const blob = new Blob([JSON.stringify(exportData, null, 2)], { type: 'application/json' })
|
||||
const url = URL.createObjectURL(blob)
|
||||
const a = document.createElement('a')
|
||||
a.href = url
|
||||
a.download = `response-${inquiry.value.requestNumber}.json`
|
||||
a.click()
|
||||
URL.revokeObjectURL(url)
|
||||
ElMessage.success('导出成功')
|
||||
}
|
||||
|
||||
const goToSearchResults = () => {
|
||||
router.push(`/inquiry/${inquiry.value.id}/search-results`)
|
||||
}
|
||||
|
||||
const formatDateTime = (dateTime) => {
|
||||
if (!dateTime) return '-'
|
||||
return new Date(dateTime).toLocaleString('zh-CN')
|
||||
}
|
||||
|
||||
const getStatusType = (status) => {
|
||||
const typeMap = {
|
||||
'PENDING': 'info',
|
||||
'KEYWORD_EXTRACTED': 'warning',
|
||||
'SEARCHING': 'warning',
|
||||
'SEARCH_COMPLETED': '',
|
||||
'UNDER_REVIEW': 'warning',
|
||||
'DOWNLOADING': 'warning',
|
||||
'COMPLETED': 'success',
|
||||
'REJECTED': 'danger'
|
||||
}
|
||||
return typeMap[status] || 'info'
|
||||
}
|
||||
|
||||
const getStatusText = (status) => {
|
||||
const textMap = {
|
||||
'PENDING': '待处理',
|
||||
'KEYWORD_EXTRACTED': '已提取关键词',
|
||||
'SEARCHING': '检索中',
|
||||
'SEARCH_COMPLETED': '检索完成',
|
||||
'UNDER_REVIEW': '审核中',
|
||||
'DOWNLOADING': '下载中',
|
||||
'COMPLETED': '已完成',
|
||||
'REJECTED': '已拒绝'
|
||||
}
|
||||
return textMap[status] || status
|
||||
}
|
||||
|
||||
const goBack = () => {
|
||||
router.back()
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.response-view {
|
||||
width: 100%;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.empty-response {
|
||||
padding: 60px 0;
|
||||
}
|
||||
|
||||
.response-content {
|
||||
padding: 20px 0;
|
||||
}
|
||||
|
||||
.response-section {
|
||||
margin-bottom: 40px;
|
||||
padding: 20px;
|
||||
background-color: #f5f7fa;
|
||||
border-radius: 8px;
|
||||
border-left: 4px solid #409EFF;
|
||||
}
|
||||
|
||||
.summary-section {
|
||||
border-left-color: #67C23A;
|
||||
background: linear-gradient(135deg, #f5f7fa 0%, #e8f4f8 100%);
|
||||
}
|
||||
|
||||
.section-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
margin: 0 0 15px 0;
|
||||
}
|
||||
|
||||
.section-content {
|
||||
font-size: 16px;
|
||||
line-height: 2;
|
||||
color: #606266;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.summary-content {
|
||||
font-size: 17px;
|
||||
font-weight: 500;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.materials-list {
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
.material-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.material-number {
|
||||
margin-right: 10px;
|
||||
color: #409EFF;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.material-name {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.response-footer {
|
||||
text-align: right;
|
||||
padding-top: 20px;
|
||||
margin-top: 20px;
|
||||
border-top: 1px dashed #dcdfe6;
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
text-align: center;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.action-buttons .el-button {
|
||||
margin: 0 10px;
|
||||
}
|
||||
|
||||
/* 打印样式 */
|
||||
@media print {
|
||||
.card-header,
|
||||
.action-buttons,
|
||||
.el-divider {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.response-content {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.response-section {
|
||||
page-break-inside: avoid;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.el-collapse-item__header) {
|
||||
font-size: 16px;
|
||||
padding: 15px 20px;
|
||||
}
|
||||
|
||||
:deep(.el-collapse-item__content) {
|
||||
padding: 20px;
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
|
|
@ -0,0 +1,500 @@
|
|||
<template>
|
||||
<div class="search-results">
|
||||
<el-card v-loading="loading">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>检索结果管理 - {{ inquiry.requestNumber }}</span>
|
||||
<div>
|
||||
<el-button @click="handleRefresh" icon="Refresh" :loading="loading">
|
||||
刷新
|
||||
</el-button>
|
||||
<el-button @click="goBack" text>返回</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 统计信息 -->
|
||||
<el-row :gutter="20" style="margin-bottom: 20px;">
|
||||
<el-col :span="6">
|
||||
<el-statistic title="总结果数" :value="results.length">
|
||||
<template #suffix>条</template>
|
||||
</el-statistic>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-statistic title="已纳入回复" :value="includedCount" value-style="color: #67C23A">
|
||||
<template #suffix>条</template>
|
||||
</el-statistic>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-statistic title="需要下载" :value="downloadCount" value-style="color: #E6A23C">
|
||||
<template #suffix>条</template>
|
||||
</el-statistic>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-statistic title="已删除" :value="deletedCount" value-style="color: #F56C6C">
|
||||
<template #suffix>条</template>
|
||||
</el-statistic>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<!-- 批量操作 -->
|
||||
<div class="batch-actions">
|
||||
<el-button
|
||||
type="primary"
|
||||
@click="handleBatchInclude"
|
||||
:disabled="selectedResults.length === 0"
|
||||
>
|
||||
批量纳入回复
|
||||
</el-button>
|
||||
<el-button
|
||||
type="warning"
|
||||
@click="handleBatchDownload"
|
||||
:disabled="selectedResults.length === 0"
|
||||
>
|
||||
批量标记下载
|
||||
</el-button>
|
||||
<el-button
|
||||
type="danger"
|
||||
@click="handleBatchDelete"
|
||||
:disabled="selectedResults.length === 0"
|
||||
>
|
||||
批量删除
|
||||
</el-button>
|
||||
<el-button
|
||||
type="success"
|
||||
@click="handleGenerateResponse"
|
||||
:disabled="includedCount === 0"
|
||||
:loading="generating"
|
||||
>
|
||||
生成回复 ({{ includedCount }}条)
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<!-- 筛选器 -->
|
||||
<div class="filters">
|
||||
<el-radio-group v-model="filterType" @change="handleFilterChange">
|
||||
<el-radio-button label="all">全部 ({{ results.length }})</el-radio-button>
|
||||
<el-radio-button label="active">未删除 ({{ activeCount }})</el-radio-button>
|
||||
<el-radio-button label="included">已纳入 ({{ includedCount }})</el-radio-button>
|
||||
<el-radio-button label="download">需下载 ({{ downloadCount }})</el-radio-button>
|
||||
<el-radio-button label="deleted">已删除 ({{ deletedCount }})</el-radio-button>
|
||||
</el-radio-group>
|
||||
<el-input
|
||||
v-model="searchText"
|
||||
placeholder="搜索标题或内容"
|
||||
clearable
|
||||
style="width: 300px; margin-left: 15px;"
|
||||
prefix-icon="Search"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 结果列表 -->
|
||||
<el-table
|
||||
ref="tableRef"
|
||||
:data="filteredResults"
|
||||
@selection-change="handleSelectionChange"
|
||||
border
|
||||
stripe
|
||||
style="margin-top: 20px;"
|
||||
>
|
||||
<el-table-column type="selection" width="55" />
|
||||
<el-table-column type="expand">
|
||||
<template #default="{ row }">
|
||||
<div class="expand-content">
|
||||
<el-descriptions :column="2" border>
|
||||
<el-descriptions-item label="标题" :span="2">
|
||||
{{ row.title }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="来源">
|
||||
<el-tag :type="getSourceTagType(row.source)">{{ row.source }}</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="发表日期">
|
||||
{{ row.publicationDate || 'N/A' }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="作者" :span="2">
|
||||
{{ row.authors || 'N/A' }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="摘要" :span="2">
|
||||
<div style="white-space: pre-wrap; max-height: 200px; overflow-y: auto;">
|
||||
{{ row.summary || 'N/A' }}
|
||||
</div>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="DOI" v-if="row.doi">
|
||||
<a :href="`https://doi.org/${row.doi}`" target="_blank" style="color: #409EFF;">
|
||||
{{ row.doi }}
|
||||
</a>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="PMID" v-if="row.pmid">
|
||||
<a :href="`https://pubmed.ncbi.nlm.nih.gov/${row.pmid}`" target="_blank" style="color: #409EFF;">
|
||||
{{ row.pmid }}
|
||||
</a>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="NCT ID" v-if="row.nctId">
|
||||
<a :href="`https://clinicaltrials.gov/study/${row.nctId}`" target="_blank" style="color: #409EFF;">
|
||||
{{ row.nctId }}
|
||||
</a>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="来源链接" v-if="row.sourceUrl" :span="2">
|
||||
<a :href="row.sourceUrl" target="_blank" style="color: #409EFF;">
|
||||
{{ row.sourceUrl }}
|
||||
</a>
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="title" label="标题" min-width="300" show-overflow-tooltip />
|
||||
<el-table-column prop="source" label="来源" width="150">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="getSourceTagType(row.source)" size="small">
|
||||
{{ row.source }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" width="200">
|
||||
<template #default="{ row }">
|
||||
<div style="display: flex; flex-direction: column; gap: 5px;">
|
||||
<el-tag v-if="row.includeInResponse" type="success" size="small">
|
||||
已纳入回复
|
||||
</el-tag>
|
||||
<el-tag v-if="row.needDownload" type="warning" size="small">
|
||||
需要下载
|
||||
</el-tag>
|
||||
<el-tag v-if="row.isDeleted" type="danger" size="small">
|
||||
已删除
|
||||
</el-tag>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="280" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<div style="display: flex; flex-direction: column; gap: 5px;">
|
||||
<el-button-group>
|
||||
<el-button
|
||||
size="small"
|
||||
:type="row.includeInResponse ? 'success' : 'default'"
|
||||
@click="toggleInclude(row)"
|
||||
:disabled="row.isDeleted"
|
||||
>
|
||||
{{ row.includeInResponse ? '已纳入' : '纳入回复' }}
|
||||
</el-button>
|
||||
<el-button
|
||||
size="small"
|
||||
:type="row.needDownload ? 'warning' : 'default'"
|
||||
@click="toggleDownload(row)"
|
||||
:disabled="row.isDeleted"
|
||||
>
|
||||
{{ row.needDownload ? '已标记' : '下载全文' }}
|
||||
</el-button>
|
||||
</el-button-group>
|
||||
<el-button
|
||||
size="small"
|
||||
type="danger"
|
||||
@click="handleDelete(row)"
|
||||
:disabled="row.isDeleted"
|
||||
>
|
||||
标记错误并删除
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<el-empty
|
||||
v-if="results.length === 0"
|
||||
description="暂无检索结果"
|
||||
style="margin-top: 40px;"
|
||||
>
|
||||
<el-button type="primary" @click="goBack">返回重新检索</el-button>
|
||||
</el-empty>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { useRouter, useRoute } from 'vue-router'
|
||||
import { getInquiryDetail, generateResponse } from '@/api/inquiry'
|
||||
import {
|
||||
getSearchResults,
|
||||
updateSearchResult,
|
||||
batchUpdateSearchResults,
|
||||
deleteSearchResult
|
||||
} from '@/api/searchResult'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
|
||||
const loading = ref(false)
|
||||
const generating = ref(false)
|
||||
const inquiry = ref({})
|
||||
const results = ref([])
|
||||
const selectedResults = ref([])
|
||||
const filterType = ref('active')
|
||||
const searchText = ref('')
|
||||
const tableRef = ref(null)
|
||||
|
||||
const includedCount = computed(() => {
|
||||
return results.value.filter(r => r.includeInResponse && !r.isDeleted).length
|
||||
})
|
||||
|
||||
const downloadCount = computed(() => {
|
||||
return results.value.filter(r => r.needDownload && !r.isDeleted).length
|
||||
})
|
||||
|
||||
const deletedCount = computed(() => {
|
||||
return results.value.filter(r => r.isDeleted).length
|
||||
})
|
||||
|
||||
const activeCount = computed(() => {
|
||||
return results.value.filter(r => !r.isDeleted).length
|
||||
})
|
||||
|
||||
const filteredResults = computed(() => {
|
||||
let filtered = results.value
|
||||
|
||||
// 按类型筛选
|
||||
switch (filterType.value) {
|
||||
case 'active':
|
||||
filtered = filtered.filter(r => !r.isDeleted)
|
||||
break
|
||||
case 'included':
|
||||
filtered = filtered.filter(r => r.includeInResponse && !r.isDeleted)
|
||||
break
|
||||
case 'download':
|
||||
filtered = filtered.filter(r => r.needDownload && !r.isDeleted)
|
||||
break
|
||||
case 'deleted':
|
||||
filtered = filtered.filter(r => r.isDeleted)
|
||||
break
|
||||
}
|
||||
|
||||
// 搜索过滤
|
||||
if (searchText.value) {
|
||||
const text = searchText.value.toLowerCase()
|
||||
filtered = filtered.filter(r =>
|
||||
r.title?.toLowerCase().includes(text) ||
|
||||
r.summary?.toLowerCase().includes(text) ||
|
||||
r.content?.toLowerCase().includes(text)
|
||||
)
|
||||
}
|
||||
|
||||
return filtered
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
loadDetail()
|
||||
loadResults()
|
||||
})
|
||||
|
||||
const loadDetail = async () => {
|
||||
try {
|
||||
const id = route.params.id
|
||||
const data = await getInquiryDetail(id)
|
||||
inquiry.value = data
|
||||
} catch (error) {
|
||||
ElMessage.error('加载详情失败')
|
||||
}
|
||||
}
|
||||
|
||||
const loadResults = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const id = route.params.id
|
||||
const data = await getSearchResults(id)
|
||||
results.value = data || []
|
||||
} catch (error) {
|
||||
ElMessage.error('加载检索结果失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleRefresh = () => {
|
||||
loadResults()
|
||||
}
|
||||
|
||||
const handleSelectionChange = (selection) => {
|
||||
selectedResults.value = selection
|
||||
}
|
||||
|
||||
const toggleInclude = async (row) => {
|
||||
try {
|
||||
const newValue = !row.includeInResponse
|
||||
await updateSearchResult(row.id, { includeInResponse: newValue })
|
||||
row.includeInResponse = newValue
|
||||
ElMessage.success(newValue ? '已纳入回复' : '已取消纳入')
|
||||
} catch (error) {
|
||||
ElMessage.error('操作失败')
|
||||
}
|
||||
}
|
||||
|
||||
const toggleDownload = async (row) => {
|
||||
try {
|
||||
const newValue = !row.needDownload
|
||||
await updateSearchResult(row.id, { needDownload: newValue })
|
||||
row.needDownload = newValue
|
||||
ElMessage.success(newValue ? '已标记下载' : '已取消下载')
|
||||
} catch (error) {
|
||||
ElMessage.error('操作失败')
|
||||
}
|
||||
}
|
||||
|
||||
const handleDelete = async (row) => {
|
||||
ElMessageBox.confirm(
|
||||
'确认将此条结果标记为错误并删除?',
|
||||
'确认删除',
|
||||
{
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}
|
||||
).then(async () => {
|
||||
try {
|
||||
await deleteSearchResult(row.id)
|
||||
row.isDeleted = true
|
||||
ElMessage.success('已删除')
|
||||
} catch (error) {
|
||||
ElMessage.error('删除失败')
|
||||
}
|
||||
}).catch(() => {})
|
||||
}
|
||||
|
||||
const handleBatchInclude = async () => {
|
||||
try {
|
||||
const updates = selectedResults.value.map(r => ({
|
||||
id: r.id,
|
||||
includeInResponse: true
|
||||
}))
|
||||
await batchUpdateSearchResults(updates)
|
||||
selectedResults.value.forEach(r => {
|
||||
r.includeInResponse = true
|
||||
})
|
||||
ElMessage.success(`已将 ${updates.length} 条结果纳入回复`)
|
||||
tableRef.value?.clearSelection()
|
||||
} catch (error) {
|
||||
ElMessage.error('批量操作失败')
|
||||
}
|
||||
}
|
||||
|
||||
const handleBatchDownload = async () => {
|
||||
try {
|
||||
const updates = selectedResults.value.map(r => ({
|
||||
id: r.id,
|
||||
needDownload: true
|
||||
}))
|
||||
await batchUpdateSearchResults(updates)
|
||||
selectedResults.value.forEach(r => {
|
||||
r.needDownload = true
|
||||
})
|
||||
ElMessage.success(`已将 ${updates.length} 条结果标记下载`)
|
||||
tableRef.value?.clearSelection()
|
||||
} catch (error) {
|
||||
ElMessage.error('批量操作失败')
|
||||
}
|
||||
}
|
||||
|
||||
const handleBatchDelete = async () => {
|
||||
ElMessageBox.confirm(
|
||||
`确认删除选中的 ${selectedResults.value.length} 条结果?`,
|
||||
'批量删除',
|
||||
{
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}
|
||||
).then(async () => {
|
||||
try {
|
||||
for (const row of selectedResults.value) {
|
||||
await deleteSearchResult(row.id)
|
||||
row.isDeleted = true
|
||||
}
|
||||
ElMessage.success('批量删除成功')
|
||||
tableRef.value?.clearSelection()
|
||||
} catch (error) {
|
||||
ElMessage.error('批量删除失败')
|
||||
}
|
||||
}).catch(() => {})
|
||||
}
|
||||
|
||||
const handleGenerateResponse = async () => {
|
||||
ElMessageBox.confirm(
|
||||
`将基于 ${includedCount.value} 条已纳入的资料生成回复,确认继续?`,
|
||||
'生成回复',
|
||||
{
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'info'
|
||||
}
|
||||
).then(async () => {
|
||||
generating.value = true
|
||||
try {
|
||||
await generateResponse(inquiry.value.id)
|
||||
ElMessage.success('回复生成成功,正在跳转...')
|
||||
router.push(`/inquiry/${inquiry.value.id}/response-view`)
|
||||
} catch (error) {
|
||||
ElMessage.error('生成回复失败: ' + (error.message || '未知错误'))
|
||||
} finally {
|
||||
generating.value = false
|
||||
}
|
||||
}).catch(() => {})
|
||||
}
|
||||
|
||||
const handleFilterChange = () => {
|
||||
tableRef.value?.clearSelection()
|
||||
}
|
||||
|
||||
const getSourceTagType = (source) => {
|
||||
const typeMap = {
|
||||
'内部数据': 'primary',
|
||||
'知识库': 'success',
|
||||
'知网': 'warning',
|
||||
'ClinicalTrials.gov': 'danger',
|
||||
'CNKI': 'warning'
|
||||
}
|
||||
return typeMap[source] || 'info'
|
||||
}
|
||||
|
||||
const goBack = () => {
|
||||
router.push(`/inquiry/${inquiry.value.id}`)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.search-results {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.batch-actions {
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
.batch-actions .el-button {
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.filters {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
.expand-content {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
:deep(.el-statistic__content) {
|
||||
font-size: 28px;
|
||||
font-weight: 600;
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
1073
presentation.html
1073
presentation.html
File diff suppressed because it is too large
Load Diff
|
|
@ -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
|
||||
<section>
|
||||
<h2>系统界面展示</h2>
|
||||
<img src="screenshots/dashboard.png" class="screenshot" alt="工作台界面">
|
||||
<p>系统工作台提供一站式的信息概览</p>
|
||||
</section>
|
||||
```
|
||||
|
||||
### 方法二:创建对比展示
|
||||
```html
|
||||
<section>
|
||||
<h2>操作流程</h2>
|
||||
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 20px;">
|
||||
<div>
|
||||
<img src="screenshots/inquiry-create.png" class="screenshot">
|
||||
<p>步骤1:创建查询</p>
|
||||
</div>
|
||||
<div>
|
||||
<img src="screenshots/inquiry-detail.png" class="screenshot">
|
||||
<p>步骤2:处理查询</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
```
|
||||
|
||||
### 方法三:全屏展示
|
||||
```html
|
||||
<section data-background-image="screenshots/dashboard.png" data-background-size="contain">
|
||||
<div style="background: rgba(0,0,0,0.7); padding: 20px; color: white;">
|
||||
<h2>工作台界面</h2>
|
||||
<p>清晰的数据展示,一目了然</p>
|
||||
</div>
|
||||
</section>
|
||||
```
|
||||
|
||||
## 快速截图工具推荐
|
||||
|
||||
### 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. 选择最佳的添加到演示文稿
|
||||
|
||||
## 注意事项
|
||||
|
||||
⚠️ **隐私保护**
|
||||
- 不要截取包含真实患者信息的页面
|
||||
- 不要截取包含真实医生信息的页面
|
||||
- 使用测试账号进行截图
|
||||
- 模糊处理任何敏感信息
|
||||
|
||||
⚠️ **版权说明**
|
||||
- 系统界面截图仅供内部演示使用
|
||||
- 对外发布前需获得授权
|
||||
|
||||
---
|
||||
|
||||
**截图完成后,删除本说明文件即可。**
|
||||
|
||||
|
||||
|
||||
Loading…
Reference in New Issue