Compare commits
2 Commits
5e970a1388
...
442e3a2e57
| Author | SHA1 | Date |
|---|---|---|
|
|
442e3a2e57 | |
|
|
f4af080b04 |
|
|
@ -1,34 +1,62 @@
|
||||||
# Git
|
# Include any files or directories that you don't want to be copied to your
|
||||||
.git
|
# container here (e.g., local build artifacts, temporary files, etc.).
|
||||||
.gitignore
|
#
|
||||||
|
# For more help, visit the .dockerignore file reference guide at
|
||||||
|
# https://docs.docker.com/go/build-context-dockerignore/
|
||||||
|
|
||||||
# Documentation
|
**/.DS_Store
|
||||||
*.md
|
**/.classpath
|
||||||
|
**/.dockerignore
|
||||||
|
**/.env
|
||||||
|
**/.factorypath
|
||||||
|
**/.git
|
||||||
|
**/.gitignore
|
||||||
|
**/.idea
|
||||||
|
**/.project
|
||||||
|
**/.sts4-cache
|
||||||
|
**/.settings
|
||||||
|
**/.toolstarget
|
||||||
|
**/.vs
|
||||||
|
**/.vscode
|
||||||
|
**/.next
|
||||||
|
**/.cache
|
||||||
|
**/*.dbmdl
|
||||||
|
**/*.jfm
|
||||||
|
**/charts
|
||||||
|
**/docker-compose*
|
||||||
|
**/compose.y*ml
|
||||||
|
**/Dockerfile*
|
||||||
|
**/secrets.dev.yaml
|
||||||
|
**/values.dev.yaml
|
||||||
|
**/vendor
|
||||||
LICENSE
|
LICENSE
|
||||||
|
README.md
|
||||||
# Docker
|
**/*.class
|
||||||
docker-compose.yml
|
**/*.iml
|
||||||
Dockerfile
|
**/*.ipr
|
||||||
.dockerignore
|
**/*.iws
|
||||||
|
**/*.log
|
||||||
# Logs
|
**/.apt_generated
|
||||||
logs
|
**/.gradle
|
||||||
*.log
|
**/.gradletasknamecache
|
||||||
|
**/.nb-gradle
|
||||||
# IDE
|
**/.springBeans
|
||||||
.idea
|
**/build
|
||||||
.vscode
|
**/dist
|
||||||
*.iml
|
**/gradle-app.setting
|
||||||
|
**/nbbuild
|
||||||
# Temporary files
|
**/nbdist
|
||||||
*.tmp
|
**/nbproject/private
|
||||||
*.bak
|
**/target
|
||||||
*.swp
|
*.ctxt
|
||||||
|
.mtj.tmp
|
||||||
# OS files
|
.mvn/timing.properties
|
||||||
.DS_Store
|
buildNumber.properties
|
||||||
Thumbs.db
|
dependency-reduced-pom.xml
|
||||||
|
hs_err_pid*
|
||||||
|
pom.xml.next
|
||||||
|
pom.xml.releaseBackup
|
||||||
|
pom.xml.tag
|
||||||
|
pom.xml.versionsBackup
|
||||||
|
release.properties
|
||||||
|
replay_pid*
|
||||||
5
.env
5
.env
|
|
@ -10,8 +10,9 @@ SPRING_PROFILES_ACTIVE=prod
|
||||||
JWT_SECRET=your-secret-key-change-in-production-environment
|
JWT_SECRET=your-secret-key-change-in-production-environment
|
||||||
|
|
||||||
# Dify配置
|
# Dify配置
|
||||||
|
# 请将下面的API Key替换为您实际的Dify API Key
|
||||||
DIFY_API_URL=https://api.dify.ai/v1
|
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
|
LLM_API_URL=https://api.openai.com/v1
|
||||||
|
|
@ -29,3 +30,5 @@ CNKI_PASSWORD=
|
||||||
# 文献下载路径
|
# 文献下载路径
|
||||||
LITERATURE_DOWNLOAD_PATH=./downloads
|
LITERATURE_DOWNLOAD_PATH=./downloads
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
{
|
{
|
||||||
"java.configuration.updateBuildConfiguration": "interactive"
|
"java.configuration.updateBuildConfiguration": "interactive",
|
||||||
|
"java.compile.nullAnalysis.mode": "automatic"
|
||||||
}
|
}
|
||||||
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部署
|
|
||||||
|
|
||||||
168
Java版本问题解决指南.txt
168
Java版本问题解决指南.txt
|
|
@ -1,168 +0,0 @@
|
||||||
====================================
|
|
||||||
Java版本不兼容问题 - 解决指南
|
|
||||||
====================================
|
|
||||||
|
|
||||||
问题症状:
|
|
||||||
-----------
|
|
||||||
启动后端时出现错误:
|
|
||||||
"class file version 61.0, this version of the Java Runtime only recognizes class file versions up to 52.0"
|
|
||||||
|
|
||||||
原因分析:
|
|
||||||
-----------
|
|
||||||
- Spring Boot 3.2.0 需要 Java 17+
|
|
||||||
- 您当前使用的是 Java 8
|
|
||||||
- 版本不匹配导致无法运行
|
|
||||||
|
|
||||||
====================================
|
|
||||||
解决方案一:升级Java到17(推荐)
|
|
||||||
====================================
|
|
||||||
|
|
||||||
为什么推荐?
|
|
||||||
-----------
|
|
||||||
✓ 使用最新技术栈
|
|
||||||
✓ 更好的性能
|
|
||||||
✓ 更多新特性
|
|
||||||
✓ 长期支持版本
|
|
||||||
|
|
||||||
步骤:
|
|
||||||
------
|
|
||||||
|
|
||||||
1. 下载 Java 17
|
|
||||||
推荐下载地址:
|
|
||||||
- Oracle JDK 17: https://www.oracle.com/java/technologies/downloads/#java17
|
|
||||||
- Eclipse Temurin 17: https://adoptium.net/temurin/releases/?version=17
|
|
||||||
|
|
||||||
选择 Windows x64 Installer (.msi)
|
|
||||||
|
|
||||||
2. 安装 Java 17
|
|
||||||
- 双击下载的安装程序
|
|
||||||
- 按默认选项安装(推荐路径:C:\Program Files\Java\jdk-17)
|
|
||||||
- 记住安装路径
|
|
||||||
|
|
||||||
3. 配置环境变量
|
|
||||||
|
|
||||||
方法A - 使用图形界面:
|
|
||||||
a) 右键"此电脑" -> "属性" -> "高级系统设置"
|
|
||||||
b) 点击"环境变量"
|
|
||||||
c) 在"系统变量"中:
|
|
||||||
- 如果有JAVA_HOME,修改它:
|
|
||||||
变量值改为:C:\Program Files\Java\jdk-17
|
|
||||||
- 如果没有,新建JAVA_HOME:
|
|
||||||
变量名:JAVA_HOME
|
|
||||||
变量值:C:\Program Files\Java\jdk-17
|
|
||||||
d) 编辑Path变量:
|
|
||||||
- 找到旧的Java路径(如果有),删除或移到底部
|
|
||||||
- 在最前面添加:%JAVA_HOME%\bin
|
|
||||||
e) 点击"确定"保存
|
|
||||||
|
|
||||||
方法B - 使用命令行(管理员权限):
|
|
||||||
setx JAVA_HOME "C:\Program Files\Java\jdk-17" /M
|
|
||||||
|
|
||||||
4. 验证安装
|
|
||||||
|
|
||||||
打开新的命令行窗口(必须是新窗口!),执行:
|
|
||||||
|
|
||||||
java -version
|
|
||||||
|
|
||||||
应该显示:
|
|
||||||
openjdk version "17.x.x"
|
|
||||||
或
|
|
||||||
java version "17.x.x"
|
|
||||||
|
|
||||||
5. 重新启动项目
|
|
||||||
|
|
||||||
cd d:\SoftwarePrj\文献流程\backend
|
|
||||||
mvn clean spring-boot:run
|
|
||||||
|
|
||||||
====================================
|
|
||||||
解决方案二:降级到Spring Boot 2.7
|
|
||||||
====================================
|
|
||||||
|
|
||||||
为什么选择?
|
|
||||||
-----------
|
|
||||||
✓ 可以继续使用 Java 8
|
|
||||||
✓ 无需升级Java
|
|
||||||
✗ 无法使用Spring Boot 3的新特性
|
|
||||||
|
|
||||||
步骤:
|
|
||||||
------
|
|
||||||
|
|
||||||
1. 备份原配置文件
|
|
||||||
copy backend\pom.xml backend\pom-backup.xml
|
|
||||||
|
|
||||||
2. 使用Java 8兼容的配置
|
|
||||||
copy backend\pom-java8.xml backend\pom.xml
|
|
||||||
|
|
||||||
3. 清理并重新编译
|
|
||||||
cd backend
|
|
||||||
mvn clean install
|
|
||||||
|
|
||||||
4. 启动服务
|
|
||||||
mvn spring-boot:run
|
|
||||||
|
|
||||||
====================================
|
|
||||||
验证Java版本
|
|
||||||
====================================
|
|
||||||
|
|
||||||
检查当前Java版本:
|
|
||||||
------------------
|
|
||||||
java -version
|
|
||||||
javac -version
|
|
||||||
|
|
||||||
检查Maven使用的Java版本:
|
|
||||||
-------------------------
|
|
||||||
mvn -version
|
|
||||||
|
|
||||||
如果Maven使用的不是Java 17:
|
|
||||||
----------------------------
|
|
||||||
编辑 Maven 配置文件:
|
|
||||||
%MAVEN_HOME%\bin\mvn.cmd
|
|
||||||
|
|
||||||
或在项目根目录创建 .mvn/jvm.config 文件:
|
|
||||||
-Djava.home=C:\Program Files\Java\jdk-17
|
|
||||||
|
|
||||||
====================================
|
|
||||||
常见问题
|
|
||||||
====================================
|
|
||||||
|
|
||||||
Q1: 安装了Java 17但java -version仍显示Java 8
|
|
||||||
A1: 环境变量配置不正确或命令行窗口未重新打开
|
|
||||||
- 关闭所有命令行窗口
|
|
||||||
- 重新打开新窗口
|
|
||||||
- 再次检查 java -version
|
|
||||||
|
|
||||||
Q2: 有多个Java版本,如何切换?
|
|
||||||
A2: 修改JAVA_HOME环境变量指向目标版本
|
|
||||||
或使用工具如 jEnv (Linux/Mac) 或手动切换
|
|
||||||
|
|
||||||
Q3: Maven仍然使用旧版本Java
|
|
||||||
A3: 检查 MAVEN_OPTS 环境变量
|
|
||||||
或在项目中创建 .mvn/jvm.config
|
|
||||||
|
|
||||||
Q4: 降级到Spring Boot 2.7后出现新错误
|
|
||||||
A4: 某些代码可能使用了Spring Boot 3的API
|
|
||||||
需要根据具体错误调整代码
|
|
||||||
主要差异:
|
|
||||||
- javax.* -> jakarta.* (在Spring Boot 3中)
|
|
||||||
- 某些配置属性名称变化
|
|
||||||
|
|
||||||
====================================
|
|
||||||
推荐方案
|
|
||||||
====================================
|
|
||||||
|
|
||||||
对于新项目,强烈建议:
|
|
||||||
✓ 升级到 Java 17
|
|
||||||
✓ 使用 Spring Boot 3.x
|
|
||||||
✓ 享受最新特性和性能改进
|
|
||||||
|
|
||||||
Java 17 是长期支持版本(LTS),值得升级!
|
|
||||||
|
|
||||||
====================================
|
|
||||||
需要帮助?
|
|
||||||
====================================
|
|
||||||
|
|
||||||
1. 如选择方案一,下载安装Java 17后重新运行 start-dev.bat
|
|
||||||
2. 如选择方案二,按照步骤执行后重新运行
|
|
||||||
3. 查看完整文档:DEPLOYMENT.md
|
|
||||||
|
|
||||||
|
|
||||||
102
Java环境问题解决.txt
102
Java环境问题解决.txt
|
|
@ -1,102 +0,0 @@
|
||||||
====================================
|
|
||||||
Java环境问题诊断和解决
|
|
||||||
====================================
|
|
||||||
|
|
||||||
问题分析:
|
|
||||||
-----------
|
|
||||||
您的系统中有Java版本不一致的问题:
|
|
||||||
|
|
||||||
✓ java -version: 1.8.0_471 (JRE)
|
|
||||||
✓ javac -version: 1.8.0_161 (JDK)
|
|
||||||
✓ Maven使用: 1.8.0_471 (JRE) ← 问题所在
|
|
||||||
|
|
||||||
Maven需要JDK才能编译,但当前使用的是JRE。
|
|
||||||
|
|
||||||
====================================
|
|
||||||
解决方案
|
|
||||||
====================================
|
|
||||||
|
|
||||||
方案一:配置Maven使用正确的JDK(推荐)
|
|
||||||
------------------------------------
|
|
||||||
|
|
||||||
1. 找到JDK安装路径
|
|
||||||
根据javac版本,JDK可能在:
|
|
||||||
C:\Program Files\Java\jdk1.8.0_161
|
|
||||||
|
|
||||||
2. 设置JAVA_HOME环境变量
|
|
||||||
|
|
||||||
方法A - 图形界面:
|
|
||||||
a) 右键"此电脑" -> "属性" -> "高级系统设置"
|
|
||||||
b) 点击"环境变量"
|
|
||||||
c) 在"系统变量"中:
|
|
||||||
- 修改JAVA_HOME为:C:\Program Files\Java\jdk1.8.0_161
|
|
||||||
- 编辑Path变量,确保%JAVA_HOME%\bin在最前面
|
|
||||||
|
|
||||||
方法B - 命令行(管理员权限):
|
|
||||||
setx JAVA_HOME "C:\Program Files\Java\jdk1.8.0_161" /M
|
|
||||||
|
|
||||||
3. 验证配置
|
|
||||||
打开新的命令行窗口:
|
|
||||||
java -version
|
|
||||||
javac -version
|
|
||||||
mvn -version
|
|
||||||
|
|
||||||
三个命令应该显示相同的Java版本
|
|
||||||
|
|
||||||
方案二:使用Maven的JVM配置
|
|
||||||
-------------------------
|
|
||||||
|
|
||||||
在项目根目录创建 .mvn/jvm.config 文件:
|
|
||||||
|
|
||||||
1. 创建目录:
|
|
||||||
mkdir .mvn
|
|
||||||
|
|
||||||
2. 创建文件 .mvn/jvm.config,内容:
|
|
||||||
-Djava.home=C:\Program Files\Java\jdk1.8.0_161
|
|
||||||
|
|
||||||
方案三:临时设置(当前会话有效)
|
|
||||||
--------------------------------
|
|
||||||
|
|
||||||
在命令行中执行:
|
|
||||||
set JAVA_HOME=C:\Program Files\Java\jdk1.8.0_161
|
|
||||||
set PATH=%JAVA_HOME%\bin;%PATH%
|
|
||||||
|
|
||||||
然后重新运行Maven命令。
|
|
||||||
|
|
||||||
====================================
|
|
||||||
推荐操作步骤
|
|
||||||
====================================
|
|
||||||
|
|
||||||
1. 首先尝试方案一(永久解决)
|
|
||||||
2. 如果方案一不行,使用方案三(临时解决)
|
|
||||||
3. 重新运行编译命令
|
|
||||||
|
|
||||||
====================================
|
|
||||||
验证步骤
|
|
||||||
====================================
|
|
||||||
|
|
||||||
配置完成后,执行以下命令验证:
|
|
||||||
|
|
||||||
1. java -version
|
|
||||||
应该显示:java version "1.8.0_161"
|
|
||||||
|
|
||||||
2. javac -version
|
|
||||||
应该显示:javac 1.8.0_161
|
|
||||||
|
|
||||||
3. mvn -version
|
|
||||||
应该显示:Java version: 1.8.0_161
|
|
||||||
|
|
||||||
4. 重新编译:
|
|
||||||
mvn clean compile
|
|
||||||
|
|
||||||
====================================
|
|
||||||
如果仍有问题
|
|
||||||
====================================
|
|
||||||
|
|
||||||
1. 检查JDK是否完整安装
|
|
||||||
2. 确认JAVA_HOME路径正确
|
|
||||||
3. 重启命令行窗口
|
|
||||||
4. 考虑重新安装JDK
|
|
||||||
|
|
||||||
====================================
|
|
||||||
|
|
||||||
|
|
@ -1,229 +0,0 @@
|
||||||
# 医学信息支持系统 - 项目结构说明
|
|
||||||
|
|
||||||
## 项目概述
|
|
||||||
|
|
||||||
本项目是为益普生(Ipsen)开发的医学信息支持系统,旨在帮助医学信息团队快速处理客户(主要是医生)的信息支持需求。
|
|
||||||
|
|
||||||
## 技术栈
|
|
||||||
|
|
||||||
### 后端
|
|
||||||
- **Java 17**
|
|
||||||
- **Spring Boot 3.2.0**
|
|
||||||
- **Spring Data JPA**
|
|
||||||
- **Spring Security**
|
|
||||||
- **MySQL 8.0**
|
|
||||||
- **Maven**
|
|
||||||
|
|
||||||
### 前端
|
|
||||||
- **Vue 3**
|
|
||||||
- **Vite**
|
|
||||||
- **Element Plus**
|
|
||||||
- **Pinia** (状态管理)
|
|
||||||
- **Vue Router**
|
|
||||||
- **Axios**
|
|
||||||
|
|
||||||
### 部署
|
|
||||||
- **Docker**
|
|
||||||
- **Docker Compose**
|
|
||||||
- **Nginx**
|
|
||||||
|
|
||||||
## 项目结构
|
|
||||||
|
|
||||||
```
|
|
||||||
文献流程/
|
|
||||||
├── backend/ # 后端Spring Boot项目
|
|
||||||
│ ├── src/
|
|
||||||
│ │ ├── main/
|
|
||||||
│ │ │ ├── java/com/ipsen/medical/
|
|
||||||
│ │ │ │ ├── MedicalInfoApplication.java # 主应用类
|
|
||||||
│ │ │ │ ├── controller/ # 控制器层
|
|
||||||
│ │ │ │ │ ├── InquiryController.java
|
|
||||||
│ │ │ │ │ ├── KnowledgeBaseController.java
|
|
||||||
│ │ │ │ │ └── LiteratureController.java
|
|
||||||
│ │ │ │ ├── service/ # 服务层接口
|
|
||||||
│ │ │ │ │ ├── InquiryService.java
|
|
||||||
│ │ │ │ │ ├── KnowledgeBaseService.java
|
|
||||||
│ │ │ │ │ ├── LiteratureService.java
|
|
||||||
│ │ │ │ │ ├── DifyService.java
|
|
||||||
│ │ │ │ │ └── ExcelParserService.java
|
|
||||||
│ │ │ │ ├── service/impl/ # 服务层实现
|
|
||||||
│ │ │ │ │ ├── InquiryServiceImpl.java
|
|
||||||
│ │ │ │ │ ├── KnowledgeBaseServiceImpl.java
|
|
||||||
│ │ │ │ │ ├── LiteratureServiceImpl.java
|
|
||||||
│ │ │ │ │ ├── DifyServiceImpl.java
|
|
||||||
│ │ │ │ │ └── ExcelParserServiceImpl.java
|
|
||||||
│ │ │ │ ├── entity/ # 实体类
|
|
||||||
│ │ │ │ │ ├── InquiryRequest.java
|
|
||||||
│ │ │ │ │ ├── KnowledgeBase.java
|
|
||||||
│ │ │ │ │ ├── Literature.java
|
|
||||||
│ │ │ │ │ ├── User.java
|
|
||||||
│ │ │ │ │ └── AuditLog.java
|
|
||||||
│ │ │ │ ├── repository/ # 数据访问层
|
|
||||||
│ │ │ │ │ ├── InquiryRequestRepository.java
|
|
||||||
│ │ │ │ │ ├── KnowledgeBaseRepository.java
|
|
||||||
│ │ │ │ │ ├── LiteratureRepository.java
|
|
||||||
│ │ │ │ │ ├── UserRepository.java
|
|
||||||
│ │ │ │ │ └── AuditLogRepository.java
|
|
||||||
│ │ │ │ └── dto/ # 数据传输对象
|
|
||||||
│ │ │ │ ├── ApiResponse.java
|
|
||||||
│ │ │ │ ├── InquiryRequestDTO.java
|
|
||||||
│ │ │ │ ├── KnowledgeBaseDTO.java
|
|
||||||
│ │ │ │ └── LiteratureDTO.java
|
|
||||||
│ │ │ └── resources/
|
|
||||||
│ │ │ └── application.yml # 应用配置
|
|
||||||
│ │ └── test/ # 测试代码
|
|
||||||
│ ├── pom.xml # Maven配置
|
|
||||||
│ ├── Dockerfile # Docker构建文件
|
|
||||||
│ └── .gitignore
|
|
||||||
│
|
|
||||||
├── frontend/ # 前端Vue3项目
|
|
||||||
│ ├── src/
|
|
||||||
│ │ ├── api/ # API接口
|
|
||||||
│ │ │ ├── inquiry.js
|
|
||||||
│ │ │ ├── knowledge.js
|
|
||||||
│ │ │ └── literature.js
|
|
||||||
│ │ ├── assets/ # 静态资源
|
|
||||||
│ │ ├── components/ # 通用组件
|
|
||||||
│ │ ├── layout/ # 布局组件
|
|
||||||
│ │ │ └── index.vue
|
|
||||||
│ │ ├── router/ # 路由配置
|
|
||||||
│ │ │ └── index.js
|
|
||||||
│ │ ├── store/ # 状态管理
|
|
||||||
│ │ ├── utils/ # 工具函数
|
|
||||||
│ │ │ └── request.js
|
|
||||||
│ │ ├── views/ # 页面组件
|
|
||||||
│ │ │ ├── Dashboard.vue # 工作台
|
|
||||||
│ │ │ ├── Login.vue # 登录页
|
|
||||||
│ │ │ ├── inquiry/ # 查询管理
|
|
||||||
│ │ │ │ ├── InquiryList.vue
|
|
||||||
│ │ │ │ ├── InquiryCreate.vue
|
|
||||||
│ │ │ │ └── InquiryDetail.vue
|
|
||||||
│ │ │ ├── knowledge/ # 知识库管理
|
|
||||||
│ │ │ │ └── KnowledgeList.vue
|
|
||||||
│ │ │ └── system/ # 系统设置
|
|
||||||
│ │ │ ├── SystemConfig.vue
|
|
||||||
│ │ │ └── UserManagement.vue
|
|
||||||
│ │ ├── App.vue # 根组件
|
|
||||||
│ │ └── main.js # 入口文件
|
|
||||||
│ ├── index.html
|
|
||||||
│ ├── package.json # npm配置
|
|
||||||
│ ├── vite.config.js # Vite配置
|
|
||||||
│ ├── nginx.conf # Nginx配置
|
|
||||||
│ ├── Dockerfile # Docker构建文件
|
|
||||||
│ └── .gitignore
|
|
||||||
│
|
|
||||||
├── database/ # 数据库脚本
|
|
||||||
│ ├── schema.sql # 数据库结构
|
|
||||||
│ └── sample_data.sql # 示例数据
|
|
||||||
│
|
|
||||||
├── docker-compose.yml # Docker Compose配置
|
|
||||||
├── .env.example # 环境变量示例
|
|
||||||
├── .dockerignore # Docker忽略文件
|
|
||||||
├── Readme.md # 项目说明(需求文档)
|
|
||||||
└── PROJECT_STRUCTURE.md # 项目结构说明(本文件)
|
|
||||||
```
|
|
||||||
|
|
||||||
## 核心功能模块
|
|
||||||
|
|
||||||
### 1. 查询请求管理 (Inquiry Management)
|
|
||||||
- 上传标准化表格或手动创建查询请求
|
|
||||||
- 自动提取关键词(药物名称、疾病、问题)
|
|
||||||
- 执行多层次信息检索
|
|
||||||
- 生成回复内容
|
|
||||||
- 审核流程管理
|
|
||||||
- 文献下载
|
|
||||||
|
|
||||||
### 2. 知识库管理 (Knowledge Base Management)
|
|
||||||
- 支持三种类型知识库:
|
|
||||||
- **自有数据 (INTERNAL)**: 企业研究、历史回复、内部文献
|
|
||||||
- **公开数据 (PUBLIC)**: PubMed、EMBASE、知网、ClinicalTrials.gov
|
|
||||||
- **扩展数据 (EXTENDED)**: 疾病药物关联数据
|
|
||||||
- 优先级配置
|
|
||||||
- 动态启用/禁用
|
|
||||||
|
|
||||||
### 3. 文献管理 (Literature Management)
|
|
||||||
- 文献检索结果展示
|
|
||||||
- 文献选择
|
|
||||||
- 批量下载
|
|
||||||
- 多数据库账号配置
|
|
||||||
|
|
||||||
### 4. 系统配置 (System Configuration)
|
|
||||||
- Dify AI引擎配置
|
|
||||||
- 大模型API配置
|
|
||||||
- 文献下载账号配置
|
|
||||||
- 用户管理
|
|
||||||
|
|
||||||
## 数据库设计
|
|
||||||
|
|
||||||
### 主要表结构
|
|
||||||
|
|
||||||
1. **users** - 用户表
|
|
||||||
- 管理员 (ADMIN)
|
|
||||||
- 医学信息专员 (MEDICAL_SPECIALIST)
|
|
||||||
- 审核人员 (REVIEWER)
|
|
||||||
|
|
||||||
2. **inquiry_requests** - 查询请求表
|
|
||||||
- 请求状态流转:PENDING → KEYWORD_EXTRACTED → SEARCHING → SEARCH_COMPLETED → UNDER_REVIEW → DOWNLOADING → COMPLETED
|
|
||||||
|
|
||||||
3. **knowledge_bases** - 知识库表
|
|
||||||
- 类型、优先级、配置管理
|
|
||||||
|
|
||||||
4. **literatures** - 文献表
|
|
||||||
- 文献信息、下载状态
|
|
||||||
|
|
||||||
5. **audit_logs** - 审核日志表
|
|
||||||
- 操作追踪、审核记录
|
|
||||||
|
|
||||||
## API接口
|
|
||||||
|
|
||||||
### 查询管理 API
|
|
||||||
- `POST /api/inquiries/upload` - 上传查询表格
|
|
||||||
- `POST /api/inquiries` - 创建查询请求
|
|
||||||
- `GET /api/inquiries` - 获取查询列表
|
|
||||||
- `GET /api/inquiries/{id}` - 获取查询详情
|
|
||||||
- `POST /api/inquiries/{id}/extract-keywords` - 提取关键词
|
|
||||||
- `POST /api/inquiries/{id}/search` - 执行检索
|
|
||||||
- `POST /api/inquiries/{id}/generate-response` - 生成回复
|
|
||||||
- `POST /api/inquiries/{id}/review` - 审核回复
|
|
||||||
- `POST /api/inquiries/{id}/complete` - 完成处理
|
|
||||||
|
|
||||||
### 知识库管理 API
|
|
||||||
- `GET /api/knowledge-bases` - 获取知识库列表
|
|
||||||
- `POST /api/knowledge-bases` - 创建知识库
|
|
||||||
- `PUT /api/knowledge-bases/{id}` - 更新知识库
|
|
||||||
- `DELETE /api/knowledge-bases/{id}` - 删除知识库
|
|
||||||
- `PATCH /api/knowledge-bases/{id}/toggle` - 启用/禁用
|
|
||||||
|
|
||||||
### 文献管理 API
|
|
||||||
- `GET /api/literatures/inquiry/{inquiryId}` - 获取文献列表
|
|
||||||
- `POST /api/literatures/{id}/select` - 选择文献
|
|
||||||
- `POST /api/literatures/{id}/download` - 下载文献
|
|
||||||
- `POST /api/literatures/inquiry/{inquiryId}/download-selected` - 批量下载
|
|
||||||
|
|
||||||
## 工作流程
|
|
||||||
|
|
||||||
1. **接收请求**: 医学信息专员上传标准化表格或手动填写查询内容
|
|
||||||
2. **关键词提取**: 系统调用Dify API提取关键词
|
|
||||||
3. **信息检索**: 按优先级在各知识库中检索相关信息
|
|
||||||
4. **生成回复**: 系统整理检索结果,生成回复草稿
|
|
||||||
5. **人工审核**: 审核人员审核回复内容
|
|
||||||
6. **下载文献**: 系统下载选中的文献
|
|
||||||
7. **完成处理**: 将回复和文献发送给客户
|
|
||||||
|
|
||||||
## 扩展点
|
|
||||||
|
|
||||||
系统设计了多个扩展点,便于后续功能增强:
|
|
||||||
|
|
||||||
1. **AI引擎**: 使用Dify作为AI引擎,支持切换不同的大模型
|
|
||||||
2. **知识库**: 支持动态添加新的知识库类型
|
|
||||||
3. **文献下载**: 支持添加新的文献数据库
|
|
||||||
4. **审核流程**: 可扩展多级审核流程
|
|
||||||
5. **权限管理**: 基于角色的权限控制
|
|
||||||
|
|
||||||
## 开发说明
|
|
||||||
|
|
||||||
详细的部署和开发说明请参考 `DEPLOYMENT.md` 文件。
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -1,222 +0,0 @@
|
||||||
# 医学信息支持系统
|
|
||||||
|
|
||||||
<div align="center">
|
|
||||||
|
|
||||||

|
|
||||||

|
|
||||||

|
|
||||||

|
|
||||||

|
|
||||||
|
|
||||||
一个为制药企业设计的智能医学信息支持系统,帮助医学信息团队高效处理客户的信息需求。
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
## 📋 项目简介
|
|
||||||
|
|
||||||
本系统为益普生(Ipsen)开发,用于处理来自医生等客户的医学信息查询请求。系统集成AI技术,能够自动提取关键词、检索相关文献、生成回复内容,并支持多级审核流程。
|
|
||||||
|
|
||||||
### 核心特性
|
|
||||||
|
|
||||||
- 🤖 **AI驱动**: 集成Dify AI引擎,自动提取关键词和生成回复
|
|
||||||
- 📚 **多层次检索**: 支持企业自有数据、公开数据库、扩展数据的分层检索
|
|
||||||
- 📄 **文献管理**: 自动检索和下载PubMed、EMBASE、知网等数据库文献
|
|
||||||
- ✅ **审核流程**: 完整的工作流程管理和审核机制
|
|
||||||
- 🔧 **灵活配置**: 知识库、数据源、优先级可动态配置
|
|
||||||
- 🐳 **容器化部署**: 支持Docker一键部署
|
|
||||||
|
|
||||||
## 🏗️ 技术架构
|
|
||||||
|
|
||||||
### 后端技术栈
|
|
||||||
- Java 17
|
|
||||||
- Spring Boot 3.2.0
|
|
||||||
- Spring Data JPA
|
|
||||||
- Spring Security
|
|
||||||
- MySQL 8.0
|
|
||||||
- Maven
|
|
||||||
|
|
||||||
### 前端技术栈
|
|
||||||
- Vue 3
|
|
||||||
- Vite
|
|
||||||
- Element Plus
|
|
||||||
- Pinia
|
|
||||||
- Vue Router
|
|
||||||
- Axios
|
|
||||||
|
|
||||||
### AI & 第三方服务
|
|
||||||
- Dify AI Platform
|
|
||||||
- OpenAI / 其他大模型
|
|
||||||
- PubMed、EMBASE、知网等文献数据库
|
|
||||||
|
|
||||||
## 🚀 快速开始
|
|
||||||
|
|
||||||
### 使用Docker部署(推荐)
|
|
||||||
|
|
||||||
1. **克隆项目**
|
|
||||||
```bash
|
|
||||||
git clone <repository-url>
|
|
||||||
cd 文献流程
|
|
||||||
```
|
|
||||||
|
|
||||||
2. **配置环境变量**
|
|
||||||
```bash
|
|
||||||
cp .env.example .env
|
|
||||||
# 编辑.env文件,配置数据库密码、API密钥等
|
|
||||||
```
|
|
||||||
|
|
||||||
3. **启动服务**
|
|
||||||
```bash
|
|
||||||
docker-compose up -d
|
|
||||||
```
|
|
||||||
|
|
||||||
4. **访问系统**
|
|
||||||
- 前端: http://localhost
|
|
||||||
- 后端API: http://localhost:8080/api
|
|
||||||
- 默认账号: `admin` / `admin123`
|
|
||||||
|
|
||||||
### 本地开发
|
|
||||||
|
|
||||||
详细的本地开发环境搭建请参考 [DEPLOYMENT.md](./DEPLOYMENT.md)
|
|
||||||
|
|
||||||
## 📖 项目文档
|
|
||||||
|
|
||||||
- [项目结构说明](./PROJECT_STRUCTURE.md) - 详细的项目结构和模块说明
|
|
||||||
- [部署指南](./DEPLOYMENT.md) - 完整的部署和配置指南
|
|
||||||
- [需求文档](./Readme.md) - 原始业务需求和功能说明
|
|
||||||
|
|
||||||
## 🔑 核心功能
|
|
||||||
|
|
||||||
### 1. 查询请求管理
|
|
||||||
- 支持上传标准化Excel表格或手动创建查询
|
|
||||||
- 自动提取查询关键词(药物、疾病、问题)
|
|
||||||
- 多数据源智能检索
|
|
||||||
- AI辅助生成回复内容
|
|
||||||
|
|
||||||
### 2. 知识库管理
|
|
||||||
- **自有数据**: 企业研究、历史回复、内部文献
|
|
||||||
- **公开数据**: PubMed、EMBASE、知网、ClinicalTrials.gov
|
|
||||||
- **扩展数据**: 疾病药物关联、相关扩展信息
|
|
||||||
- 支持优先级配置和动态启用/禁用
|
|
||||||
|
|
||||||
### 3. 文献管理
|
|
||||||
- 自动检索多个文献数据库
|
|
||||||
- 支持文献筛选和批量下载
|
|
||||||
- 配置多个数据库账号
|
|
||||||
|
|
||||||
### 4. 工作流程
|
|
||||||
```
|
|
||||||
接收请求 → 提取关键词 → 信息检索 → 生成回复 → 审核 → 下载文献 → 完成
|
|
||||||
```
|
|
||||||
|
|
||||||
### 5. 系统管理
|
|
||||||
- 用户管理(管理员、医学信息专员、审核人员)
|
|
||||||
- Dify API配置
|
|
||||||
- 大模型配置
|
|
||||||
- 文献数据库账号配置
|
|
||||||
|
|
||||||
## 📊 系统架构
|
|
||||||
|
|
||||||
```
|
|
||||||
┌─────────────┐
|
|
||||||
│ 客户端 │ (Vue3 + Element Plus)
|
|
||||||
└──────┬──────┘
|
|
||||||
│ HTTP/HTTPS
|
|
||||||
▼
|
|
||||||
┌─────────────┐
|
|
||||||
│ Nginx │ (反向代理)
|
|
||||||
└──────┬──────┘
|
|
||||||
│
|
|
||||||
├─────────────► 静态资源
|
|
||||||
│
|
|
||||||
▼
|
|
||||||
┌─────────────┐
|
|
||||||
│ Spring Boot │ (REST API)
|
|
||||||
└──────┬──────┘
|
|
||||||
│
|
|
||||||
├──────► MySQL (数据存储)
|
|
||||||
│
|
|
||||||
├──────► Dify API (AI服务)
|
|
||||||
│
|
|
||||||
└──────► 文献数据库 (PubMed/EMBASE/CNKI)
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🗂️ 数据库设计
|
|
||||||
|
|
||||||
主要数据表:
|
|
||||||
- `users` - 用户管理
|
|
||||||
- `inquiry_requests` - 查询请求
|
|
||||||
- `knowledge_bases` - 知识库配置
|
|
||||||
- `literatures` - 文献信息
|
|
||||||
- `audit_logs` - 审核日志
|
|
||||||
|
|
||||||
详细的数据库结构请参考 [database/schema.sql](./database/schema.sql)
|
|
||||||
|
|
||||||
## 🔐 安全性
|
|
||||||
|
|
||||||
- JWT身份认证
|
|
||||||
- 基于角色的访问控制(RBAC)
|
|
||||||
- 密码加密存储
|
|
||||||
- API请求限流
|
|
||||||
- SQL注入防护
|
|
||||||
|
|
||||||
## 🌐 API接口
|
|
||||||
|
|
||||||
### 查询管理
|
|
||||||
```
|
|
||||||
POST /api/inquiries/upload # 上传查询表格
|
|
||||||
POST /api/inquiries # 创建查询
|
|
||||||
GET /api/inquiries # 查询列表
|
|
||||||
GET /api/inquiries/{id} # 查询详情
|
|
||||||
POST /api/inquiries/{id}/extract-keywords # 提取关键词
|
|
||||||
POST /api/inquiries/{id}/search # 执行检索
|
|
||||||
POST /api/inquiries/{id}/generate-response # 生成回复
|
|
||||||
POST /api/inquiries/{id}/review # 审核
|
|
||||||
```
|
|
||||||
|
|
||||||
### 知识库管理
|
|
||||||
```
|
|
||||||
GET /api/knowledge-bases # 列表
|
|
||||||
POST /api/knowledge-bases # 创建
|
|
||||||
PUT /api/knowledge-bases/{id} # 更新
|
|
||||||
DELETE /api/knowledge-bases/{id} # 删除
|
|
||||||
```
|
|
||||||
|
|
||||||
### 文献管理
|
|
||||||
```
|
|
||||||
GET /api/literatures/inquiry/{id} # 获取文献
|
|
||||||
POST /api/literatures/{id}/download # 下载文献
|
|
||||||
POST /api/literatures/inquiry/{id}/download-selected # 批量下载
|
|
||||||
```
|
|
||||||
|
|
||||||
## 📝 开发计划
|
|
||||||
|
|
||||||
- [ ] 实现完整的用户认证和授权
|
|
||||||
- [ ] 集成实际的Dify API
|
|
||||||
- [ ] 实现文献自动下载功能
|
|
||||||
- [ ] 添加数据统计和报表功能
|
|
||||||
- [ ] 实现邮件通知功能
|
|
||||||
- [ ] 添加API文档(Swagger)
|
|
||||||
- [ ] 性能优化和缓存策略
|
|
||||||
- [ ] 单元测试和集成测试
|
|
||||||
|
|
||||||
## 🤝 贡献指南
|
|
||||||
|
|
||||||
本项目为企业内部项目,暂不接受外部贡献。
|
|
||||||
|
|
||||||
## 📄 许可证
|
|
||||||
|
|
||||||
本项目为专有软件,版权归益普生(Ipsen)所有。
|
|
||||||
|
|
||||||
## 👥 联系方式
|
|
||||||
|
|
||||||
如有问题或建议,请联系项目团队。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
<div align="center">
|
|
||||||
Made with ❤️ for Ipsen Medical Information Team
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
306
Readme.md
306
Readme.md
|
|
@ -1,44 +1,280 @@
|
||||||
Readme
|
# 益普生医学信息支持系统 - 软件开发需求文档
|
||||||
|
|
||||||
网站目的及功能
|
## 1. 项目概述
|
||||||
|
|
||||||
以益普生的达菲林为具体数据,我需要构建一个网站,解决企业在受到客户(主要是医生)的信息支持需求时,能够利用该网站快速处理信息,给到客户支持。
|
### 1.1 项目目的
|
||||||
|
构建一个智能化的医学信息支持系统,解决企业在受到客户(主要是医生)的信息支持需求时,能够利用该网站快速处理信息,给到客户支持。
|
||||||
业务流程
|
|
||||||
1.客户通过邮件,向益普生提出需求,比如,请提供达菲林最新的临床试验数据。
|
|
||||||
2.医学信息团队会记录这些需求,并指派给专门人员进行信息的查询,并进行回复。
|
|
||||||
3.需要的网站功能从此步开始,医学信息专员将需要查询的内容,以标准化的表格,上传到我们的网站。
|
|
||||||
4.网站收到表格后,会进行处理,并给出回复。具体的处理步骤如下:
|
|
||||||
4.1 提取需要检索的关键词,如药物名称、疾病、问题。
|
|
||||||
4.2 根据关键词,进行信息检索。检索的顺序,首先是企业自有数据、第二部 公开数据; 第三步关联的扩展数据数据。
|
|
||||||
4.3 根据检索到的信息,进行整理,形成回复的内容。
|
|
||||||
4.4 将回复的内容,发送给医学信息人员,由其进行审核。
|
|
||||||
4.5 确认回复的思路后,指出需要下载的文献,系统根据客户需求,将文献下载到本地。下载文献所用账号,之前已配置于系统中。
|
|
||||||
4.6 将下载的文献,发送给医学信息人员,由其进行审核。
|
|
||||||
4.7 确认无误后,将回复的内容,发送给客户。
|
|
||||||
|
|
||||||
为了支持信息的查询,系统的后台需要配置以下信息:
|
|
||||||
1. 检索的顺序,首先是企业自有数据、第二部 公开数据; 第三步关联的扩展数据数据。
|
|
||||||
2. 检索的文献,需要配置文献的下载账号。
|
|
||||||
3. 需要配置文献的下载路径。
|
|
||||||
4. 知识库的配置,包括知识库的类型、知识库的地址等。
|
|
||||||
4.1 自由数据:以企业开展的研究、历史的回复、文献等为主;
|
|
||||||
4.2 公开数据 :以第三方数据库为主,包括监管机构网站、知网、Pubmed、EMBASE、ClinicalTrials.gov等已有数据,此类数据已由人工进行过整理,并配置于系统中;
|
|
||||||
4.3 扩展数据:以关联的扩展数据为主,包括疾病、药物、疾病药物关联等数据,此类数据已由人工进行过整理,并配置于系统中;
|
|
||||||
5. 系统应有知识库的管理页面,以方便管理知识库中的数据。
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
技术架构要求:
|
|
||||||
1. 开发语言 Java
|
|
||||||
2. 系统应采用前后端分离的架构,前端采用Vue3,后端采用Spring Boot。
|
|
||||||
3. 数据库采用MySQL。
|
|
||||||
4. 涉及到AI相关功能,需要调用大模型的API,因此需要配置大模型的API Key。
|
|
||||||
5. 将使用Dify作为AI的引擎,因此需要配置Dify的API Key。
|
|
||||||
|
|
||||||
|
### 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和用户文档
|
||||||
|
- **模块化设计**:便于功能扩展和维护
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
171
START_GUIDE.md
171
START_GUIDE.md
|
|
@ -1,171 +0,0 @@
|
||||||
# 快速启动指南
|
|
||||||
|
|
||||||
## 方式一:使用Docker(推荐 - 最简单)
|
|
||||||
|
|
||||||
### 1. 确保Docker Desktop已安装并运行
|
|
||||||
|
|
||||||
### 2. 创建配置文件
|
|
||||||
在项目根目录创建 `.env` 文件(如果不存在):
|
|
||||||
```bash
|
|
||||||
copy .env.example .env
|
|
||||||
```
|
|
||||||
|
|
||||||
编辑 `.env` 文件,配置必要的信息(数据库密码、API密钥等)
|
|
||||||
|
|
||||||
### 3. 启动所有服务
|
|
||||||
```bash
|
|
||||||
# Windows
|
|
||||||
start.bat
|
|
||||||
|
|
||||||
# 或者手动执行
|
|
||||||
docker-compose up -d
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4. 访问系统
|
|
||||||
- 前端: http://localhost
|
|
||||||
- 后端API: http://localhost:8080/api
|
|
||||||
- 默认账号: admin / admin123
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 方式二:本地开发环境
|
|
||||||
|
|
||||||
### 前置要求
|
|
||||||
- ✅ JDK 17+
|
|
||||||
- ✅ Maven 3.8+
|
|
||||||
- ✅ Node.js 18+
|
|
||||||
- ✅ MySQL 8.0+
|
|
||||||
|
|
||||||
### 步骤1: 启动MySQL数据库
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 启动MySQL服务
|
|
||||||
# Windows: 在服务中启动MySQL
|
|
||||||
# Linux: sudo systemctl start mysql
|
|
||||||
```
|
|
||||||
|
|
||||||
### 步骤2: 创建数据库
|
|
||||||
|
|
||||||
```sql
|
|
||||||
-- 连接MySQL
|
|
||||||
mysql -u root -p
|
|
||||||
|
|
||||||
-- 执行数据库脚本
|
|
||||||
source database/schema.sql
|
|
||||||
source database/sample_data.sql
|
|
||||||
```
|
|
||||||
|
|
||||||
### 步骤3: 启动后端服务
|
|
||||||
|
|
||||||
**打开第一个命令行窗口:**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 进入后端目录
|
|
||||||
cd backend
|
|
||||||
|
|
||||||
# 启动Spring Boot(会自动编译)
|
|
||||||
mvn spring-boot:run
|
|
||||||
```
|
|
||||||
|
|
||||||
或者如果已经编译过:
|
|
||||||
```bash
|
|
||||||
cd backend
|
|
||||||
java -jar target/medical-info-system-1.0.0.jar
|
|
||||||
```
|
|
||||||
|
|
||||||
后端服务将在 **http://localhost:8080** 启动
|
|
||||||
|
|
||||||
### 步骤4: 启动前端服务
|
|
||||||
|
|
||||||
**打开第二个命令行窗口:**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 进入前端目录
|
|
||||||
cd frontend
|
|
||||||
|
|
||||||
# 安装依赖(首次运行)
|
|
||||||
npm install
|
|
||||||
|
|
||||||
# 启动开发服务器
|
|
||||||
npm run dev
|
|
||||||
```
|
|
||||||
|
|
||||||
前端服务将在 **http://localhost:3000** 启动
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 验证服务状态
|
|
||||||
|
|
||||||
### 检查后端
|
|
||||||
访问: http://localhost:8080/api/inquiries
|
|
||||||
|
|
||||||
应该返回JSON数据(即使是空数组)
|
|
||||||
|
|
||||||
### 检查前端
|
|
||||||
访问: http://localhost:3000
|
|
||||||
|
|
||||||
应该看到登录页面
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 常见问题
|
|
||||||
|
|
||||||
### 1. 端口被占用
|
|
||||||
```bash
|
|
||||||
# Windows查看端口占用
|
|
||||||
netstat -ano | findstr :8080
|
|
||||||
netstat -ano | findstr :3000
|
|
||||||
|
|
||||||
# 结束进程
|
|
||||||
taskkill /PID <进程ID> /F
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. 后端启动失败
|
|
||||||
- 检查MySQL是否已启动
|
|
||||||
- 检查`application.yml`中的数据库配置
|
|
||||||
- 确认数据库已创建
|
|
||||||
|
|
||||||
### 3. 前端启动失败
|
|
||||||
- 删除`node_modules`文件夹,重新`npm install`
|
|
||||||
- 检查Node.js版本是否为18+
|
|
||||||
|
|
||||||
### 4. Maven依赖下载慢
|
|
||||||
编辑 `backend/pom.xml`,添加国内镜像:
|
|
||||||
```xml
|
|
||||||
<repositories>
|
|
||||||
<repository>
|
|
||||||
<id>aliyun</id>
|
|
||||||
<url>https://maven.aliyun.com/repository/public</url>
|
|
||||||
</repository>
|
|
||||||
</repositories>
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 开发工具建议
|
|
||||||
|
|
||||||
### 后端开发
|
|
||||||
- **IntelliJ IDEA** (推荐)
|
|
||||||
- **Eclipse** with Spring Tools
|
|
||||||
|
|
||||||
### 前端开发
|
|
||||||
- **VS Code** (推荐)
|
|
||||||
- 安装插件: Vue Language Features (Volar)
|
|
||||||
- 安装插件: ESLint
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 下一步
|
|
||||||
|
|
||||||
1. 使用默认账号登录系统
|
|
||||||
2. 创建第一个查询请求
|
|
||||||
3. 配置知识库
|
|
||||||
4. 配置Dify API和大模型API
|
|
||||||
|
|
||||||
详细文档请参考:
|
|
||||||
- [PROJECT_STRUCTURE.md](./PROJECT_STRUCTURE.md) - 项目结构
|
|
||||||
- [DEPLOYMENT.md](./DEPLOYMENT.md) - 完整部署指南
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -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>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -11,6 +11,7 @@ import org.springframework.security.web.SecurityFilterChain;
|
||||||
import org.springframework.web.cors.CorsConfiguration;
|
import org.springframework.web.cors.CorsConfiguration;
|
||||||
import org.springframework.web.cors.CorsConfigurationSource;
|
import org.springframework.web.cors.CorsConfigurationSource;
|
||||||
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
|
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
|
||||||
|
import org.springframework.web.reactive.function.client.WebClient;
|
||||||
|
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
|
|
||||||
|
|
@ -54,4 +55,10 @@ public class SecurityConfig {
|
||||||
public PasswordEncoder passwordEncoder() {
|
public PasswordEncoder passwordEncoder() {
|
||||||
return new BCryptPasswordEncoder();
|
return new BCryptPasswordEncoder();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public WebClient.Builder webClientBuilder() {
|
||||||
|
return WebClient.builder()
|
||||||
|
.codecs(configurer -> configurer.defaultCodecs().maxInMemorySize(10 * 1024 * 1024)); // 10MB buffer
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -0,0 +1,153 @@
|
||||||
|
package com.ipsen.medical.controller;
|
||||||
|
|
||||||
|
import com.ipsen.medical.dto.ApiResponse;
|
||||||
|
import com.ipsen.medical.dto.DrugDTO;
|
||||||
|
import com.ipsen.medical.dto.DrugSafetyInfoDTO;
|
||||||
|
import com.ipsen.medical.service.DrugService;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 药物信息控制器
|
||||||
|
*/
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/drugs")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@CrossOrigin(origins = "*")
|
||||||
|
public class DrugController {
|
||||||
|
|
||||||
|
private final DrugService drugService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取所有药物列表
|
||||||
|
*/
|
||||||
|
@GetMapping
|
||||||
|
public ResponseEntity<ApiResponse<List<DrugDTO>>> getAllDrugs() {
|
||||||
|
List<DrugDTO> drugs = drugService.getAllDrugs();
|
||||||
|
return ResponseEntity.ok(ApiResponse.success(drugs));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据ID获取药物详情(包含所有安全信息)
|
||||||
|
*/
|
||||||
|
@GetMapping("/{id}")
|
||||||
|
public ResponseEntity<ApiResponse<DrugDTO>> getDrugById(@PathVariable Long id) {
|
||||||
|
DrugDTO drug = drugService.getDrugById(id);
|
||||||
|
return ResponseEntity.ok(ApiResponse.success(drug));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据药物编码获取药物详情
|
||||||
|
*/
|
||||||
|
@GetMapping("/code/{drugCode}")
|
||||||
|
public ResponseEntity<ApiResponse<DrugDTO>> getDrugByCode(@PathVariable String drugCode) {
|
||||||
|
DrugDTO drug = drugService.getDrugByCode(drugCode);
|
||||||
|
return ResponseEntity.ok(ApiResponse.success(drug));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 搜索药物
|
||||||
|
*/
|
||||||
|
@GetMapping("/search")
|
||||||
|
public ResponseEntity<ApiResponse<List<DrugDTO>>> searchDrugs(@RequestParam String keyword) {
|
||||||
|
List<DrugDTO> drugs = drugService.searchDrugs(keyword);
|
||||||
|
return ResponseEntity.ok(ApiResponse.success(drugs));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建药物
|
||||||
|
*/
|
||||||
|
@PostMapping
|
||||||
|
public ResponseEntity<ApiResponse<DrugDTO>> createDrug(@RequestBody DrugDTO drugDTO) {
|
||||||
|
DrugDTO created = drugService.createDrug(drugDTO);
|
||||||
|
return ResponseEntity.ok(ApiResponse.success(created));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新药物
|
||||||
|
*/
|
||||||
|
@PutMapping("/{id}")
|
||||||
|
public ResponseEntity<ApiResponse<DrugDTO>> updateDrug(
|
||||||
|
@PathVariable Long id,
|
||||||
|
@RequestBody DrugDTO drugDTO) {
|
||||||
|
DrugDTO updated = drugService.updateDrug(id, drugDTO);
|
||||||
|
return ResponseEntity.ok(ApiResponse.success(updated));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除药物
|
||||||
|
*/
|
||||||
|
@DeleteMapping("/{id}")
|
||||||
|
public ResponseEntity<ApiResponse<Void>> deleteDrug(@PathVariable Long id) {
|
||||||
|
drugService.deleteDrug(id);
|
||||||
|
return ResponseEntity.ok(ApiResponse.success(null));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取药物的所有安全信息
|
||||||
|
*/
|
||||||
|
@GetMapping("/{id}/safety-infos")
|
||||||
|
public ResponseEntity<ApiResponse<List<DrugSafetyInfoDTO>>> getDrugSafetyInfos(
|
||||||
|
@PathVariable Long id) {
|
||||||
|
List<DrugSafetyInfoDTO> safetyInfos = drugService.getDrugSafetyInfos(id);
|
||||||
|
return ResponseEntity.ok(ApiResponse.success(safetyInfos));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据来源获取安全信息
|
||||||
|
*/
|
||||||
|
@GetMapping("/{id}/safety-infos/source/{source}")
|
||||||
|
public ResponseEntity<ApiResponse<List<DrugSafetyInfoDTO>>> getDrugSafetyInfosBySource(
|
||||||
|
@PathVariable Long id,
|
||||||
|
@PathVariable String source) {
|
||||||
|
List<DrugSafetyInfoDTO> safetyInfos = drugService.getDrugSafetyInfosBySource(id, source);
|
||||||
|
return ResponseEntity.ok(ApiResponse.success(safetyInfos));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加安全信息
|
||||||
|
*/
|
||||||
|
@PostMapping("/{id}/safety-infos")
|
||||||
|
public ResponseEntity<ApiResponse<DrugSafetyInfoDTO>> addSafetyInfo(
|
||||||
|
@PathVariable Long id,
|
||||||
|
@RequestBody DrugSafetyInfoDTO safetyInfoDTO) {
|
||||||
|
DrugSafetyInfoDTO created = drugService.addSafetyInfo(id, safetyInfoDTO);
|
||||||
|
return ResponseEntity.ok(ApiResponse.success(created));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新安全信息
|
||||||
|
*/
|
||||||
|
@PutMapping("/safety-infos/{safetyInfoId}")
|
||||||
|
public ResponseEntity<ApiResponse<DrugSafetyInfoDTO>> updateSafetyInfo(
|
||||||
|
@PathVariable Long safetyInfoId,
|
||||||
|
@RequestBody DrugSafetyInfoDTO safetyInfoDTO) {
|
||||||
|
DrugSafetyInfoDTO updated = drugService.updateSafetyInfo(safetyInfoId, safetyInfoDTO);
|
||||||
|
return ResponseEntity.ok(ApiResponse.success(updated));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除安全信息
|
||||||
|
*/
|
||||||
|
@DeleteMapping("/safety-infos/{safetyInfoId}")
|
||||||
|
public ResponseEntity<ApiResponse<Void>> deleteSafetyInfo(@PathVariable Long safetyInfoId) {
|
||||||
|
drugService.deleteSafetyInfo(safetyInfoId);
|
||||||
|
return ResponseEntity.ok(ApiResponse.success(null));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证安全信息
|
||||||
|
*/
|
||||||
|
@PostMapping("/safety-infos/{safetyInfoId}/verify")
|
||||||
|
public ResponseEntity<ApiResponse<DrugSafetyInfoDTO>> verifySafetyInfo(
|
||||||
|
@PathVariable Long safetyInfoId,
|
||||||
|
@RequestParam String verifiedBy) {
|
||||||
|
DrugSafetyInfoDTO verified = drugService.verifySafetyInfo(safetyInfoId, verifiedBy);
|
||||||
|
return ResponseEntity.ok(ApiResponse.success(verified));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -1,9 +1,16 @@
|
||||||
package com.ipsen.medical.controller;
|
package com.ipsen.medical.controller;
|
||||||
|
|
||||||
import com.ipsen.medical.dto.ApiResponse;
|
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.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;
|
import com.ipsen.medical.service.InquiryService;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.http.HttpHeaders;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
@ -20,6 +27,8 @@ import java.util.List;
|
||||||
public class InquiryController {
|
public class InquiryController {
|
||||||
|
|
||||||
private final InquiryService inquiryService;
|
private final InquiryService inquiryService;
|
||||||
|
private final ClinicalTrialsService clinicalTrialsService;
|
||||||
|
private final AutoSearchService autoSearchService;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 上传查询表格
|
* 上传查询表格
|
||||||
|
|
@ -69,6 +78,28 @@ public class InquiryController {
|
||||||
return ResponseEntity.ok(ApiResponse.success(result));
|
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));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 执行信息检索
|
* 执行信息检索
|
||||||
*/
|
*/
|
||||||
|
|
@ -107,6 +138,73 @@ public class InquiryController {
|
||||||
InquiryRequestDTO result = inquiryService.completeInquiry(id);
|
InquiryRequestDTO result = inquiryService.completeInquiry(id);
|
||||||
return ResponseEntity.ok(ApiResponse.success(result));
|
return ResponseEntity.ok(ApiResponse.success(result));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 搜索临床试验
|
||||||
|
*/
|
||||||
|
@PostMapping("/{id}/clinical-trials/search")
|
||||||
|
public ResponseEntity<ApiResponse<List<ClinicalTrialDTO>>> searchClinicalTrials(
|
||||||
|
@PathVariable Long id,
|
||||||
|
@RequestParam String keyword) {
|
||||||
|
List<ClinicalTrialDTO> results = clinicalTrialsService.searchAndSaveForInquiry(id, keyword);
|
||||||
|
return ResponseEntity.ok(ApiResponse.success(results));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取查询请求的临床试验列表
|
||||||
|
*/
|
||||||
|
@GetMapping("/{id}/clinical-trials")
|
||||||
|
public ResponseEntity<ApiResponse<List<ClinicalTrialDTO>>> getClinicalTrials(@PathVariable Long id) {
|
||||||
|
List<ClinicalTrialDTO> results = clinicalTrialsService.getClinicalTrialsByInquiry(id);
|
||||||
|
return ResponseEntity.ok(ApiResponse.success(results));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 导出临床试验为CSV
|
||||||
|
*/
|
||||||
|
@GetMapping("/{id}/clinical-trials/export")
|
||||||
|
public ResponseEntity<byte[]> exportClinicalTrialsToCsv(@PathVariable Long id) {
|
||||||
|
byte[] csvData = clinicalTrialsService.exportToCsv(id);
|
||||||
|
|
||||||
|
HttpHeaders headers = new HttpHeaders();
|
||||||
|
headers.setContentType(MediaType.parseMediaType("text/csv"));
|
||||||
|
headers.setContentDispositionFormData("attachment", "clinical_trials_" + id + ".csv");
|
||||||
|
|
||||||
|
return ResponseEntity.ok()
|
||||||
|
.headers(headers)
|
||||||
|
.body(csvData);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除查询请求的临床试验数据
|
||||||
|
*/
|
||||||
|
@DeleteMapping("/{id}/clinical-trials")
|
||||||
|
public ResponseEntity<ApiResponse<Void>> deleteClinicalTrials(@PathVariable Long id) {
|
||||||
|
clinicalTrialsService.deleteClinicalTrials(id);
|
||||||
|
return ResponseEntity.ok(ApiResponse.success(null));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 基于AI识别的关键词执行自动检索
|
||||||
|
* 包括:临床试验、文献数据库等
|
||||||
|
*/
|
||||||
|
@PostMapping("/{id}/auto-search")
|
||||||
|
public ResponseEntity<ApiResponse<InquiryRequestDTO>> performAutoSearch(@PathVariable Long id) {
|
||||||
|
InquiryRequestDTO result = autoSearchService.performAutoSearch(id);
|
||||||
|
return ResponseEntity.ok(ApiResponse.success(result));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 执行完整的AI工作流
|
||||||
|
* 1. 提取关键词
|
||||||
|
* 2. 自动检索(临床试验、知识库)
|
||||||
|
* 3. 生成回复
|
||||||
|
*/
|
||||||
|
@PostMapping("/{id}/full-workflow")
|
||||||
|
public ResponseEntity<ApiResponse<InquiryRequestDTO>> executeFullWorkflow(@PathVariable Long id) {
|
||||||
|
InquiryRequestDTO result = autoSearchService.executeFullWorkflow(id);
|
||||||
|
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;
|
package com.ipsen.medical.controller;
|
||||||
|
|
||||||
import org.springframework.web.bind.annotation.CrossOrigin;
|
import com.ipsen.medical.dto.ApiResponse;
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
import com.ipsen.medical.service.DifyService;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import java.lang.reflect.Field;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
|
|
@ -14,8 +15,11 @@ import java.util.Map;
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/test")
|
@RequestMapping("/test")
|
||||||
@CrossOrigin(origins = "*")
|
@CrossOrigin(origins = "*")
|
||||||
|
@RequiredArgsConstructor
|
||||||
public class TestController {
|
public class TestController {
|
||||||
|
|
||||||
|
private final DifyService difyService;
|
||||||
|
|
||||||
@GetMapping("/health")
|
@GetMapping("/health")
|
||||||
public Map<String, Object> health() {
|
public Map<String, Object> health() {
|
||||||
Map<String, Object> response = new HashMap<>();
|
Map<String, Object> response = new HashMap<>();
|
||||||
|
|
@ -34,4 +38,43 @@ public class TestController {
|
||||||
response.put("java.home", System.getProperty("java.home"));
|
response.put("java.home", System.getProperty("java.home"));
|
||||||
return response;
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,52 @@
|
||||||
|
package com.ipsen.medical.dto;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 临床试验数据传输对象
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class ClinicalTrialDTO {
|
||||||
|
|
||||||
|
private Long id;
|
||||||
|
private Long inquiryId;
|
||||||
|
private String nctId;
|
||||||
|
private String studyTitle;
|
||||||
|
private String briefTitle;
|
||||||
|
private String officialTitle;
|
||||||
|
private String overallStatus;
|
||||||
|
private String startDate;
|
||||||
|
private String completionDate;
|
||||||
|
private String studyType;
|
||||||
|
private String phase;
|
||||||
|
private Integer enrollment;
|
||||||
|
private List<String> conditions;
|
||||||
|
private List<String> interventions;
|
||||||
|
private String sponsor;
|
||||||
|
private List<String> collaborators;
|
||||||
|
private List<LocationInfo> locations;
|
||||||
|
private String briefSummary;
|
||||||
|
private String detailedDescription;
|
||||||
|
private String primaryOutcome;
|
||||||
|
private String secondaryOutcome;
|
||||||
|
private String eligibilityCriteria;
|
||||||
|
private String url;
|
||||||
|
private LocalDateTime createdAt;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public static class LocationInfo {
|
||||||
|
private String facility;
|
||||||
|
private String city;
|
||||||
|
private String state;
|
||||||
|
private String country;
|
||||||
|
private String status;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -0,0 +1,31 @@
|
||||||
|
package com.ipsen.medical.dto;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 临床试验搜索结果
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class ClinicalTrialsSearchResult {
|
||||||
|
|
||||||
|
private String searchTerm; // 搜索关键词
|
||||||
|
private Integer totalCount; // 总结果数
|
||||||
|
private List<ClinicalTrialDTO> studies; // 研究列表
|
||||||
|
private String nextPageToken; // 下一页token(如果有分页)
|
||||||
|
|
||||||
|
public ClinicalTrialsSearchResult() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public ClinicalTrialsSearchResult(String searchTerm, Integer totalCount, List<ClinicalTrialDTO> studies) {
|
||||||
|
this.searchTerm = searchTerm;
|
||||||
|
this.totalCount = totalCount;
|
||||||
|
this.studies = studies;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -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中检索
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -0,0 +1,41 @@
|
||||||
|
package com.ipsen.medical.dto;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dify API请求
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class DifyRequest {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 输入内容
|
||||||
|
*/
|
||||||
|
private Map<String, Object> inputs;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询内容
|
||||||
|
*/
|
||||||
|
private String query;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 响应模式: blocking(同步), streaming(流式)
|
||||||
|
*/
|
||||||
|
@JsonProperty("response_mode")
|
||||||
|
private String responseMode = "blocking";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 会话ID(可选)
|
||||||
|
*/
|
||||||
|
@JsonProperty("conversation_id")
|
||||||
|
private String conversationId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户标识
|
||||||
|
*/
|
||||||
|
private String user;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -0,0 +1,58 @@
|
||||||
|
package com.ipsen.medical.dto;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dify API响应
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class DifyResponse {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 事件类型
|
||||||
|
*/
|
||||||
|
private String event;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 任务ID
|
||||||
|
*/
|
||||||
|
@JsonProperty("task_id")
|
||||||
|
private String taskId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 消息ID
|
||||||
|
*/
|
||||||
|
@JsonProperty("message_id")
|
||||||
|
private String messageId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 会话ID
|
||||||
|
*/
|
||||||
|
@JsonProperty("conversation_id")
|
||||||
|
private String conversationId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 模式
|
||||||
|
*/
|
||||||
|
private String mode;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 回答内容
|
||||||
|
*/
|
||||||
|
private String answer;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 元数据
|
||||||
|
*/
|
||||||
|
private Map<String, Object> metadata;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建时间
|
||||||
|
*/
|
||||||
|
@JsonProperty("created_at")
|
||||||
|
private Long createdAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -0,0 +1,28 @@
|
||||||
|
package com.ipsen.medical.dto;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class DrugDTO {
|
||||||
|
private Long id;
|
||||||
|
private String drugCode;
|
||||||
|
private String genericName;
|
||||||
|
private String tradeName;
|
||||||
|
private String activeIngredient;
|
||||||
|
private String description;
|
||||||
|
private String manufacturer;
|
||||||
|
private String approvalNumber;
|
||||||
|
private String indications;
|
||||||
|
private String dosageAndAdministration;
|
||||||
|
private String contraindications;
|
||||||
|
private String therapeuticClass;
|
||||||
|
private String atcCode;
|
||||||
|
private String status;
|
||||||
|
private LocalDateTime createdAt;
|
||||||
|
private LocalDateTime updatedAt;
|
||||||
|
private List<DrugSafetyInfoDTO> safetyInfos;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -0,0 +1,27 @@
|
||||||
|
package com.ipsen.medical.dto;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class DrugSafetyInfoDTO {
|
||||||
|
private Long id;
|
||||||
|
private Long drugId;
|
||||||
|
private String source; // INTERNAL, LITERATURE, REGULATORY, SOCIAL_MEDIA, CLINICAL_TRIAL
|
||||||
|
private String title;
|
||||||
|
private String content;
|
||||||
|
private String category; // 安全信息分类
|
||||||
|
private String severityLevel; // MILD, MODERATE, SEVERE, CRITICAL
|
||||||
|
private String referenceUrl;
|
||||||
|
private String referenceDocument;
|
||||||
|
private String reportedBy;
|
||||||
|
private LocalDateTime reportedAt;
|
||||||
|
private String additionalInfo;
|
||||||
|
private Boolean verified;
|
||||||
|
private String verifiedBy;
|
||||||
|
private LocalDateTime verifiedAt;
|
||||||
|
private LocalDateTime createdAt;
|
||||||
|
private LocalDateTime updatedAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -12,6 +12,11 @@ public class InquiryRequestDTO {
|
||||||
private String customerTitle;
|
private String customerTitle;
|
||||||
private String inquiryContent;
|
private String inquiryContent;
|
||||||
private String keywords;
|
private String keywords;
|
||||||
|
private Boolean keywordsConfirmed;
|
||||||
|
private Boolean searchInternalData;
|
||||||
|
private Boolean searchKnowledgeBase;
|
||||||
|
private Boolean searchCnki;
|
||||||
|
private Boolean searchClinicalTrials;
|
||||||
private String status;
|
private String status;
|
||||||
private String searchResults;
|
private String searchResults;
|
||||||
private String responseContent;
|
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,31 @@
|
||||||
|
package com.ipsen.medical.dto;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AI关键词提取结果
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class KeywordExtractionResult {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 药物中文名称
|
||||||
|
*/
|
||||||
|
private String drugNameChinese;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 药物英文名称
|
||||||
|
*/
|
||||||
|
private String drugNameEnglish;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询项目/问题
|
||||||
|
*/
|
||||||
|
private String requestItem;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 置信度
|
||||||
|
*/
|
||||||
|
private Double confidence;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -0,0 +1,106 @@
|
||||||
|
package com.ipsen.medical.entity;
|
||||||
|
|
||||||
|
import javax.persistence.*;
|
||||||
|
import lombok.Data;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 临床试验实体
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@Entity
|
||||||
|
@Table(name = "clinical_trials",
|
||||||
|
uniqueConstraints = @UniqueConstraint(columnNames = {"inquiry_id", "nct_id"}))
|
||||||
|
public class ClinicalTrial {
|
||||||
|
|
||||||
|
@Id
|
||||||
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
@Column(name = "inquiry_id")
|
||||||
|
private Long inquiryId; // 关联的查询请求ID
|
||||||
|
|
||||||
|
@Column(name = "nct_id")
|
||||||
|
private String nctId; // NCT编号(ClinicalTrials.gov的唯一标识)
|
||||||
|
|
||||||
|
@Column(name = "study_title", columnDefinition = "TEXT")
|
||||||
|
private String studyTitle; // 研究标题
|
||||||
|
|
||||||
|
@Column(name = "brief_title")
|
||||||
|
private String briefTitle; // 简短标题
|
||||||
|
|
||||||
|
@Column(name = "official_title", columnDefinition = "TEXT")
|
||||||
|
private String officialTitle; // 官方标题
|
||||||
|
|
||||||
|
@Column(name = "overall_status")
|
||||||
|
private String overallStatus; // 研究状态
|
||||||
|
|
||||||
|
@Column(name = "start_date")
|
||||||
|
private String startDate; // 开始日期
|
||||||
|
|
||||||
|
@Column(name = "completion_date")
|
||||||
|
private String completionDate; // 完成日期
|
||||||
|
|
||||||
|
@Column(name = "study_type")
|
||||||
|
private String studyType; // 研究类型
|
||||||
|
|
||||||
|
@Column(name = "phase")
|
||||||
|
private String phase; // 研究阶段
|
||||||
|
|
||||||
|
@Column(name = "enrollment")
|
||||||
|
private Integer enrollment; // 入组人数
|
||||||
|
|
||||||
|
@Column(name = "conditions", columnDefinition = "TEXT")
|
||||||
|
private String conditions; // 适应症(JSON数组)
|
||||||
|
|
||||||
|
@Column(name = "interventions", columnDefinition = "TEXT")
|
||||||
|
private String interventions; // 干预措施(JSON数组)
|
||||||
|
|
||||||
|
@Column(name = "sponsor")
|
||||||
|
private String sponsor; // 主要研究者/机构
|
||||||
|
|
||||||
|
@Column(name = "collaborators", columnDefinition = "TEXT")
|
||||||
|
private String collaborators; // 合作者(JSON数组)
|
||||||
|
|
||||||
|
@Column(name = "locations", columnDefinition = "TEXT")
|
||||||
|
private String locations; // 研究地点(JSON数组)
|
||||||
|
|
||||||
|
@Column(name = "brief_summary", columnDefinition = "TEXT")
|
||||||
|
private String briefSummary; // 简要摘要
|
||||||
|
|
||||||
|
@Column(name = "detailed_description", columnDefinition = "TEXT")
|
||||||
|
private String detailedDescription; // 详细描述
|
||||||
|
|
||||||
|
@Column(name = "primary_outcome", columnDefinition = "TEXT")
|
||||||
|
private String primaryOutcome; // 主要终点
|
||||||
|
|
||||||
|
@Column(name = "secondary_outcome", columnDefinition = "TEXT")
|
||||||
|
private String secondaryOutcome; // 次要终点
|
||||||
|
|
||||||
|
@Column(name = "eligibility_criteria", columnDefinition = "TEXT")
|
||||||
|
private String eligibilityCriteria; // 入选标准
|
||||||
|
|
||||||
|
@Column(name = "url")
|
||||||
|
private String url; // ClinicalTrials.gov链接
|
||||||
|
|
||||||
|
@Column(name = "raw_data", columnDefinition = "LONGTEXT")
|
||||||
|
private String rawData; // 原始API返回数据(JSON格式)
|
||||||
|
|
||||||
|
@Column(name = "created_at", nullable = false)
|
||||||
|
private LocalDateTime createdAt;
|
||||||
|
|
||||||
|
@Column(name = "updated_at")
|
||||||
|
private LocalDateTime updatedAt;
|
||||||
|
|
||||||
|
@PrePersist
|
||||||
|
protected void onCreate() {
|
||||||
|
createdAt = LocalDateTime.now();
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreUpdate
|
||||||
|
protected void onUpdate() {
|
||||||
|
updatedAt = LocalDateTime.now();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -0,0 +1,81 @@
|
||||||
|
package com.ipsen.medical.entity;
|
||||||
|
|
||||||
|
import javax.persistence.*;
|
||||||
|
import lombok.Data;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 药物实体
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@Entity
|
||||||
|
@Table(name = "drugs")
|
||||||
|
public class Drug {
|
||||||
|
|
||||||
|
@Id
|
||||||
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
@Column(nullable = false, unique = true)
|
||||||
|
private String drugCode; // 药物编码
|
||||||
|
|
||||||
|
@Column(nullable = false)
|
||||||
|
private String genericName; // 通用名
|
||||||
|
|
||||||
|
private String tradeName; // 商品名
|
||||||
|
|
||||||
|
private String activeIngredient; // 活性成分
|
||||||
|
|
||||||
|
@Column(columnDefinition = "TEXT")
|
||||||
|
private String description; // 药物描述
|
||||||
|
|
||||||
|
private String manufacturer; // 生产厂家
|
||||||
|
|
||||||
|
private String approvalNumber; // 批准文号
|
||||||
|
|
||||||
|
@Column(columnDefinition = "TEXT")
|
||||||
|
private String indications; // 适应症
|
||||||
|
|
||||||
|
@Column(columnDefinition = "TEXT")
|
||||||
|
private String dosageAndAdministration; // 用法用量
|
||||||
|
|
||||||
|
@Column(columnDefinition = "TEXT")
|
||||||
|
private String contraindications; // 禁忌症
|
||||||
|
|
||||||
|
private String therapeuticClass; // 治疗分类
|
||||||
|
|
||||||
|
private String atcCode; // ATC编码
|
||||||
|
|
||||||
|
@Enumerated(EnumType.STRING)
|
||||||
|
private DrugStatus status; // 药物状态
|
||||||
|
|
||||||
|
@Column(nullable = false)
|
||||||
|
private LocalDateTime createdAt;
|
||||||
|
|
||||||
|
private LocalDateTime updatedAt;
|
||||||
|
|
||||||
|
@OneToMany(mappedBy = "drug", cascade = CascadeType.ALL, orphanRemoval = true)
|
||||||
|
private List<DrugSafetyInfo> safetyInfos;
|
||||||
|
|
||||||
|
@PrePersist
|
||||||
|
protected void onCreate() {
|
||||||
|
createdAt = LocalDateTime.now();
|
||||||
|
if (status == null) {
|
||||||
|
status = DrugStatus.ACTIVE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreUpdate
|
||||||
|
protected void onUpdate() {
|
||||||
|
updatedAt = LocalDateTime.now();
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum DrugStatus {
|
||||||
|
ACTIVE, // 在市
|
||||||
|
SUSPENDED, // 暂停
|
||||||
|
WITHDRAWN // 撤市
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -0,0 +1,118 @@
|
||||||
|
package com.ipsen.medical.entity;
|
||||||
|
|
||||||
|
import javax.persistence.*;
|
||||||
|
import lombok.Data;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 药物安全信息实体
|
||||||
|
* 包括内部数据、文献数据、监管数据、自媒体数据
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@Entity
|
||||||
|
@Table(name = "drug_safety_infos")
|
||||||
|
public class DrugSafetyInfo {
|
||||||
|
|
||||||
|
@Id
|
||||||
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
@ManyToOne
|
||||||
|
@JoinColumn(name = "drug_id", nullable = false)
|
||||||
|
private Drug drug;
|
||||||
|
|
||||||
|
@Enumerated(EnumType.STRING)
|
||||||
|
@Column(nullable = false)
|
||||||
|
private InfoSource source; // 信息来源类型
|
||||||
|
|
||||||
|
@Column(nullable = false)
|
||||||
|
private String title; // 标题
|
||||||
|
|
||||||
|
@Column(columnDefinition = "TEXT")
|
||||||
|
private String content; // 内容详情
|
||||||
|
|
||||||
|
@Enumerated(EnumType.STRING)
|
||||||
|
@Column(nullable = false)
|
||||||
|
private SafetyCategory category; // 安全信息分类
|
||||||
|
|
||||||
|
@Enumerated(EnumType.STRING)
|
||||||
|
private SeverityLevel severityLevel; // 严重程度
|
||||||
|
|
||||||
|
private String referenceUrl; // 参考链接
|
||||||
|
|
||||||
|
private String referenceDocument; // 参考文档
|
||||||
|
|
||||||
|
private String reportedBy; // 报告来源
|
||||||
|
|
||||||
|
private LocalDateTime reportedAt; // 报告时间
|
||||||
|
|
||||||
|
@Column(columnDefinition = "TEXT")
|
||||||
|
private String additionalInfo; // 附加信息(JSON格式)
|
||||||
|
|
||||||
|
private Boolean verified; // 是否已验证
|
||||||
|
|
||||||
|
private String verifiedBy; // 验证人
|
||||||
|
|
||||||
|
private LocalDateTime verifiedAt; // 验证时间
|
||||||
|
|
||||||
|
@Column(nullable = false)
|
||||||
|
private LocalDateTime createdAt;
|
||||||
|
|
||||||
|
private LocalDateTime updatedAt;
|
||||||
|
|
||||||
|
@PrePersist
|
||||||
|
protected void onCreate() {
|
||||||
|
createdAt = LocalDateTime.now();
|
||||||
|
if (verified == null) {
|
||||||
|
verified = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreUpdate
|
||||||
|
protected void onUpdate() {
|
||||||
|
updatedAt = LocalDateTime.now();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 信息来源类型
|
||||||
|
*/
|
||||||
|
public enum InfoSource {
|
||||||
|
INTERNAL, // 内部数据
|
||||||
|
LITERATURE, // 文献数据
|
||||||
|
REGULATORY, // 监管公开信息
|
||||||
|
SOCIAL_MEDIA, // 自媒体信息
|
||||||
|
CLINICAL_TRIAL // 临床试验
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 安全信息分类
|
||||||
|
*/
|
||||||
|
public enum SafetyCategory {
|
||||||
|
ADVERSE_REACTION, // 不良反应
|
||||||
|
DRUG_INTERACTION, // 药物相互作用
|
||||||
|
CONTRAINDICATION, // 禁忌症
|
||||||
|
SPECIAL_POPULATION, // 特殊人群用药
|
||||||
|
OVERDOSE, // 过量
|
||||||
|
WITHDRAWAL_SYMPTOM, // 停药症状
|
||||||
|
LONG_TERM_EFFECT, // 长期使用影响
|
||||||
|
PREGNANCY_LACTATION, // 妊娠哺乳期用药
|
||||||
|
PEDIATRIC_USE, // 儿童用药
|
||||||
|
GERIATRIC_USE, // 老年用药
|
||||||
|
HEPATIC_IMPAIRMENT, // 肝功能不全
|
||||||
|
RENAL_IMPAIRMENT, // 肾功能不全
|
||||||
|
PRECAUTION, // 注意事项
|
||||||
|
WARNING // 警告信息
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 严重程度
|
||||||
|
*/
|
||||||
|
public enum SeverityLevel {
|
||||||
|
MILD, // 轻度
|
||||||
|
MODERATE, // 中度
|
||||||
|
SEVERE, // 重度
|
||||||
|
CRITICAL // 严重/致命
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -19,19 +19,28 @@ public class InquiryRequest {
|
||||||
@Column(nullable = false)
|
@Column(nullable = false)
|
||||||
private String requestNumber; // 请求编号
|
private String requestNumber; // 请求编号
|
||||||
|
|
||||||
@Column(nullable = false)
|
private String customerName; // 客户姓名(非必填)
|
||||||
private String customerName; // 客户姓名
|
|
||||||
|
|
||||||
private String customerEmail; // 客户邮箱
|
private String customerEmail; // 客户邮箱(非必填)
|
||||||
|
|
||||||
private String customerTitle; // 客户职称
|
private String customerTitle; // 客户职称
|
||||||
|
|
||||||
@Column(columnDefinition = "TEXT")
|
@Column(columnDefinition = "TEXT", nullable = false)
|
||||||
private String inquiryContent; // 查询内容
|
private String inquiryContent; // 查询内容(必填)
|
||||||
|
|
||||||
@Column(columnDefinition = "TEXT")
|
@Column(columnDefinition = "TEXT")
|
||||||
private String keywords; // 提取的关键词(JSON格式)
|
private String keywords; // 提取的关键词(JSON格式)
|
||||||
|
|
||||||
|
private Boolean keywordsConfirmed; // 用户是否确认关键词
|
||||||
|
|
||||||
|
private Boolean searchInternalData; // 是否在内部数据中检索
|
||||||
|
|
||||||
|
private Boolean searchKnowledgeBase; // 是否在知识库中检索
|
||||||
|
|
||||||
|
private Boolean searchCnki; // 是否在知网中检索
|
||||||
|
|
||||||
|
private Boolean searchClinicalTrials; // 是否在ClinicalTrials中检索
|
||||||
|
|
||||||
@Enumerated(EnumType.STRING)
|
@Enumerated(EnumType.STRING)
|
||||||
@Column(nullable = false)
|
@Column(nullable = false)
|
||||||
private RequestStatus status; // 状态
|
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 // 失败
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -0,0 +1,41 @@
|
||||||
|
package com.ipsen.medical.repository;
|
||||||
|
|
||||||
|
import com.ipsen.medical.entity.ClinicalTrial;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
import org.springframework.stereotype.Repository;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 临床试验数据仓库
|
||||||
|
*/
|
||||||
|
@Repository
|
||||||
|
public interface ClinicalTrialRepository extends JpaRepository<ClinicalTrial, Long> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据查询请求ID查找临床试验
|
||||||
|
*/
|
||||||
|
List<ClinicalTrial> findByInquiryId(Long inquiryId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据NCT ID查找
|
||||||
|
*/
|
||||||
|
Optional<ClinicalTrial> findByNctId(String nctId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据查询请求ID和NCT ID查找
|
||||||
|
*/
|
||||||
|
Optional<ClinicalTrial> findByInquiryIdAndNctId(Long inquiryId, String nctId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除指定查询请求的所有临床试验数据
|
||||||
|
*/
|
||||||
|
void deleteByInquiryId(Long inquiryId);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -0,0 +1,28 @@
|
||||||
|
package com.ipsen.medical.repository;
|
||||||
|
|
||||||
|
import com.ipsen.medical.entity.Drug;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
import org.springframework.data.jpa.repository.Query;
|
||||||
|
import org.springframework.data.repository.query.Param;
|
||||||
|
import org.springframework.stereotype.Repository;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
@Repository
|
||||||
|
public interface DrugRepository extends JpaRepository<Drug, Long> {
|
||||||
|
|
||||||
|
Optional<Drug> findByDrugCode(String drugCode);
|
||||||
|
|
||||||
|
List<Drug> findByStatus(Drug.DrugStatus status);
|
||||||
|
|
||||||
|
@Query("SELECT d FROM Drug d WHERE " +
|
||||||
|
"LOWER(d.genericName) LIKE LOWER(CONCAT('%', :keyword, '%')) OR " +
|
||||||
|
"LOWER(d.tradeName) LIKE LOWER(CONCAT('%', :keyword, '%')) OR " +
|
||||||
|
"LOWER(d.activeIngredient) LIKE LOWER(CONCAT('%', :keyword, '%'))")
|
||||||
|
List<Drug> searchDrugs(@Param("keyword") String keyword);
|
||||||
|
|
||||||
|
List<Drug> findByTherapeuticClass(String therapeuticClass);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
package com.ipsen.medical.repository;
|
||||||
|
|
||||||
|
import com.ipsen.medical.entity.DrugSafetyInfo;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
import org.springframework.stereotype.Repository;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Repository
|
||||||
|
public interface DrugSafetyInfoRepository extends JpaRepository<DrugSafetyInfo, Long> {
|
||||||
|
|
||||||
|
List<DrugSafetyInfo> findByDrugId(Long drugId);
|
||||||
|
|
||||||
|
List<DrugSafetyInfo> findByDrugIdAndSource(Long drugId, DrugSafetyInfo.InfoSource source);
|
||||||
|
|
||||||
|
List<DrugSafetyInfo> findByDrugIdAndCategory(Long drugId, DrugSafetyInfo.SafetyCategory category);
|
||||||
|
|
||||||
|
List<DrugSafetyInfo> findByDrugIdAndVerified(Long drugId, Boolean verified);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
package com.ipsen.medical.service;
|
||||||
|
|
||||||
|
import com.ipsen.medical.dto.InquiryRequestDTO;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 自动检索服务接口
|
||||||
|
*/
|
||||||
|
public interface AutoSearchService {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 基于AI识别的关键词执行自动检索
|
||||||
|
* 包括:临床试验、文献数据库等
|
||||||
|
*/
|
||||||
|
InquiryRequestDTO performAutoSearch(Long inquiryId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 执行完整的AI工作流
|
||||||
|
* 1. 提取关键词
|
||||||
|
* 2. 自动检索
|
||||||
|
* 3. 生成回复
|
||||||
|
*/
|
||||||
|
InquiryRequestDTO executeFullWorkflow(Long inquiryId);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -0,0 +1,54 @@
|
||||||
|
package com.ipsen.medical.service;
|
||||||
|
|
||||||
|
import com.ipsen.medical.dto.ClinicalTrialDTO;
|
||||||
|
import com.ipsen.medical.dto.ClinicalTrialsSearchResult;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 临床试验服务接口
|
||||||
|
*/
|
||||||
|
public interface ClinicalTrialsService {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据关键词搜索临床试验
|
||||||
|
* @param keyword 搜索关键词(通常是药品名称)
|
||||||
|
* @param pageSize 每页数量
|
||||||
|
* @return 搜索结果
|
||||||
|
*/
|
||||||
|
ClinicalTrialsSearchResult searchClinicalTrials(String keyword, Integer pageSize);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 为指定查询请求搜索并保存临床试验数据
|
||||||
|
* @param inquiryId 查询请求ID
|
||||||
|
* @param keyword 搜索关键词
|
||||||
|
* @return 找到的临床试验列表
|
||||||
|
*/
|
||||||
|
List<ClinicalTrialDTO> searchAndSaveForInquiry(Long inquiryId, String keyword);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取指定查询请求的所有临床试验
|
||||||
|
* @param inquiryId 查询请求ID
|
||||||
|
* @return 临床试验列表
|
||||||
|
*/
|
||||||
|
List<ClinicalTrialDTO> getClinicalTrialsByInquiry(Long inquiryId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 导出临床试验数据为CSV格式
|
||||||
|
* @param inquiryId 查询请求ID
|
||||||
|
* @return CSV内容(字节数组)
|
||||||
|
*/
|
||||||
|
byte[] exportToCsv(Long inquiryId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除指定查询请求的临床试验数据
|
||||||
|
* @param inquiryId 查询请求ID
|
||||||
|
*/
|
||||||
|
void deleteClinicalTrials(Long inquiryId);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -1,12 +1,20 @@
|
||||||
package com.ipsen.medical.service;
|
package com.ipsen.medical.service;
|
||||||
|
|
||||||
|
import com.ipsen.medical.dto.KeywordExtractionResult;
|
||||||
|
import com.ipsen.medical.dto.ResponseGenerationDTO;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Dify AI服务接口
|
* Dify AI服务接口
|
||||||
*/
|
*/
|
||||||
public interface DifyService {
|
public interface DifyService {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 提取关键词
|
* 提取关键词(返回结构化数据)
|
||||||
|
*/
|
||||||
|
KeywordExtractionResult extractKeywordsStructured(String content);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 提取关键词(返回JSON字符串)
|
||||||
*/
|
*/
|
||||||
String extractKeywords(String content);
|
String extractKeywords(String content);
|
||||||
|
|
||||||
|
|
@ -16,9 +24,14 @@ public interface DifyService {
|
||||||
String performSearch(String keywords);
|
String performSearch(String keywords);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 生成回复
|
* 生成回复(返回JSON字符串)
|
||||||
*/
|
*/
|
||||||
String generateResponse(String inquiryContent, String searchResults);
|
String generateResponse(String inquiryContent, String searchResults);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成结构化回复
|
||||||
|
*/
|
||||||
|
ResponseGenerationDTO generateStructuredResponse(String inquiryContent, String selectedMaterials);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,76 @@
|
||||||
|
package com.ipsen.medical.service;
|
||||||
|
|
||||||
|
import com.ipsen.medical.dto.DrugDTO;
|
||||||
|
import com.ipsen.medical.dto.DrugSafetyInfoDTO;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public interface DrugService {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取所有药物列表
|
||||||
|
*/
|
||||||
|
List<DrugDTO> getAllDrugs();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据ID获取药物详情
|
||||||
|
*/
|
||||||
|
DrugDTO getDrugById(Long id);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据药物编码获取药物详情
|
||||||
|
*/
|
||||||
|
DrugDTO getDrugByCode(String drugCode);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 搜索药物
|
||||||
|
*/
|
||||||
|
List<DrugDTO> searchDrugs(String keyword);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建药物
|
||||||
|
*/
|
||||||
|
DrugDTO createDrug(DrugDTO drugDTO);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新药物
|
||||||
|
*/
|
||||||
|
DrugDTO updateDrug(Long id, DrugDTO drugDTO);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除药物
|
||||||
|
*/
|
||||||
|
void deleteDrug(Long id);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取药物的所有安全信息
|
||||||
|
*/
|
||||||
|
List<DrugSafetyInfoDTO> getDrugSafetyInfos(Long drugId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据来源获取安全信息
|
||||||
|
*/
|
||||||
|
List<DrugSafetyInfoDTO> getDrugSafetyInfosBySource(Long drugId, String source);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加安全信息
|
||||||
|
*/
|
||||||
|
DrugSafetyInfoDTO addSafetyInfo(Long drugId, DrugSafetyInfoDTO safetyInfoDTO);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新安全信息
|
||||||
|
*/
|
||||||
|
DrugSafetyInfoDTO updateSafetyInfo(Long safetyInfoId, DrugSafetyInfoDTO safetyInfoDTO);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除安全信息
|
||||||
|
*/
|
||||||
|
void deleteSafetyInfo(Long safetyInfoId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证安全信息
|
||||||
|
*/
|
||||||
|
DrugSafetyInfoDTO verifySafetyInfo(Long safetyInfoId, String verifiedBy);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
package com.ipsen.medical.service;
|
package com.ipsen.medical.service;
|
||||||
|
|
||||||
|
import com.ipsen.medical.dto.DataSourceSelectionDTO;
|
||||||
import com.ipsen.medical.dto.InquiryRequestDTO;
|
import com.ipsen.medical.dto.InquiryRequestDTO;
|
||||||
|
import com.ipsen.medical.dto.KeywordConfirmationDTO;
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
@ -36,7 +38,17 @@ public interface InquiryService {
|
||||||
InquiryRequestDTO extractKeywords(Long id);
|
InquiryRequestDTO extractKeywords(Long id);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 执行信息检索
|
* 确认和更新关键词
|
||||||
|
*/
|
||||||
|
InquiryRequestDTO confirmKeywords(Long id, KeywordConfirmationDTO keywordDTO);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 选择数据源
|
||||||
|
*/
|
||||||
|
InquiryRequestDTO selectDataSources(Long id, DataSourceSelectionDTO dataSourceDTO);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 执行信息检索(基于选择的数据源)
|
||||||
*/
|
*/
|
||||||
InquiryRequestDTO performSearch(Long id);
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -0,0 +1,209 @@
|
||||||
|
package com.ipsen.medical.service.impl;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import com.ipsen.medical.dto.ClinicalTrialDTO;
|
||||||
|
import com.ipsen.medical.dto.InquiryRequestDTO;
|
||||||
|
import com.ipsen.medical.dto.KeywordExtractionResult;
|
||||||
|
import com.ipsen.medical.entity.InquiryRequest;
|
||||||
|
import com.ipsen.medical.repository.InquiryRequestRepository;
|
||||||
|
import com.ipsen.medical.service.AutoSearchService;
|
||||||
|
import com.ipsen.medical.service.ClinicalTrialsService;
|
||||||
|
import com.ipsen.medical.service.DifyService;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 自动检索服务实现
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class AutoSearchServiceImpl implements AutoSearchService {
|
||||||
|
|
||||||
|
private final InquiryRequestRepository inquiryRequestRepository;
|
||||||
|
private final DifyService difyService;
|
||||||
|
private final ClinicalTrialsService clinicalTrialsService;
|
||||||
|
private final ObjectMapper objectMapper;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional
|
||||||
|
public InquiryRequestDTO performAutoSearch(Long inquiryId) {
|
||||||
|
log.info("Starting auto search for inquiry: {}", inquiryId);
|
||||||
|
|
||||||
|
InquiryRequest inquiry = inquiryRequestRepository.findById(inquiryId)
|
||||||
|
.orElseThrow(() -> new RuntimeException("查询请求不存在: " + inquiryId));
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 1. 解析已提取的关键词
|
||||||
|
KeywordExtractionResult keywords = parseKeywords(inquiry.getKeywords());
|
||||||
|
|
||||||
|
if (keywords == null || keywords.getDrugNameEnglish() == null) {
|
||||||
|
throw new RuntimeException("未找到有效的关键词,请先提取关键词");
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info("Using keywords for search: Chinese={}, English={}",
|
||||||
|
keywords.getDrugNameChinese(), keywords.getDrugNameEnglish());
|
||||||
|
|
||||||
|
// 2. 使用英文药物名称搜索临床试验
|
||||||
|
List<ClinicalTrialDTO> clinicalTrials = null;
|
||||||
|
try {
|
||||||
|
log.info("Searching clinical trials with keyword: {}", keywords.getDrugNameEnglish());
|
||||||
|
clinicalTrials = clinicalTrialsService.searchAndSaveForInquiry(
|
||||||
|
inquiryId,
|
||||||
|
keywords.getDrugNameEnglish()
|
||||||
|
);
|
||||||
|
log.info("Found {} clinical trials", clinicalTrials != null ? clinicalTrials.size() : 0);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Error searching clinical trials: ", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 使用Dify搜索知识库
|
||||||
|
String knowledgeBaseResults = null;
|
||||||
|
try {
|
||||||
|
String searchQuery = buildSearchQuery(keywords);
|
||||||
|
log.info("Searching knowledge base with query: {}", searchQuery);
|
||||||
|
knowledgeBaseResults = difyService.performSearch(searchQuery);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Error searching knowledge base: ", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. 整合检索结果
|
||||||
|
Map<String, Object> searchResults = new HashMap<>();
|
||||||
|
searchResults.put("keywords", keywords);
|
||||||
|
searchResults.put("clinicalTrialsCount", clinicalTrials != null ? clinicalTrials.size() : 0);
|
||||||
|
searchResults.put("knowledgeBaseResults", knowledgeBaseResults);
|
||||||
|
searchResults.put("searchTime", LocalDateTime.now().toString());
|
||||||
|
|
||||||
|
String searchResultsJson = objectMapper.writeValueAsString(searchResults);
|
||||||
|
|
||||||
|
// 5. 更新查询请求
|
||||||
|
inquiry.setSearchResults(searchResultsJson);
|
||||||
|
inquiry.setStatus(InquiryRequest.RequestStatus.SEARCH_COMPLETED);
|
||||||
|
inquiry.setUpdatedAt(LocalDateTime.now());
|
||||||
|
|
||||||
|
InquiryRequest savedInquiry = inquiryRequestRepository.save(inquiry);
|
||||||
|
|
||||||
|
log.info("Auto search completed for inquiry: {}", inquiryId);
|
||||||
|
|
||||||
|
return convertToDTO(savedInquiry);
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Error performing auto search: ", e);
|
||||||
|
throw new RuntimeException("自动检索失败: " + e.getMessage(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional
|
||||||
|
public InquiryRequestDTO executeFullWorkflow(Long inquiryId) {
|
||||||
|
log.info("Executing full AI workflow for inquiry: {}", inquiryId);
|
||||||
|
|
||||||
|
try {
|
||||||
|
InquiryRequest inquiry = inquiryRequestRepository.findById(inquiryId)
|
||||||
|
.orElseThrow(() -> new RuntimeException("查询请求不存在: " + inquiryId));
|
||||||
|
|
||||||
|
// 步骤1: 提取关键词
|
||||||
|
log.info("Step 1: Extracting keywords");
|
||||||
|
KeywordExtractionResult keywords = difyService.extractKeywordsStructured(inquiry.getInquiryContent());
|
||||||
|
String keywordsJson = objectMapper.writeValueAsString(keywords);
|
||||||
|
inquiry.setKeywords(keywordsJson);
|
||||||
|
inquiry.setStatus(InquiryRequest.RequestStatus.KEYWORD_EXTRACTED);
|
||||||
|
inquiry.setUpdatedAt(LocalDateTime.now());
|
||||||
|
inquiryRequestRepository.save(inquiry);
|
||||||
|
|
||||||
|
// 步骤2: 自动检索
|
||||||
|
log.info("Step 2: Performing auto search");
|
||||||
|
performAutoSearch(inquiryId);
|
||||||
|
|
||||||
|
// 步骤3: 生成回复(可选)
|
||||||
|
inquiry = inquiryRequestRepository.findById(inquiryId)
|
||||||
|
.orElseThrow(() -> new RuntimeException("查询请求不存在: " + inquiryId));
|
||||||
|
try {
|
||||||
|
log.info("Step 3: Generating response");
|
||||||
|
String response = difyService.generateResponse(
|
||||||
|
inquiry.getInquiryContent(),
|
||||||
|
inquiry.getSearchResults()
|
||||||
|
);
|
||||||
|
inquiry.setResponseContent(response);
|
||||||
|
inquiry.setStatus(InquiryRequest.RequestStatus.UNDER_REVIEW);
|
||||||
|
inquiry.setUpdatedAt(LocalDateTime.now());
|
||||||
|
inquiryRequestRepository.save(inquiry);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Error generating response: ", e);
|
||||||
|
// 继续执行,不中断流程
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info("Full workflow completed for inquiry: {}", inquiryId);
|
||||||
|
|
||||||
|
return convertToDTO(inquiry);
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Error executing full workflow: ", e);
|
||||||
|
throw new RuntimeException("完整工作流执行失败: " + e.getMessage(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析关键词JSON
|
||||||
|
*/
|
||||||
|
private KeywordExtractionResult parseKeywords(String keywordsJson) {
|
||||||
|
try {
|
||||||
|
if (keywordsJson == null || keywordsJson.isEmpty()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return objectMapper.readValue(keywordsJson, KeywordExtractionResult.class);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Error parsing keywords: ", e);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构建搜索查询
|
||||||
|
*/
|
||||||
|
private String buildSearchQuery(KeywordExtractionResult keywords) {
|
||||||
|
StringBuilder query = new StringBuilder();
|
||||||
|
|
||||||
|
if (keywords.getDrugNameChinese() != null) {
|
||||||
|
query.append(keywords.getDrugNameChinese()).append(" ");
|
||||||
|
}
|
||||||
|
if (keywords.getDrugNameEnglish() != null) {
|
||||||
|
query.append(keywords.getDrugNameEnglish()).append(" ");
|
||||||
|
}
|
||||||
|
if (keywords.getRequestItem() != null) {
|
||||||
|
query.append(keywords.getRequestItem());
|
||||||
|
}
|
||||||
|
|
||||||
|
return query.toString().trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 转换为DTO
|
||||||
|
*/
|
||||||
|
private InquiryRequestDTO convertToDTO(InquiryRequest inquiry) {
|
||||||
|
InquiryRequestDTO dto = new InquiryRequestDTO();
|
||||||
|
dto.setId(inquiry.getId());
|
||||||
|
dto.setRequestNumber(inquiry.getRequestNumber());
|
||||||
|
dto.setCustomerName(inquiry.getCustomerName());
|
||||||
|
dto.setCustomerEmail(inquiry.getCustomerEmail());
|
||||||
|
dto.setCustomerTitle(inquiry.getCustomerTitle());
|
||||||
|
dto.setInquiryContent(inquiry.getInquiryContent());
|
||||||
|
dto.setKeywords(inquiry.getKeywords());
|
||||||
|
dto.setStatus(inquiry.getStatus().name());
|
||||||
|
dto.setSearchResults(inquiry.getSearchResults());
|
||||||
|
dto.setResponseContent(inquiry.getResponseContent());
|
||||||
|
dto.setAssignedTo(inquiry.getAssignedTo());
|
||||||
|
dto.setCreatedAt(inquiry.getCreatedAt());
|
||||||
|
dto.setUpdatedAt(inquiry.getUpdatedAt());
|
||||||
|
dto.setCompletedAt(inquiry.getCompletedAt());
|
||||||
|
return dto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -0,0 +1,442 @@
|
||||||
|
package com.ipsen.medical.service.impl;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import com.ipsen.medical.dto.ClinicalTrialDTO;
|
||||||
|
import com.ipsen.medical.dto.ClinicalTrialsSearchResult;
|
||||||
|
import com.ipsen.medical.entity.ClinicalTrial;
|
||||||
|
import com.ipsen.medical.repository.ClinicalTrialRepository;
|
||||||
|
import com.ipsen.medical.service.ClinicalTrialsService;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
import org.springframework.web.reactive.function.client.WebClient;
|
||||||
|
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.io.OutputStreamWriter;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 临床试验服务实现
|
||||||
|
* 使用 ClinicalTrials.gov API v2
|
||||||
|
* API文档: https://clinicaltrials.gov/data-api/about-api
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class ClinicalTrialsServiceImpl implements ClinicalTrialsService {
|
||||||
|
|
||||||
|
private static final String API_BASE_URL = "https://clinicaltrials.gov/api/v2";
|
||||||
|
private static final Integer DEFAULT_PAGE_SIZE = 50;
|
||||||
|
|
||||||
|
private final ClinicalTrialRepository clinicalTrialRepository;
|
||||||
|
private final ObjectMapper objectMapper;
|
||||||
|
private final WebClient.Builder webClientBuilder;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ClinicalTrialsSearchResult searchClinicalTrials(String keyword, Integer pageSize) {
|
||||||
|
log.info("Searching clinical trials for keyword: {}", keyword);
|
||||||
|
|
||||||
|
final int finalPageSize = (pageSize == null || pageSize <= 0) ? DEFAULT_PAGE_SIZE : pageSize;
|
||||||
|
|
||||||
|
try {
|
||||||
|
WebClient webClient = webClientBuilder.baseUrl(API_BASE_URL).build();
|
||||||
|
|
||||||
|
// 调用 ClinicalTrials.gov API v2
|
||||||
|
// 查询格式: /studies?query.term=KEYWORD&pageSize=N
|
||||||
|
String response = webClient.get()
|
||||||
|
.uri(uriBuilder -> uriBuilder
|
||||||
|
.path("/studies")
|
||||||
|
.queryParam("query.term", keyword)
|
||||||
|
.queryParam("pageSize", finalPageSize)
|
||||||
|
.queryParam("format", "json")
|
||||||
|
.build())
|
||||||
|
.retrieve()
|
||||||
|
.bodyToMono(String.class)
|
||||||
|
.block();
|
||||||
|
|
||||||
|
// 解析响应
|
||||||
|
return parseApiResponse(response, keyword);
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Error searching clinical trials: ", e);
|
||||||
|
throw new RuntimeException("Failed to search clinical trials: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional
|
||||||
|
public List<ClinicalTrialDTO> searchAndSaveForInquiry(Long inquiryId, String keyword) {
|
||||||
|
log.info("Searching and saving clinical trials for inquiry: {}, keyword: {}", inquiryId, keyword);
|
||||||
|
|
||||||
|
// 先删除该查询请求的旧数据
|
||||||
|
clinicalTrialRepository.deleteByInquiryId(inquiryId);
|
||||||
|
|
||||||
|
// 搜索临床试验
|
||||||
|
ClinicalTrialsSearchResult searchResult = searchClinicalTrials(keyword, 100);
|
||||||
|
|
||||||
|
// 保存到数据库
|
||||||
|
List<ClinicalTrial> savedTrials = new ArrayList<>();
|
||||||
|
for (ClinicalTrialDTO dto : searchResult.getStudies()) {
|
||||||
|
ClinicalTrial trial = convertToEntity(dto);
|
||||||
|
trial.setInquiryId(inquiryId);
|
||||||
|
savedTrials.add(clinicalTrialRepository.save(trial));
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info("Saved {} clinical trials for inquiry: {}", savedTrials.size(), inquiryId);
|
||||||
|
|
||||||
|
return savedTrials.stream()
|
||||||
|
.map(this::convertToDTO)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<ClinicalTrialDTO> getClinicalTrialsByInquiry(Long inquiryId) {
|
||||||
|
List<ClinicalTrial> trials = clinicalTrialRepository.findByInquiryId(inquiryId);
|
||||||
|
return trials.stream()
|
||||||
|
.map(this::convertToDTO)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public byte[] exportToCsv(Long inquiryId) {
|
||||||
|
log.info("Exporting clinical trials to CSV for inquiry: {}", inquiryId);
|
||||||
|
|
||||||
|
List<ClinicalTrial> trials = clinicalTrialRepository.findByInquiryId(inquiryId);
|
||||||
|
|
||||||
|
try (ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||||
|
OutputStreamWriter writer = new OutputStreamWriter(baos, StandardCharsets.UTF_8)) {
|
||||||
|
|
||||||
|
// 写入 BOM 以支持 Excel 正确识别 UTF-8
|
||||||
|
baos.write(0xEF);
|
||||||
|
baos.write(0xBB);
|
||||||
|
baos.write(0xBF);
|
||||||
|
|
||||||
|
// 写入CSV头部
|
||||||
|
writer.write("NCT ID,Study Title,Overall Status,Phase,Study Type,Start Date,Completion Date,");
|
||||||
|
writer.write("Enrollment,Conditions,Interventions,Sponsor,Brief Summary,URL\n");
|
||||||
|
|
||||||
|
// 写入数据行
|
||||||
|
for (ClinicalTrial trial : trials) {
|
||||||
|
writer.write(escapeCsv(trial.getNctId()) + ",");
|
||||||
|
writer.write(escapeCsv(trial.getStudyTitle()) + ",");
|
||||||
|
writer.write(escapeCsv(trial.getOverallStatus()) + ",");
|
||||||
|
writer.write(escapeCsv(trial.getPhase()) + ",");
|
||||||
|
writer.write(escapeCsv(trial.getStudyType()) + ",");
|
||||||
|
writer.write(escapeCsv(trial.getStartDate()) + ",");
|
||||||
|
writer.write(escapeCsv(trial.getCompletionDate()) + ",");
|
||||||
|
writer.write(escapeCsv(String.valueOf(trial.getEnrollment())) + ",");
|
||||||
|
writer.write(escapeCsv(trial.getConditions()) + ",");
|
||||||
|
writer.write(escapeCsv(trial.getInterventions()) + ",");
|
||||||
|
writer.write(escapeCsv(trial.getSponsor()) + ",");
|
||||||
|
writer.write(escapeCsv(trial.getBriefSummary()) + ",");
|
||||||
|
writer.write(escapeCsv(trial.getUrl()) + "\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
writer.flush();
|
||||||
|
return baos.toByteArray();
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Error exporting to CSV: ", e);
|
||||||
|
throw new RuntimeException("Failed to export CSV: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional
|
||||||
|
public void deleteClinicalTrials(Long inquiryId) {
|
||||||
|
log.info("Deleting clinical trials for inquiry: {}", inquiryId);
|
||||||
|
clinicalTrialRepository.deleteByInquiryId(inquiryId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析 ClinicalTrials.gov API 响应
|
||||||
|
*/
|
||||||
|
private ClinicalTrialsSearchResult parseApiResponse(String response, String keyword) {
|
||||||
|
try {
|
||||||
|
JsonNode root = objectMapper.readTree(response);
|
||||||
|
|
||||||
|
// 获取总数
|
||||||
|
Integer totalCount = root.path("totalCount").asInt(0);
|
||||||
|
|
||||||
|
// 解析研究列表
|
||||||
|
List<ClinicalTrialDTO> studies = new ArrayList<>();
|
||||||
|
JsonNode studiesNode = root.path("studies");
|
||||||
|
|
||||||
|
if (studiesNode.isArray()) {
|
||||||
|
for (JsonNode studyNode : studiesNode) {
|
||||||
|
JsonNode protocolSection = studyNode.path("protocolSection");
|
||||||
|
ClinicalTrialDTO dto = parseStudy(protocolSection);
|
||||||
|
if (dto != null) {
|
||||||
|
studies.add(dto);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info("Parsed {} studies from API response (total count: {})", studies.size(), totalCount);
|
||||||
|
|
||||||
|
return new ClinicalTrialsSearchResult(keyword, totalCount, studies);
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Error parsing API response: ", e);
|
||||||
|
throw new RuntimeException("Failed to parse API response: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析单个研究
|
||||||
|
*/
|
||||||
|
private ClinicalTrialDTO parseStudy(JsonNode protocolSection) {
|
||||||
|
try {
|
||||||
|
ClinicalTrialDTO dto = new ClinicalTrialDTO();
|
||||||
|
|
||||||
|
// Identification Module
|
||||||
|
JsonNode idModule = protocolSection.path("identificationModule");
|
||||||
|
dto.setNctId(idModule.path("nctId").asText(null));
|
||||||
|
dto.setBriefTitle(idModule.path("briefTitle").asText(null));
|
||||||
|
dto.setOfficialTitle(idModule.path("officialTitle").asText(null));
|
||||||
|
|
||||||
|
// Status Module
|
||||||
|
JsonNode statusModule = protocolSection.path("statusModule");
|
||||||
|
dto.setOverallStatus(statusModule.path("overallStatus").asText(null));
|
||||||
|
dto.setStartDate(statusModule.path("startDateStruct").path("date").asText(null));
|
||||||
|
dto.setCompletionDate(statusModule.path("completionDateStruct").path("date").asText(null));
|
||||||
|
|
||||||
|
// Design Module
|
||||||
|
JsonNode designModule = protocolSection.path("designModule");
|
||||||
|
dto.setStudyType(designModule.path("studyType").asText(null));
|
||||||
|
JsonNode phasesNode = designModule.path("phases");
|
||||||
|
if (phasesNode.isArray() && phasesNode.size() > 0) {
|
||||||
|
dto.setPhase(phasesNode.get(0).asText(null));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enrollment
|
||||||
|
JsonNode enrollmentModule = designModule.path("enrollmentInfo");
|
||||||
|
dto.setEnrollment(enrollmentModule.path("count").asInt(0));
|
||||||
|
|
||||||
|
// Conditions
|
||||||
|
JsonNode conditionsModule = protocolSection.path("conditionsModule");
|
||||||
|
dto.setConditions(parseStringArray(conditionsModule.path("conditions")));
|
||||||
|
|
||||||
|
// Interventions
|
||||||
|
JsonNode armsModule = protocolSection.path("armsInterventionsModule");
|
||||||
|
dto.setInterventions(parseInterventions(armsModule.path("interventions")));
|
||||||
|
|
||||||
|
// Sponsor
|
||||||
|
JsonNode sponsorModule = protocolSection.path("sponsorCollaboratorsModule");
|
||||||
|
JsonNode leadSponsor = sponsorModule.path("leadSponsor");
|
||||||
|
dto.setSponsor(leadSponsor.path("name").asText(null));
|
||||||
|
dto.setCollaborators(parseCollaborators(sponsorModule.path("collaborators")));
|
||||||
|
|
||||||
|
// Description
|
||||||
|
JsonNode descModule = protocolSection.path("descriptionModule");
|
||||||
|
dto.setBriefSummary(descModule.path("briefSummary").asText(null));
|
||||||
|
dto.setDetailedDescription(descModule.path("detailedDescription").asText(null));
|
||||||
|
|
||||||
|
// Outcomes
|
||||||
|
JsonNode outcomesModule = protocolSection.path("outcomesModule");
|
||||||
|
dto.setPrimaryOutcome(parseOutcomes(outcomesModule.path("primaryOutcomes")));
|
||||||
|
dto.setSecondaryOutcome(parseOutcomes(outcomesModule.path("secondaryOutcomes")));
|
||||||
|
|
||||||
|
// Eligibility
|
||||||
|
JsonNode eligibilityModule = protocolSection.path("eligibilityModule");
|
||||||
|
dto.setEligibilityCriteria(eligibilityModule.path("eligibilityCriteria").asText(null));
|
||||||
|
|
||||||
|
// Locations
|
||||||
|
JsonNode locationsModule = protocolSection.path("contactsLocationsModule");
|
||||||
|
dto.setLocations(parseLocations(locationsModule.path("locations")));
|
||||||
|
|
||||||
|
// URL
|
||||||
|
dto.setUrl("https://clinicaltrials.gov/study/" + dto.getNctId());
|
||||||
|
|
||||||
|
dto.setStudyTitle(dto.getBriefTitle() != null ? dto.getBriefTitle() : dto.getOfficialTitle());
|
||||||
|
|
||||||
|
return dto;
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("Error parsing study: ", e);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<String> parseStringArray(JsonNode node) {
|
||||||
|
List<String> result = new ArrayList<>();
|
||||||
|
if (node.isArray()) {
|
||||||
|
for (JsonNode item : node) {
|
||||||
|
result.add(item.asText());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<String> parseInterventions(JsonNode node) {
|
||||||
|
List<String> result = new ArrayList<>();
|
||||||
|
if (node.isArray()) {
|
||||||
|
for (JsonNode item : node) {
|
||||||
|
String name = item.path("name").asText(null);
|
||||||
|
String type = item.path("type").asText(null);
|
||||||
|
if (name != null) {
|
||||||
|
result.add((type != null ? type + ": " : "") + name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<String> parseCollaborators(JsonNode node) {
|
||||||
|
List<String> result = new ArrayList<>();
|
||||||
|
if (node.isArray()) {
|
||||||
|
for (JsonNode item : node) {
|
||||||
|
String name = item.path("name").asText(null);
|
||||||
|
if (name != null) {
|
||||||
|
result.add(name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String parseOutcomes(JsonNode node) {
|
||||||
|
StringBuilder result = new StringBuilder();
|
||||||
|
if (node.isArray()) {
|
||||||
|
for (JsonNode item : node) {
|
||||||
|
String measure = item.path("measure").asText(null);
|
||||||
|
if (measure != null) {
|
||||||
|
if (result.length() > 0) result.append("; ");
|
||||||
|
result.append(measure);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result.length() > 0 ? result.toString() : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<ClinicalTrialDTO.LocationInfo> parseLocations(JsonNode node) {
|
||||||
|
List<ClinicalTrialDTO.LocationInfo> result = new ArrayList<>();
|
||||||
|
if (node.isArray()) {
|
||||||
|
for (JsonNode item : node) {
|
||||||
|
ClinicalTrialDTO.LocationInfo location = new ClinicalTrialDTO.LocationInfo();
|
||||||
|
location.setFacility(item.path("facility").asText(null));
|
||||||
|
location.setCity(item.path("city").asText(null));
|
||||||
|
location.setState(item.path("state").asText(null));
|
||||||
|
location.setCountry(item.path("country").asText(null));
|
||||||
|
location.setStatus(item.path("status").asText(null));
|
||||||
|
result.add(location);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 转换 DTO 到实体
|
||||||
|
*/
|
||||||
|
private ClinicalTrial convertToEntity(ClinicalTrialDTO dto) {
|
||||||
|
ClinicalTrial entity = new ClinicalTrial();
|
||||||
|
entity.setNctId(dto.getNctId());
|
||||||
|
entity.setStudyTitle(dto.getStudyTitle());
|
||||||
|
entity.setBriefTitle(dto.getBriefTitle());
|
||||||
|
entity.setOfficialTitle(dto.getOfficialTitle());
|
||||||
|
entity.setOverallStatus(dto.getOverallStatus());
|
||||||
|
entity.setStartDate(dto.getStartDate());
|
||||||
|
entity.setCompletionDate(dto.getCompletionDate());
|
||||||
|
entity.setStudyType(dto.getStudyType());
|
||||||
|
entity.setPhase(dto.getPhase());
|
||||||
|
entity.setEnrollment(dto.getEnrollment());
|
||||||
|
entity.setSponsor(dto.getSponsor());
|
||||||
|
entity.setBriefSummary(dto.getBriefSummary());
|
||||||
|
entity.setDetailedDescription(dto.getDetailedDescription());
|
||||||
|
entity.setPrimaryOutcome(dto.getPrimaryOutcome());
|
||||||
|
entity.setSecondaryOutcome(dto.getSecondaryOutcome());
|
||||||
|
entity.setEligibilityCriteria(dto.getEligibilityCriteria());
|
||||||
|
entity.setUrl(dto.getUrl());
|
||||||
|
|
||||||
|
// 转换列表为 JSON
|
||||||
|
try {
|
||||||
|
if (dto.getConditions() != null) {
|
||||||
|
entity.setConditions(objectMapper.writeValueAsString(dto.getConditions()));
|
||||||
|
}
|
||||||
|
if (dto.getInterventions() != null) {
|
||||||
|
entity.setInterventions(objectMapper.writeValueAsString(dto.getInterventions()));
|
||||||
|
}
|
||||||
|
if (dto.getCollaborators() != null) {
|
||||||
|
entity.setCollaborators(objectMapper.writeValueAsString(dto.getCollaborators()));
|
||||||
|
}
|
||||||
|
if (dto.getLocations() != null) {
|
||||||
|
entity.setLocations(objectMapper.writeValueAsString(dto.getLocations()));
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Error converting lists to JSON: ", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
return entity;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 转换实体到 DTO
|
||||||
|
*/
|
||||||
|
private ClinicalTrialDTO convertToDTO(ClinicalTrial entity) {
|
||||||
|
ClinicalTrialDTO dto = new ClinicalTrialDTO();
|
||||||
|
dto.setId(entity.getId());
|
||||||
|
dto.setInquiryId(entity.getInquiryId());
|
||||||
|
dto.setNctId(entity.getNctId());
|
||||||
|
dto.setStudyTitle(entity.getStudyTitle());
|
||||||
|
dto.setBriefTitle(entity.getBriefTitle());
|
||||||
|
dto.setOfficialTitle(entity.getOfficialTitle());
|
||||||
|
dto.setOverallStatus(entity.getOverallStatus());
|
||||||
|
dto.setStartDate(entity.getStartDate());
|
||||||
|
dto.setCompletionDate(entity.getCompletionDate());
|
||||||
|
dto.setStudyType(entity.getStudyType());
|
||||||
|
dto.setPhase(entity.getPhase());
|
||||||
|
dto.setEnrollment(entity.getEnrollment());
|
||||||
|
dto.setSponsor(entity.getSponsor());
|
||||||
|
dto.setBriefSummary(entity.getBriefSummary());
|
||||||
|
dto.setDetailedDescription(entity.getDetailedDescription());
|
||||||
|
dto.setPrimaryOutcome(entity.getPrimaryOutcome());
|
||||||
|
dto.setSecondaryOutcome(entity.getSecondaryOutcome());
|
||||||
|
dto.setEligibilityCriteria(entity.getEligibilityCriteria());
|
||||||
|
dto.setUrl(entity.getUrl());
|
||||||
|
dto.setCreatedAt(entity.getCreatedAt());
|
||||||
|
|
||||||
|
// 解析 JSON 为列表
|
||||||
|
try {
|
||||||
|
if (entity.getConditions() != null) {
|
||||||
|
dto.setConditions(objectMapper.readValue(entity.getConditions(),
|
||||||
|
objectMapper.getTypeFactory().constructCollectionType(List.class, String.class)));
|
||||||
|
}
|
||||||
|
if (entity.getInterventions() != null) {
|
||||||
|
dto.setInterventions(objectMapper.readValue(entity.getInterventions(),
|
||||||
|
objectMapper.getTypeFactory().constructCollectionType(List.class, String.class)));
|
||||||
|
}
|
||||||
|
if (entity.getCollaborators() != null) {
|
||||||
|
dto.setCollaborators(objectMapper.readValue(entity.getCollaborators(),
|
||||||
|
objectMapper.getTypeFactory().constructCollectionType(List.class, String.class)));
|
||||||
|
}
|
||||||
|
if (entity.getLocations() != null) {
|
||||||
|
dto.setLocations(objectMapper.readValue(entity.getLocations(),
|
||||||
|
objectMapper.getTypeFactory().constructCollectionType(List.class, ClinicalTrialDTO.LocationInfo.class)));
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Error parsing JSON to lists: ", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
return dto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* CSV 字段转义
|
||||||
|
*/
|
||||||
|
private String escapeCsv(String value) {
|
||||||
|
if (value == null) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
// 如果包含逗号、引号或换行符,需要用引号包裹并转义引号
|
||||||
|
if (value.contains(",") || value.contains("\"") || value.contains("\n")) {
|
||||||
|
return "\"" + value.replace("\"", "\"\"") + "\"";
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -1,10 +1,22 @@
|
||||||
package com.ipsen.medical.service.impl;
|
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.KeywordExtractionResult;
|
||||||
|
import com.ipsen.medical.dto.ResponseGenerationDTO;
|
||||||
import com.ipsen.medical.service.DifyService;
|
import com.ipsen.medical.service.DifyService;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.http.HttpHeaders;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.web.reactive.function.client.WebClient;
|
import org.springframework.web.reactive.function.client.WebClient;
|
||||||
|
import reactor.core.publisher.Mono;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Dify AI服务实现
|
* Dify AI服务实现
|
||||||
|
|
@ -20,61 +32,257 @@ public class DifyServiceImpl implements DifyService {
|
||||||
private String difyApiKey;
|
private String difyApiKey;
|
||||||
|
|
||||||
private final WebClient webClient;
|
private final WebClient webClient;
|
||||||
|
private final ObjectMapper objectMapper;
|
||||||
|
|
||||||
public DifyServiceImpl() {
|
@Autowired
|
||||||
this.webClient = WebClient.builder().build();
|
public DifyServiceImpl(WebClient.Builder webClientBuilder, ObjectMapper objectMapper) {
|
||||||
|
this.webClient = webClientBuilder
|
||||||
|
.codecs(configurer -> configurer.defaultCodecs().maxInMemorySize(10 * 1024 * 1024))
|
||||||
|
.build();
|
||||||
|
this.objectMapper = objectMapper;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
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");
|
||||||
|
|
||||||
|
// 解析响应获取结构化数据
|
||||||
|
JsonNode rootNode = objectMapper.readTree(response);
|
||||||
|
JsonNode answerNode = rootNode.path("answer");
|
||||||
|
|
||||||
|
String answer;
|
||||||
|
if (answerNode.isTextual()) {
|
||||||
|
answer = answerNode.asText();
|
||||||
|
} else {
|
||||||
|
answer = answerNode.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 尝试解析answer字段中的JSON
|
||||||
|
KeywordExtractionResult result;
|
||||||
|
try {
|
||||||
|
// 如果answer是JSON格式
|
||||||
|
JsonNode resultNode = objectMapper.readTree(answer);
|
||||||
|
result = new KeywordExtractionResult();
|
||||||
|
result.setDrugNameChinese(resultNode.path("drugNameChinese").asText(null));
|
||||||
|
result.setDrugNameEnglish(resultNode.path("drugNameEnglish").asText(null));
|
||||||
|
result.setRequestItem(resultNode.path("requestItem").asText(null));
|
||||||
|
|
||||||
|
// 提取置信度
|
||||||
|
if (rootNode.has("metadata")) {
|
||||||
|
JsonNode metadata = rootNode.get("metadata");
|
||||||
|
if (metadata.has("confidence")) {
|
||||||
|
result.setConfidence(metadata.get("confidence").asDouble());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
// 如果answer不是JSON格式,尝试文本解析
|
||||||
|
log.warn("Answer is not JSON format, trying text parsing");
|
||||||
|
result = parseKeywordsFromText(answer);
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info("Extracted keywords: Chinese={}, English={}, Item={}",
|
||||||
|
result.getDrugNameChinese(), result.getDrugNameEnglish(), result.getRequestItem());
|
||||||
|
|
||||||
|
return result;
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Error extracting keywords: ", e);
|
||||||
|
throw new RuntimeException("Failed to extract keywords: " + e.getMessage(), e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String extractKeywords(String content) {
|
public String extractKeywords(String content) {
|
||||||
log.info("Extracting keywords from content");
|
try {
|
||||||
|
KeywordExtractionResult result = extractKeywordsStructured(content);
|
||||||
// TODO: 实际实现需要调用Dify API
|
return objectMapper.writeValueAsString(result);
|
||||||
// 这里提供一个示例实现
|
} catch (Exception e) {
|
||||||
String prompt = "请从以下内容中提取关键词(药物名称、疾病、问题):\n" + content;
|
log.error("Error converting keywords to JSON: ", e);
|
||||||
|
throw new RuntimeException("Failed to convert keywords: " + e.getMessage(), e);
|
||||||
return callDifyAPI(prompt);
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String performSearch(String keywords) {
|
public String performSearch(String keywords) {
|
||||||
log.info("Performing search with keywords: {}", keywords);
|
log.info("Performing search with keywords: {}", keywords);
|
||||||
|
|
||||||
// TODO: 实际实现需要调用Dify API进行知识库检索
|
try {
|
||||||
String prompt = "根据以下关键词检索相关信息:\n" + keywords;
|
return callDifyAPI(keywords, "perform_search");
|
||||||
|
} catch (Exception e) {
|
||||||
return callDifyAPI(prompt);
|
log.error("Error performing search: ", e);
|
||||||
|
throw new RuntimeException("Failed to perform search: " + e.getMessage(), e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String generateResponse(String inquiryContent, String searchResults) {
|
public String generateResponse(String inquiryContent, String searchResults) {
|
||||||
log.info("Generating response");
|
log.info("Generating response");
|
||||||
|
|
||||||
// TODO: 实际实现需要调用Dify API生成回复
|
try {
|
||||||
String prompt = String.format(
|
String combined = String.format(
|
||||||
"根据以下查询内容和检索结果,生成专业的回复:\n查询内容:%s\n检索结果:%s",
|
"查询内容:%s\n\n检索结果:%s",
|
||||||
inquiryContent, searchResults
|
inquiryContent, searchResults
|
||||||
);
|
);
|
||||||
|
return callDifyAPI(combined, "generate_response");
|
||||||
return callDifyAPI(prompt);
|
} catch (Exception e) {
|
||||||
|
log.error("Error generating response: ", e);
|
||||||
|
throw new RuntimeException("Failed to generate response: " + e.getMessage(), e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private String callDifyAPI(String prompt) {
|
@Override
|
||||||
|
public ResponseGenerationDTO generateStructuredResponse(String inquiryContent, String selectedMaterials) {
|
||||||
|
log.info("Generating structured response");
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// TODO: 实际实现Dify API调用
|
String prompt = String.format(
|
||||||
// 这里提供一个示例返回
|
"请基于以下查询问题和检索到的资料生成标准化回复。\n\n" +
|
||||||
log.info("Calling Dify API with prompt length: {}", prompt.length());
|
"查询问题:%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
|
||||||
|
);
|
||||||
|
|
||||||
// 示例返回JSON格式
|
String response = callDifyAPI(prompt, "generate_structured_response");
|
||||||
return "{\"result\": \"示例结果\", \"confidence\": 0.95}";
|
|
||||||
|
// 解析响应
|
||||||
|
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) {
|
} catch (Exception e) {
|
||||||
log.error("Error calling Dify API", e);
|
log.error("Error generating structured response: ", e);
|
||||||
throw new RuntimeException("Failed to call Dify API", 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();
|
||||||
|
request.setQuery(content);
|
||||||
|
request.setResponseMode("blocking");
|
||||||
|
request.setUser("medical-info-system");
|
||||||
|
|
||||||
|
// 添加任务类型到inputs
|
||||||
|
Map<String, Object> inputs = new HashMap<>();
|
||||||
|
inputs.put("task_type", taskType);
|
||||||
|
inputs.put("content", content);
|
||||||
|
request.setInputs(inputs);
|
||||||
|
|
||||||
|
log.debug("Dify API request: {}", objectMapper.writeValueAsString(request));
|
||||||
|
|
||||||
|
// 调用API
|
||||||
|
String response = webClient.post()
|
||||||
|
.uri(difyApiUrl + "/chat-messages")
|
||||||
|
.header(HttpHeaders.AUTHORIZATION, "Bearer " + difyApiKey)
|
||||||
|
.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();
|
||||||
|
|
||||||
|
log.debug("Dify API response: {}", response);
|
||||||
|
|
||||||
|
return response;
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Error calling Dify API: ", e);
|
||||||
|
throw new RuntimeException("Failed to call Dify API: " + e.getMessage(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从文本中解析关键词
|
||||||
|
*/
|
||||||
|
private KeywordExtractionResult parseKeywordsFromText(String text) {
|
||||||
|
KeywordExtractionResult result = new KeywordExtractionResult();
|
||||||
|
|
||||||
|
// 简单的文本解析逻辑
|
||||||
|
String[] lines = text.split("\n");
|
||||||
|
for (String line : lines) {
|
||||||
|
if (line.contains("中文") || line.contains("Chinese")) {
|
||||||
|
result.setDrugNameChinese(extractValue(line));
|
||||||
|
} else if (line.contains("英文") || line.contains("English")) {
|
||||||
|
result.setDrugNameEnglish(extractValue(line));
|
||||||
|
} else if (line.contains("问题") || line.contains("Item") || line.contains("Request")) {
|
||||||
|
result.setRequestItem(extractValue(line));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从行中提取值
|
||||||
|
*/
|
||||||
|
private String extractValue(String line) {
|
||||||
|
int colonIndex = line.indexOf(":");
|
||||||
|
if (colonIndex != -1 && colonIndex < line.length() - 1) {
|
||||||
|
return line.substring(colonIndex + 1).trim();
|
||||||
|
}
|
||||||
|
int equalIndex = line.indexOf("=");
|
||||||
|
if (equalIndex != -1 && equalIndex < line.length() - 1) {
|
||||||
|
return line.substring(equalIndex + 1).trim();
|
||||||
|
}
|
||||||
|
return line.trim();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,262 @@
|
||||||
|
package com.ipsen.medical.service.impl;
|
||||||
|
|
||||||
|
import com.ipsen.medical.dto.DrugDTO;
|
||||||
|
import com.ipsen.medical.dto.DrugSafetyInfoDTO;
|
||||||
|
import com.ipsen.medical.entity.Drug;
|
||||||
|
import com.ipsen.medical.entity.DrugSafetyInfo;
|
||||||
|
import com.ipsen.medical.repository.DrugRepository;
|
||||||
|
import com.ipsen.medical.repository.DrugSafetyInfoRepository;
|
||||||
|
import com.ipsen.medical.service.DrugService;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class DrugServiceImpl implements DrugService {
|
||||||
|
|
||||||
|
private final DrugRepository drugRepository;
|
||||||
|
private final DrugSafetyInfoRepository safetyInfoRepository;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<DrugDTO> getAllDrugs() {
|
||||||
|
return drugRepository.findAll().stream()
|
||||||
|
.map(this::convertToDTO)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public DrugDTO getDrugById(Long id) {
|
||||||
|
Drug drug = drugRepository.findById(id)
|
||||||
|
.orElseThrow(() -> new RuntimeException("Drug not found with id: " + id));
|
||||||
|
DrugDTO dto = convertToDTO(drug);
|
||||||
|
dto.setSafetyInfos(getDrugSafetyInfos(id));
|
||||||
|
return dto;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public DrugDTO getDrugByCode(String drugCode) {
|
||||||
|
Drug drug = drugRepository.findByDrugCode(drugCode)
|
||||||
|
.orElseThrow(() -> new RuntimeException("Drug not found with code: " + drugCode));
|
||||||
|
DrugDTO dto = convertToDTO(drug);
|
||||||
|
dto.setSafetyInfos(getDrugSafetyInfos(drug.getId()));
|
||||||
|
return dto;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<DrugDTO> searchDrugs(String keyword) {
|
||||||
|
return drugRepository.searchDrugs(keyword).stream()
|
||||||
|
.map(this::convertToDTO)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional
|
||||||
|
public DrugDTO createDrug(DrugDTO drugDTO) {
|
||||||
|
Drug drug = convertToEntity(drugDTO);
|
||||||
|
drug = drugRepository.save(drug);
|
||||||
|
return convertToDTO(drug);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional
|
||||||
|
public DrugDTO updateDrug(Long id, DrugDTO drugDTO) {
|
||||||
|
Drug drug = drugRepository.findById(id)
|
||||||
|
.orElseThrow(() -> new RuntimeException("Drug not found with id: " + id));
|
||||||
|
|
||||||
|
updateDrugFromDTO(drug, drugDTO);
|
||||||
|
drug = drugRepository.save(drug);
|
||||||
|
return convertToDTO(drug);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional
|
||||||
|
public void deleteDrug(Long id) {
|
||||||
|
drugRepository.deleteById(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<DrugSafetyInfoDTO> getDrugSafetyInfos(Long drugId) {
|
||||||
|
return safetyInfoRepository.findByDrugId(drugId).stream()
|
||||||
|
.map(this::convertSafetyInfoToDTO)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<DrugSafetyInfoDTO> getDrugSafetyInfosBySource(Long drugId, String source) {
|
||||||
|
DrugSafetyInfo.InfoSource infoSource = DrugSafetyInfo.InfoSource.valueOf(source);
|
||||||
|
return safetyInfoRepository.findByDrugIdAndSource(drugId, infoSource).stream()
|
||||||
|
.map(this::convertSafetyInfoToDTO)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional
|
||||||
|
public DrugSafetyInfoDTO addSafetyInfo(Long drugId, DrugSafetyInfoDTO safetyInfoDTO) {
|
||||||
|
Drug drug = drugRepository.findById(drugId)
|
||||||
|
.orElseThrow(() -> new RuntimeException("Drug not found with id: " + drugId));
|
||||||
|
|
||||||
|
DrugSafetyInfo safetyInfo = convertSafetyInfoToEntity(safetyInfoDTO);
|
||||||
|
safetyInfo.setDrug(drug);
|
||||||
|
safetyInfo = safetyInfoRepository.save(safetyInfo);
|
||||||
|
|
||||||
|
return convertSafetyInfoToDTO(safetyInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional
|
||||||
|
public DrugSafetyInfoDTO updateSafetyInfo(Long safetyInfoId, DrugSafetyInfoDTO safetyInfoDTO) {
|
||||||
|
DrugSafetyInfo safetyInfo = safetyInfoRepository.findById(safetyInfoId)
|
||||||
|
.orElseThrow(() -> new RuntimeException("Safety info not found with id: " + safetyInfoId));
|
||||||
|
|
||||||
|
updateSafetyInfoFromDTO(safetyInfo, safetyInfoDTO);
|
||||||
|
safetyInfo = safetyInfoRepository.save(safetyInfo);
|
||||||
|
|
||||||
|
return convertSafetyInfoToDTO(safetyInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional
|
||||||
|
public void deleteSafetyInfo(Long safetyInfoId) {
|
||||||
|
safetyInfoRepository.deleteById(safetyInfoId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional
|
||||||
|
public DrugSafetyInfoDTO verifySafetyInfo(Long safetyInfoId, String verifiedBy) {
|
||||||
|
DrugSafetyInfo safetyInfo = safetyInfoRepository.findById(safetyInfoId)
|
||||||
|
.orElseThrow(() -> new RuntimeException("Safety info not found with id: " + safetyInfoId));
|
||||||
|
|
||||||
|
safetyInfo.setVerified(true);
|
||||||
|
safetyInfo.setVerifiedBy(verifiedBy);
|
||||||
|
safetyInfo.setVerifiedAt(LocalDateTime.now());
|
||||||
|
safetyInfo = safetyInfoRepository.save(safetyInfo);
|
||||||
|
|
||||||
|
return convertSafetyInfoToDTO(safetyInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Conversion methods
|
||||||
|
private DrugDTO convertToDTO(Drug drug) {
|
||||||
|
DrugDTO dto = new DrugDTO();
|
||||||
|
dto.setId(drug.getId());
|
||||||
|
dto.setDrugCode(drug.getDrugCode());
|
||||||
|
dto.setGenericName(drug.getGenericName());
|
||||||
|
dto.setTradeName(drug.getTradeName());
|
||||||
|
dto.setActiveIngredient(drug.getActiveIngredient());
|
||||||
|
dto.setDescription(drug.getDescription());
|
||||||
|
dto.setManufacturer(drug.getManufacturer());
|
||||||
|
dto.setApprovalNumber(drug.getApprovalNumber());
|
||||||
|
dto.setIndications(drug.getIndications());
|
||||||
|
dto.setDosageAndAdministration(drug.getDosageAndAdministration());
|
||||||
|
dto.setContraindications(drug.getContraindications());
|
||||||
|
dto.setTherapeuticClass(drug.getTherapeuticClass());
|
||||||
|
dto.setAtcCode(drug.getAtcCode());
|
||||||
|
dto.setStatus(drug.getStatus() != null ? drug.getStatus().name() : null);
|
||||||
|
dto.setCreatedAt(drug.getCreatedAt());
|
||||||
|
dto.setUpdatedAt(drug.getUpdatedAt());
|
||||||
|
return dto;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Drug convertToEntity(DrugDTO dto) {
|
||||||
|
Drug drug = new Drug();
|
||||||
|
drug.setDrugCode(dto.getDrugCode());
|
||||||
|
drug.setGenericName(dto.getGenericName());
|
||||||
|
drug.setTradeName(dto.getTradeName());
|
||||||
|
drug.setActiveIngredient(dto.getActiveIngredient());
|
||||||
|
drug.setDescription(dto.getDescription());
|
||||||
|
drug.setManufacturer(dto.getManufacturer());
|
||||||
|
drug.setApprovalNumber(dto.getApprovalNumber());
|
||||||
|
drug.setIndications(dto.getIndications());
|
||||||
|
drug.setDosageAndAdministration(dto.getDosageAndAdministration());
|
||||||
|
drug.setContraindications(dto.getContraindications());
|
||||||
|
drug.setTherapeuticClass(dto.getTherapeuticClass());
|
||||||
|
drug.setAtcCode(dto.getAtcCode());
|
||||||
|
if (dto.getStatus() != null) {
|
||||||
|
drug.setStatus(Drug.DrugStatus.valueOf(dto.getStatus()));
|
||||||
|
}
|
||||||
|
return drug;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateDrugFromDTO(Drug drug, DrugDTO dto) {
|
||||||
|
if (dto.getGenericName() != null) drug.setGenericName(dto.getGenericName());
|
||||||
|
if (dto.getTradeName() != null) drug.setTradeName(dto.getTradeName());
|
||||||
|
if (dto.getActiveIngredient() != null) drug.setActiveIngredient(dto.getActiveIngredient());
|
||||||
|
if (dto.getDescription() != null) drug.setDescription(dto.getDescription());
|
||||||
|
if (dto.getManufacturer() != null) drug.setManufacturer(dto.getManufacturer());
|
||||||
|
if (dto.getApprovalNumber() != null) drug.setApprovalNumber(dto.getApprovalNumber());
|
||||||
|
if (dto.getIndications() != null) drug.setIndications(dto.getIndications());
|
||||||
|
if (dto.getDosageAndAdministration() != null) drug.setDosageAndAdministration(dto.getDosageAndAdministration());
|
||||||
|
if (dto.getContraindications() != null) drug.setContraindications(dto.getContraindications());
|
||||||
|
if (dto.getTherapeuticClass() != null) drug.setTherapeuticClass(dto.getTherapeuticClass());
|
||||||
|
if (dto.getAtcCode() != null) drug.setAtcCode(dto.getAtcCode());
|
||||||
|
if (dto.getStatus() != null) drug.setStatus(Drug.DrugStatus.valueOf(dto.getStatus()));
|
||||||
|
}
|
||||||
|
|
||||||
|
private DrugSafetyInfoDTO convertSafetyInfoToDTO(DrugSafetyInfo safetyInfo) {
|
||||||
|
DrugSafetyInfoDTO dto = new DrugSafetyInfoDTO();
|
||||||
|
dto.setId(safetyInfo.getId());
|
||||||
|
dto.setDrugId(safetyInfo.getDrug().getId());
|
||||||
|
dto.setSource(safetyInfo.getSource() != null ? safetyInfo.getSource().name() : null);
|
||||||
|
dto.setTitle(safetyInfo.getTitle());
|
||||||
|
dto.setContent(safetyInfo.getContent());
|
||||||
|
dto.setCategory(safetyInfo.getCategory() != null ? safetyInfo.getCategory().name() : null);
|
||||||
|
dto.setSeverityLevel(safetyInfo.getSeverityLevel() != null ? safetyInfo.getSeverityLevel().name() : null);
|
||||||
|
dto.setReferenceUrl(safetyInfo.getReferenceUrl());
|
||||||
|
dto.setReferenceDocument(safetyInfo.getReferenceDocument());
|
||||||
|
dto.setReportedBy(safetyInfo.getReportedBy());
|
||||||
|
dto.setReportedAt(safetyInfo.getReportedAt());
|
||||||
|
dto.setAdditionalInfo(safetyInfo.getAdditionalInfo());
|
||||||
|
dto.setVerified(safetyInfo.getVerified());
|
||||||
|
dto.setVerifiedBy(safetyInfo.getVerifiedBy());
|
||||||
|
dto.setVerifiedAt(safetyInfo.getVerifiedAt());
|
||||||
|
dto.setCreatedAt(safetyInfo.getCreatedAt());
|
||||||
|
dto.setUpdatedAt(safetyInfo.getUpdatedAt());
|
||||||
|
return dto;
|
||||||
|
}
|
||||||
|
|
||||||
|
private DrugSafetyInfo convertSafetyInfoToEntity(DrugSafetyInfoDTO dto) {
|
||||||
|
DrugSafetyInfo safetyInfo = new DrugSafetyInfo();
|
||||||
|
if (dto.getSource() != null) {
|
||||||
|
safetyInfo.setSource(DrugSafetyInfo.InfoSource.valueOf(dto.getSource()));
|
||||||
|
}
|
||||||
|
safetyInfo.setTitle(dto.getTitle());
|
||||||
|
safetyInfo.setContent(dto.getContent());
|
||||||
|
if (dto.getCategory() != null) {
|
||||||
|
safetyInfo.setCategory(DrugSafetyInfo.SafetyCategory.valueOf(dto.getCategory()));
|
||||||
|
}
|
||||||
|
if (dto.getSeverityLevel() != null) {
|
||||||
|
safetyInfo.setSeverityLevel(DrugSafetyInfo.SeverityLevel.valueOf(dto.getSeverityLevel()));
|
||||||
|
}
|
||||||
|
safetyInfo.setReferenceUrl(dto.getReferenceUrl());
|
||||||
|
safetyInfo.setReferenceDocument(dto.getReferenceDocument());
|
||||||
|
safetyInfo.setReportedBy(dto.getReportedBy());
|
||||||
|
safetyInfo.setReportedAt(dto.getReportedAt());
|
||||||
|
safetyInfo.setAdditionalInfo(dto.getAdditionalInfo());
|
||||||
|
return safetyInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateSafetyInfoFromDTO(DrugSafetyInfo safetyInfo, DrugSafetyInfoDTO dto) {
|
||||||
|
if (dto.getSource() != null) {
|
||||||
|
safetyInfo.setSource(DrugSafetyInfo.InfoSource.valueOf(dto.getSource()));
|
||||||
|
}
|
||||||
|
if (dto.getTitle() != null) safetyInfo.setTitle(dto.getTitle());
|
||||||
|
if (dto.getContent() != null) safetyInfo.setContent(dto.getContent());
|
||||||
|
if (dto.getCategory() != null) {
|
||||||
|
safetyInfo.setCategory(DrugSafetyInfo.SafetyCategory.valueOf(dto.getCategory()));
|
||||||
|
}
|
||||||
|
if (dto.getSeverityLevel() != null) {
|
||||||
|
safetyInfo.setSeverityLevel(DrugSafetyInfo.SeverityLevel.valueOf(dto.getSeverityLevel()));
|
||||||
|
}
|
||||||
|
if (dto.getReferenceUrl() != null) safetyInfo.setReferenceUrl(dto.getReferenceUrl());
|
||||||
|
if (dto.getReferenceDocument() != null) safetyInfo.setReferenceDocument(dto.getReferenceDocument());
|
||||||
|
if (dto.getReportedBy() != null) safetyInfo.setReportedBy(dto.getReportedBy());
|
||||||
|
if (dto.getReportedAt() != null) safetyInfo.setReportedAt(dto.getReportedAt());
|
||||||
|
if (dto.getAdditionalInfo() != null) safetyInfo.setAdditionalInfo(dto.getAdditionalInfo());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -1,10 +1,13 @@
|
||||||
package com.ipsen.medical.service.impl;
|
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.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.InquiryRequest;
|
||||||
import com.ipsen.medical.entity.User;
|
|
||||||
import com.ipsen.medical.repository.InquiryRequestRepository;
|
import com.ipsen.medical.repository.InquiryRequestRepository;
|
||||||
import com.ipsen.medical.repository.UserRepository;
|
|
||||||
import com.ipsen.medical.service.InquiryService;
|
import com.ipsen.medical.service.InquiryService;
|
||||||
import com.ipsen.medical.service.ExcelParserService;
|
import com.ipsen.medical.service.ExcelParserService;
|
||||||
import com.ipsen.medical.service.DifyService;
|
import com.ipsen.medical.service.DifyService;
|
||||||
|
|
@ -27,15 +30,20 @@ public class InquiryServiceImpl implements InquiryService {
|
||||||
@Autowired
|
@Autowired
|
||||||
private InquiryRequestRepository inquiryRequestRepository;
|
private InquiryRequestRepository inquiryRequestRepository;
|
||||||
|
|
||||||
@Autowired
|
|
||||||
private UserRepository userRepository;
|
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private ExcelParserService excelParserService;
|
private ExcelParserService excelParserService;
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private DifyService difyService;
|
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
|
@Override
|
||||||
public InquiryRequestDTO uploadInquiry(MultipartFile file) {
|
public InquiryRequestDTO uploadInquiry(MultipartFile file) {
|
||||||
try {
|
try {
|
||||||
|
|
@ -115,14 +123,14 @@ public class InquiryServiceImpl implements InquiryService {
|
||||||
.orElseThrow(() -> new RuntimeException("查询请求不存在: " + id));
|
.orElseThrow(() -> new RuntimeException("查询请求不存在: " + id));
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 使用Dify服务执行信息检索
|
// 执行多源检索
|
||||||
String searchResults = difyService.performSearch(inquiryRequest.getKeywords());
|
multiSourceSearchService.performMultiSourceSearch(id);
|
||||||
inquiryRequest.setSearchResults(searchResults);
|
|
||||||
inquiryRequest.setStatus(InquiryRequest.RequestStatus.SEARCH_COMPLETED);
|
// 重新加载更新后的请求
|
||||||
inquiryRequest.setUpdatedAt(LocalDateTime.now());
|
inquiryRequest = inquiryRequestRepository.findById(id)
|
||||||
|
.orElseThrow(() -> new RuntimeException("查询请求不存在: " + id));
|
||||||
InquiryRequest savedRequest = inquiryRequestRepository.save(inquiryRequest);
|
|
||||||
return convertToDTO(savedRequest);
|
return convertToDTO(inquiryRequest);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
throw new RuntimeException("信息检索失败: " + e.getMessage(), e);
|
throw new RuntimeException("信息检索失败: " + e.getMessage(), e);
|
||||||
}
|
}
|
||||||
|
|
@ -134,11 +142,27 @@ public class InquiryServiceImpl implements InquiryService {
|
||||||
.orElseThrow(() -> new RuntimeException("查询请求不存在: " + id));
|
.orElseThrow(() -> new RuntimeException("查询请求不存在: " + id));
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 使用Dify服务生成回复内容
|
// 获取用户选中的检索结果(includeInResponse = true)
|
||||||
String responseContent = difyService.generateResponse(
|
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.getInquiryContent(),
|
||||||
inquiryRequest.getSearchResults()
|
selectedMaterials
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// 将结构化回复转换为JSON字符串保存
|
||||||
|
String responseContent = objectMapper.writeValueAsString(structuredResponse);
|
||||||
|
|
||||||
inquiryRequest.setResponseContent(responseContent);
|
inquiryRequest.setResponseContent(responseContent);
|
||||||
inquiryRequest.setStatus(InquiryRequest.RequestStatus.UNDER_REVIEW);
|
inquiryRequest.setStatus(InquiryRequest.RequestStatus.UNDER_REVIEW);
|
||||||
inquiryRequest.setUpdatedAt(LocalDateTime.now());
|
inquiryRequest.setUpdatedAt(LocalDateTime.now());
|
||||||
|
|
@ -180,6 +204,41 @@ public class InquiryServiceImpl implements InquiryService {
|
||||||
return convertToDTO(savedRequest);
|
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.setCustomerTitle(inquiryRequest.getCustomerTitle());
|
||||||
dto.setInquiryContent(inquiryRequest.getInquiryContent());
|
dto.setInquiryContent(inquiryRequest.getInquiryContent());
|
||||||
dto.setKeywords(inquiryRequest.getKeywords());
|
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.setStatus(inquiryRequest.getStatus().name());
|
||||||
dto.setSearchResults(inquiryRequest.getSearchResults());
|
dto.setSearchResults(inquiryRequest.getSearchResults());
|
||||||
dto.setResponseContent(inquiryRequest.getResponseContent());
|
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配置
|
||||||
dify:
|
dify:
|
||||||
api-url: ${DIFY_API_URL:https://api.dify.ai/v1}
|
api-url: ${DIFY_API_URL:https://api.dify.ai/v1}
|
||||||
api-key: ${DIFY_API_KEY:your-dify-api-key}
|
api-key: ${DIFY_API_KEY:your-dify-api-key-here}
|
||||||
|
|
||||||
# 大模型配置
|
# 大模型配置
|
||||||
llm:
|
llm:
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,57 @@
|
||||||
|
-- 添加临床试验表的迁移脚本
|
||||||
|
-- 执行此脚本将在现有数据库中添加 clinical_trials 表
|
||||||
|
|
||||||
|
USE medical_info_system;
|
||||||
|
|
||||||
|
-- 临床试验表
|
||||||
|
CREATE TABLE IF NOT EXISTS clinical_trials (
|
||||||
|
id BIGINT PRIMARY KEY AUTO_INCREMENT,
|
||||||
|
inquiry_id BIGINT COMMENT '关联的查询请求ID',
|
||||||
|
nct_id VARCHAR(50) NOT NULL COMMENT 'ClinicalTrials.gov唯一标识',
|
||||||
|
study_title TEXT COMMENT '研究标题',
|
||||||
|
brief_title VARCHAR(500) COMMENT '简短标题',
|
||||||
|
official_title TEXT COMMENT '官方标题',
|
||||||
|
overall_status VARCHAR(50) COMMENT '研究状态',
|
||||||
|
start_date VARCHAR(50) COMMENT '开始日期',
|
||||||
|
completion_date VARCHAR(50) COMMENT '完成日期',
|
||||||
|
study_type VARCHAR(50) COMMENT '研究类型',
|
||||||
|
phase VARCHAR(50) COMMENT '研究阶段',
|
||||||
|
enrollment INT COMMENT '入组人数',
|
||||||
|
conditions TEXT COMMENT '适应症(JSON数组)',
|
||||||
|
interventions TEXT COMMENT '干预措施(JSON数组)',
|
||||||
|
sponsor VARCHAR(500) COMMENT '主要研究者/机构',
|
||||||
|
collaborators TEXT COMMENT '合作者(JSON数组)',
|
||||||
|
locations TEXT COMMENT '研究地点(JSON数组)',
|
||||||
|
brief_summary TEXT COMMENT '简要摘要',
|
||||||
|
detailed_description LONGTEXT COMMENT '详细描述',
|
||||||
|
primary_outcome TEXT COMMENT '主要终点',
|
||||||
|
secondary_outcome TEXT COMMENT '次要终点',
|
||||||
|
eligibility_criteria TEXT COMMENT '入选标准',
|
||||||
|
url VARCHAR(500) COMMENT 'ClinicalTrials.gov链接',
|
||||||
|
raw_data LONGTEXT COMMENT '原始API返回数据(JSON格式)',
|
||||||
|
created_at DATETIME NOT NULL,
|
||||||
|
updated_at DATETIME,
|
||||||
|
INDEX idx_inquiry_id (inquiry_id),
|
||||||
|
INDEX idx_nct_id (nct_id),
|
||||||
|
INDEX idx_overall_status (overall_status),
|
||||||
|
INDEX idx_phase (phase),
|
||||||
|
INDEX idx_created_at (created_at),
|
||||||
|
UNIQUE KEY uk_inquiry_nct (inquiry_id, nct_id),
|
||||||
|
FOREIGN KEY (inquiry_id) REFERENCES inquiry_requests(id) ON DELETE CASCADE
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
-- 验证表是否创建成功
|
||||||
|
SELECT
|
||||||
|
TABLE_NAME,
|
||||||
|
TABLE_ROWS,
|
||||||
|
CREATE_TIME
|
||||||
|
FROM
|
||||||
|
information_schema.TABLES
|
||||||
|
WHERE
|
||||||
|
TABLE_SCHEMA = 'medical_info_system'
|
||||||
|
AND TABLE_NAME = 'clinical_trials';
|
||||||
|
|
||||||
|
-- 显示表结构
|
||||||
|
DESCRIBE clinical_trials;
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -0,0 +1,149 @@
|
||||||
|
-- 药物示例数据 - 达菲林 (Diphereline)
|
||||||
|
-- Database: medical_info_system
|
||||||
|
|
||||||
|
USE medical_info_system;
|
||||||
|
|
||||||
|
-- 插入达菲林药物基本信息
|
||||||
|
INSERT INTO drugs (drug_code, generic_name, trade_name, active_ingredient, description,
|
||||||
|
manufacturer, approval_number, indications, dosage_and_administration,
|
||||||
|
contraindications, therapeutic_class, atc_code, status, created_at)
|
||||||
|
VALUES (
|
||||||
|
'DAFE001',
|
||||||
|
'注射用醋酸曲普瑞林',
|
||||||
|
'达菲林',
|
||||||
|
'醋酸曲普瑞林 (Triptorelin Acetate)',
|
||||||
|
'达菲林是一种促性腺激素释放激素(GnRH)激动剂,通过持续刺激垂体,导致性激素的初期升高,随后因受体下调而抑制性激素的分泌。',
|
||||||
|
'益普生制药(Ipsen Pharma)',
|
||||||
|
'国药准字H20140108',
|
||||||
|
'1. 前列腺癌(转移性或局部晚期);2. 子宫内膜异位症;3. 子宫肌瘤;4. 中枢性性早熟(儿童);5. 辅助生殖技术中的降调节',
|
||||||
|
'成人:肌肉注射或皮下注射,3.75mg,每4周一次;或11.25mg,每12周一次。儿童性早熟:根据体重调整剂量,通常3.75mg,每4周一次。',
|
||||||
|
'1. 对曲普瑞林或其他GnRH类似物过敏者;2. 妊娠期和哺乳期妇女(除非用于辅助生殖);3. 骨质疏松症患者需谨慎使用',
|
||||||
|
'抗肿瘤和内分泌治疗药物',
|
||||||
|
'L02AE04',
|
||||||
|
'ACTIVE',
|
||||||
|
NOW()
|
||||||
|
)
|
||||||
|
ON DUPLICATE KEY UPDATE drug_code=drug_code;
|
||||||
|
|
||||||
|
-- 获取刚插入的药物ID(用于后续插入安全信息)
|
||||||
|
SET @drug_id = LAST_INSERT_ID();
|
||||||
|
|
||||||
|
-- 插入内部数据(INTERNAL)
|
||||||
|
INSERT INTO drug_safety_infos (drug_id, source, title, content, category, severity_level,
|
||||||
|
reported_by, reported_at, verified, created_at)
|
||||||
|
VALUES
|
||||||
|
(@drug_id, 'INTERNAL', '达菲林临床应用安全性总结',
|
||||||
|
'根据公司内部研究数据,达菲林在治疗前列腺癌和子宫内膜异位症方面显示出良好的疗效和安全性。长期使用需要监测骨密度变化。建议患者在使用前进行全面的骨质疏松风险评估。',
|
||||||
|
'PRECAUTION', 'MODERATE', '益普生医学部', '2024-01-15 10:00:00', TRUE, NOW()),
|
||||||
|
|
||||||
|
(@drug_id, 'INTERNAL', '儿童性早熟治疗经验',
|
||||||
|
'在儿童性早熟治疗中,达菲林显示出良好的疗效。需要密切监测身高、骨龄、第二性征发育情况。治疗期间应每3-6个月评估一次治疗效果。',
|
||||||
|
'PEDIATRIC_USE', 'MILD', '益普生儿科专家组', '2024-02-20 14:30:00', TRUE, NOW()),
|
||||||
|
|
||||||
|
(@drug_id, 'INTERNAL', '常见不良反应及处理',
|
||||||
|
'最常见的不良反应包括:潮热(40-60%)、头痛(15-20%)、情绪波动(10-15%)、注射部位反应(5-10%)。大多数不良反应为轻到中度,可自行缓解。对于严重潮热,可考虑短期使用激素替代治疗。',
|
||||||
|
'ADVERSE_REACTION', 'MILD', '药物警戒部', '2024-03-10 09:15:00', TRUE, NOW());
|
||||||
|
|
||||||
|
-- 插入文献数据(LITERATURE)
|
||||||
|
INSERT INTO drug_safety_infos (drug_id, source, title, content, category, severity_level,
|
||||||
|
reference_url, reported_at, verified, created_at)
|
||||||
|
VALUES
|
||||||
|
(@drug_id, 'LITERATURE', '曲普瑞林治疗前列腺癌的长期随访研究',
|
||||||
|
'一项为期10年的随访研究显示,曲普瑞林在前列腺癌治疗中具有良好的长期安全性。主要不良反应包括骨质疏松(30%)、心血管事件(5-8%)和代谢综合征(10-15%)。建议定期监测骨密度、血脂和血糖水平。',
|
||||||
|
'LONG_TERM_EFFECT', 'MODERATE', 'https://pubmed.ncbi.nlm.nih.gov/example1', '2023-06-15 00:00:00', TRUE, NOW()),
|
||||||
|
|
||||||
|
(@drug_id, 'LITERATURE', '子宫内膜异位症患者使用GnRH激动剂的安全性评估',
|
||||||
|
'系统综述分析了15项临床试验共2,500例患者的数据。曲普瑞林治疗子宫内膜异位症有效且安全。最常见不良反应为低雌激素症状(潮热、阴道干燥、性欲减退)。建议治疗期限不超过6个月,或联合小剂量雌激素-孕激素回补治疗。',
|
||||||
|
'ADVERSE_REACTION', 'MILD', 'https://pubmed.ncbi.nlm.nih.gov/example2', '2023-09-20 00:00:00', TRUE, NOW()),
|
||||||
|
|
||||||
|
(@drug_id, 'LITERATURE', 'GnRH激动剂导致的症状加重现象',
|
||||||
|
'治疗初期(首次注射后1-2周)可能出现"症状加重"(tumor flare),表现为骨痛加剧、排尿困难恶化等。这是由于初期性激素水平升高所致。前列腺癌患者建议在开始治疗时联合使用抗雄激素药物,以预防症状加重。',
|
||||||
|
'WARNING', 'SEVERE', 'https://pubmed.ncbi.nlm.nih.gov/example3', '2023-11-05 00:00:00', TRUE, NOW()),
|
||||||
|
|
||||||
|
(@drug_id, 'LITERATURE', '儿童性早熟患者身高增长研究',
|
||||||
|
'多中心研究表明,及时使用曲普瑞林治疗中枢性性早熟可以改善最终成年身高。平均身高增加4-7cm。治疗期间需要监测生长速度、骨龄进展和青春期发育情况。',
|
||||||
|
'PEDIATRIC_USE', 'MILD', 'https://pubmed.ncbi.nlm.nih.gov/example4', '2024-01-10 00:00:00', TRUE, NOW());
|
||||||
|
|
||||||
|
-- 插入监管公开信息(REGULATORY)
|
||||||
|
INSERT INTO drug_safety_infos (drug_id, source, title, content, category, severity_level,
|
||||||
|
reference_url, reference_document, reported_at, verified, created_at)
|
||||||
|
VALUES
|
||||||
|
(@drug_id, 'REGULATORY', '国家药监局:GnRH激动剂类药物安全性信息通报',
|
||||||
|
'国家药品监督管理局发布安全性信息通报,提醒医疗机构和患者关注GnRH激动剂类药物的以下风险:1. 骨质疏松和骨折风险;2. 心血管风险(包括心肌梗死、脑卒中);3. 糖尿病和代谢综合征风险;4. 情绪障碍和抑郁风险。建议在使用前进行风险评估,治疗期间密切监测。',
|
||||||
|
'WARNING', 'MODERATE', 'https://www.nmpa.gov.cn/example1', 'NMPA-2023-0156', '2023-08-15 00:00:00', TRUE, NOW()),
|
||||||
|
|
||||||
|
(@drug_id, 'REGULATORY', '药品说明书修订通知 - 增加骨质疏松警示',
|
||||||
|
'根据国家药监局要求,达菲林说明书已更新,在【注意事项】中增加骨质疏松相关警示:长期使用GnRH激动剂会导致骨密度下降。治疗前应评估骨质疏松风险因素,治疗期间建议补充钙剂和维生素D,必要时进行骨密度检查。',
|
||||||
|
'PRECAUTION', 'MODERATE', 'https://www.nmpa.gov.cn/example2', 'NMPA-2024-0023', '2024-02-01 00:00:00', TRUE, NOW()),
|
||||||
|
|
||||||
|
(@drug_id, 'REGULATORY', 'FDA:曲普瑞林的心血管风险评估',
|
||||||
|
'FDA药物安全通讯指出,GnRH激动剂类药物可能增加心血管疾病风险,特别是在已有心血管疾病或危险因素的患者中。建议在开始治疗前评估心血管风险,治疗期间监测血压、血脂和血糖。对于高风险患者,需要权衡获益与风险。',
|
||||||
|
'WARNING', 'SEVERE', 'https://www.fda.gov/example1', 'FDA-2023-D-4567', '2023-10-20 00:00:00', TRUE, NOW()),
|
||||||
|
|
||||||
|
(@drug_id, 'REGULATORY', 'EMA:儿童性早熟治疗建议更新',
|
||||||
|
'欧洲药品管理局发布儿童性早熟治疗指南更新。强调应严格掌握适应症,仅用于真性性早熟。治疗期间需要定期监测:身高和体重、骨龄、性腺激素水平、第二性征发育情况。建议由儿科内分泌专家进行治疗和随访。',
|
||||||
|
'PEDIATRIC_USE', 'MODERATE', 'https://www.ema.europa.eu/example1', 'EMA-2024-0089', '2024-03-15 00:00:00', TRUE, NOW());
|
||||||
|
|
||||||
|
-- 插入自媒体信息(SOCIAL_MEDIA)
|
||||||
|
INSERT INTO drug_safety_infos (drug_id, source, title, content, category, severity_level,
|
||||||
|
reported_by, reported_at, verified, created_at)
|
||||||
|
VALUES
|
||||||
|
(@drug_id, 'SOCIAL_MEDIA', '患者分享:达菲林治疗子宫内膜异位症的体验',
|
||||||
|
'多位患者在健康论坛分享使用达菲林治疗子宫内膜异位症的经历。常见反馈:疼痛症状明显改善(80%以上患者),但潮热、出汗等不适较为普遍(约60%)。部分患者报告情绪波动、失眠等症状。大多数患者表示症状可以耐受,停药后逐渐恢复正常。',
|
||||||
|
'ADVERSE_REACTION', 'MILD', '患者论坛收集', '2024-01-20 00:00:00', FALSE, NOW()),
|
||||||
|
|
||||||
|
(@drug_id, 'SOCIAL_MEDIA', '家长分享:孩子使用达菲林治疗性早熟的经验',
|
||||||
|
'家长群体反馈,儿童使用达菲林治疗性早熟效果良好,第二性征发育得到有效控制。主要关注点包括:注射疼痛(可以通过正确的注射技术缓解)、治疗费用、是否影响最终身高等。医生建议按时注射、定期随访非常重要。',
|
||||||
|
'PEDIATRIC_USE', 'MILD', '患儿家长群', '2024-02-10 00:00:00', FALSE, NOW()),
|
||||||
|
|
||||||
|
(@drug_id, 'SOCIAL_MEDIA', '患者报告:治疗初期症状加重的担忧',
|
||||||
|
'部分前列腺癌患者在社交媒体上分享,开始使用达菲林后1-2周内出现骨痛加剧、排尿更困难等情况,引起担忧。医学专家回应这是正常的"症状加重"现象,是药物起效的表现,通常2-4周后会缓解。强调需要在医生指导下使用,必要时联合其他药物预防。',
|
||||||
|
'ADVERSE_REACTION', 'MODERATE', '医疗社交平台', '2024-03-01 00:00:00', FALSE, NOW()),
|
||||||
|
|
||||||
|
(@drug_id, 'SOCIAL_MEDIA', '长期使用者分享:骨质健康管理经验',
|
||||||
|
'长期使用达菲林的患者分享骨质健康管理经验:坚持补充钙剂和维生素D、进行适量的负重运动(如快走、慢跑)、定期检查骨密度。部分患者在医生建议下使用双膦酸盐类药物预防骨质疏松。强调与医生保持良好沟通、定期复查的重要性。',
|
||||||
|
'PRECAUTION', 'MODERATE', '患者互助社区', '2024-03-25 00:00:00', FALSE, NOW());
|
||||||
|
|
||||||
|
-- 插入临床试验数据(CLINICAL_TRIAL)
|
||||||
|
INSERT INTO drug_safety_infos (drug_id, source, title, content, category, severity_level,
|
||||||
|
reference_url, reported_at, verified, created_at)
|
||||||
|
VALUES
|
||||||
|
(@drug_id, 'CLINICAL_TRIAL', '达菲林治疗前列腺癌III期临床试验安全性结果',
|
||||||
|
'多中心随机对照III期临床试验,纳入560例局部晚期或转移性前列腺癌患者。安全性分析显示:最常见不良反应为潮热(56.7%)、注射部位反应(12.3%)、疲劳(18.9%)、骨痛(15.4%)。严重不良事件发生率为8.2%,主要为骨折(2.1%)和心血管事件(3.5%)。总体安全性良好,与既往研究一致。',
|
||||||
|
'ADVERSE_REACTION', 'MODERATE', 'https://clinicaltrials.gov/ct2/show/NCT00000001', '2023-07-30 00:00:00', TRUE, NOW()),
|
||||||
|
|
||||||
|
(@drug_id, 'CLINICAL_TRIAL', '子宫内膜异位症治疗的安全性和有效性研究',
|
||||||
|
'II期临床试验评估达菲林治疗中重度子宫内膜异位症的安全性。120例患者接受6个月治疗。安全性结果:潮热(65%)、头痛(25%)、情绪改变(18%)、阴道干燥(22%)。骨密度平均下降3.5%,停药后6-12个月内大部分恢复。未发现严重安全性问题。',
|
||||||
|
'ADVERSE_REACTION', 'MILD', 'https://clinicaltrials.gov/ct2/show/NCT00000002', '2023-11-18 00:00:00', TRUE, NOW()),
|
||||||
|
|
||||||
|
(@drug_id, 'CLINICAL_TRIAL', '儿童中枢性性早熟长期治疗随访研究',
|
||||||
|
'前瞻性队列研究,纳入180例中枢性性早熟患儿,接受达菲林治疗平均2.5年。安全性监测显示:生长速度下降(预期效果)、体重增加(35%)、注射部位无菌性脓肿(3例,1.7%)。未发现严重不良反应。骨密度、代谢指标均在正常范围。最终身高较预测身高平均增加5.2cm。',
|
||||||
|
'PEDIATRIC_USE', 'MILD', 'https://clinicaltrials.gov/ct2/show/NCT00000003', '2024-02-28 00:00:00', TRUE, NOW());
|
||||||
|
|
||||||
|
-- 插入药物相互作用信息
|
||||||
|
INSERT INTO drug_safety_infos (drug_id, source, title, content, category, severity_level,
|
||||||
|
reported_by, reported_at, verified, created_at)
|
||||||
|
VALUES
|
||||||
|
(@drug_id, 'INTERNAL', '达菲林的药物相互作用',
|
||||||
|
'达菲林与其他药物的相互作用较少。需注意:1. 与抗雄激素药物联用:前列腺癌治疗初期联合使用,可预防症状加重;2. 与激素替代治疗联用:子宫内膜异位症患者可以考虑"回补"治疗,减轻低雌激素症状;3. 与影响垂体功能的药物:可能影响疗效,需谨慎评估。',
|
||||||
|
'DRUG_INTERACTION', 'MODERATE', '药学部', '2024-03-20 10:30:00', TRUE, NOW());
|
||||||
|
|
||||||
|
-- 插入特殊人群用药信息
|
||||||
|
INSERT INTO drug_safety_infos (drug_id, source, title, content, category, severity_level,
|
||||||
|
reported_by, reported_at, verified, created_at)
|
||||||
|
VALUES
|
||||||
|
(@drug_id, 'INTERNAL', '妊娠期和哺乳期用药',
|
||||||
|
'禁止在妊娠期使用达菲林(用于辅助生殖除外)。动物研究显示对胎儿有不良影响。哺乳期妇女应停止哺乳。育龄期妇女在治疗期间应采取有效的非激素避孕措施。',
|
||||||
|
'PREGNANCY_LACTATION', 'CRITICAL', '妇产科专家组', '2024-01-05 14:00:00', TRUE, NOW()),
|
||||||
|
|
||||||
|
(@drug_id, 'INTERNAL', '老年患者用药注意事项',
|
||||||
|
'老年前列腺癌患者使用达菲林需特别注意:1. 骨质疏松风险更高,建议治疗前评估骨密度;2. 心血管疾病风险增加,需要监测血压、血脂;3. 代谢功能下降,注意血糖监测;4. 认知功能评估,警惕情绪和认知变化。剂量不需调整,但需加强监测。',
|
||||||
|
'GERIATRIC_USE', 'MODERATE', '老年医学科', '2024-02-15 09:45:00', TRUE, NOW()),
|
||||||
|
|
||||||
|
(@drug_id, 'REGULATORY', '肝肾功能不全患者用药',
|
||||||
|
'轻至中度肝肾功能不全患者无需调整剂量。严重肝肾功能不全患者使用经验有限,应谨慎使用并密切监测。目前尚无充分的药代动力学数据支持在严重肝肾功能不全患者中的使用。',
|
||||||
|
'HEPATIC_IMPAIRMENT', 'MODERATE', '临床药理科', '2024-03-10 11:20:00', TRUE, NOW());
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
-- 修复临床试验表的唯一约束
|
||||||
|
-- 将 nct_id 的全局唯一约束改为 (inquiry_id, nct_id) 的组合唯一约束
|
||||||
|
-- 这样允许不同的查询请求可以包含相同的临床试验
|
||||||
|
|
||||||
|
USE medical_info_system;
|
||||||
|
|
||||||
|
-- 删除现有的 nct_id 唯一索引(如果存在)
|
||||||
|
-- 注意:可能需要先检查索引名称
|
||||||
|
ALTER TABLE clinical_trials DROP INDEX nct_id;
|
||||||
|
|
||||||
|
-- 添加新的组合唯一约束
|
||||||
|
ALTER TABLE clinical_trials ADD UNIQUE KEY uk_inquiry_nct (inquiry_id, nct_id);
|
||||||
|
|
||||||
|
-- 验证修改
|
||||||
|
SHOW INDEX FROM clinical_trials;
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -0,0 +1,217 @@
|
||||||
|
-- 药物模块一键初始化脚本
|
||||||
|
-- 包含表结构创建和达菲林示例数据
|
||||||
|
-- Database: medical_info_system
|
||||||
|
|
||||||
|
USE medical_info_system;
|
||||||
|
|
||||||
|
-- ============================================
|
||||||
|
-- 第一部分:创建表结构
|
||||||
|
-- ============================================
|
||||||
|
|
||||||
|
-- 药物表
|
||||||
|
CREATE TABLE IF NOT EXISTS drugs (
|
||||||
|
id BIGINT PRIMARY KEY AUTO_INCREMENT,
|
||||||
|
drug_code VARCHAR(50) NOT NULL UNIQUE,
|
||||||
|
generic_name VARCHAR(200) NOT NULL,
|
||||||
|
trade_name VARCHAR(200),
|
||||||
|
active_ingredient VARCHAR(200),
|
||||||
|
description TEXT,
|
||||||
|
manufacturer VARCHAR(200),
|
||||||
|
approval_number VARCHAR(100),
|
||||||
|
indications TEXT,
|
||||||
|
dosage_and_administration TEXT,
|
||||||
|
contraindications TEXT,
|
||||||
|
therapeutic_class VARCHAR(100),
|
||||||
|
atc_code VARCHAR(20),
|
||||||
|
status VARCHAR(20) NOT NULL DEFAULT 'ACTIVE' COMMENT 'ACTIVE, SUSPENDED, WITHDRAWN',
|
||||||
|
created_at DATETIME NOT NULL,
|
||||||
|
updated_at DATETIME,
|
||||||
|
INDEX idx_drug_code (drug_code),
|
||||||
|
INDEX idx_generic_name (generic_name),
|
||||||
|
INDEX idx_trade_name (trade_name),
|
||||||
|
INDEX idx_therapeutic_class (therapeutic_class),
|
||||||
|
INDEX idx_status (status)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
-- 药物安全信息表
|
||||||
|
CREATE TABLE IF NOT EXISTS drug_safety_infos (
|
||||||
|
id BIGINT PRIMARY KEY AUTO_INCREMENT,
|
||||||
|
drug_id BIGINT NOT NULL,
|
||||||
|
source VARCHAR(30) NOT NULL COMMENT 'INTERNAL, LITERATURE, REGULATORY, SOCIAL_MEDIA, CLINICAL_TRIAL',
|
||||||
|
title VARCHAR(500) NOT NULL,
|
||||||
|
content TEXT,
|
||||||
|
category VARCHAR(50) NOT NULL COMMENT 'ADVERSE_REACTION, DRUG_INTERACTION, CONTRAINDICATION, etc.',
|
||||||
|
severity_level VARCHAR(20) COMMENT 'MILD, MODERATE, SEVERE, CRITICAL',
|
||||||
|
reference_url VARCHAR(500),
|
||||||
|
reference_document VARCHAR(500),
|
||||||
|
reported_by VARCHAR(100),
|
||||||
|
reported_at DATETIME,
|
||||||
|
additional_info TEXT COMMENT 'JSON格式的附加信息',
|
||||||
|
verified BOOLEAN DEFAULT FALSE,
|
||||||
|
verified_by VARCHAR(100),
|
||||||
|
verified_at DATETIME,
|
||||||
|
created_at DATETIME NOT NULL,
|
||||||
|
updated_at DATETIME,
|
||||||
|
INDEX idx_drug_id (drug_id),
|
||||||
|
INDEX idx_source (source),
|
||||||
|
INDEX idx_category (category),
|
||||||
|
INDEX idx_severity_level (severity_level),
|
||||||
|
INDEX idx_verified (verified),
|
||||||
|
FOREIGN KEY (drug_id) REFERENCES drugs(id) ON DELETE CASCADE
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
-- ============================================
|
||||||
|
-- 第二部分:插入达菲林示例数据
|
||||||
|
-- ============================================
|
||||||
|
|
||||||
|
-- 插入达菲林药物基本信息
|
||||||
|
INSERT INTO drugs (drug_code, generic_name, trade_name, active_ingredient, description,
|
||||||
|
manufacturer, approval_number, indications, dosage_and_administration,
|
||||||
|
contraindications, therapeutic_class, atc_code, status, created_at)
|
||||||
|
VALUES (
|
||||||
|
'DAFE001',
|
||||||
|
'注射用醋酸曲普瑞林',
|
||||||
|
'达菲林',
|
||||||
|
'醋酸曲普瑞林 (Triptorelin Acetate)',
|
||||||
|
'达菲林是一种促性腺激素释放激素(GnRH)激动剂,通过持续刺激垂体,导致性激素的初期升高,随后因受体下调而抑制性激素的分泌。',
|
||||||
|
'益普生制药(Ipsen Pharma)',
|
||||||
|
'国药准字H20140108',
|
||||||
|
'1. 前列腺癌(转移性或局部晚期);2. 子宫内膜异位症;3. 子宫肌瘤;4. 中枢性性早熟(儿童);5. 辅助生殖技术中的降调节',
|
||||||
|
'成人:肌肉注射或皮下注射,3.75mg,每4周一次;或11.25mg,每12周一次。儿童性早熟:根据体重调整剂量,通常3.75mg,每4周一次。',
|
||||||
|
'1. 对曲普瑞林或其他GnRH类似物过敏者;2. 妊娠期和哺乳期妇女(除非用于辅助生殖);3. 骨质疏松症患者需谨慎使用',
|
||||||
|
'抗肿瘤和内分泌治疗药物',
|
||||||
|
'L02AE04',
|
||||||
|
'ACTIVE',
|
||||||
|
NOW()
|
||||||
|
)
|
||||||
|
ON DUPLICATE KEY UPDATE drug_code=drug_code;
|
||||||
|
|
||||||
|
-- 获取刚插入的药物ID
|
||||||
|
SET @drug_id = (SELECT id FROM drugs WHERE drug_code = 'DAFE001');
|
||||||
|
|
||||||
|
-- 清除旧的安全信息(如果重复执行)
|
||||||
|
DELETE FROM drug_safety_infos WHERE drug_id = @drug_id;
|
||||||
|
|
||||||
|
-- 插入内部数据(INTERNAL)
|
||||||
|
INSERT INTO drug_safety_infos (drug_id, source, title, content, category, severity_level,
|
||||||
|
reported_by, reported_at, verified, created_at)
|
||||||
|
VALUES
|
||||||
|
(@drug_id, 'INTERNAL', '达菲林临床应用安全性总结',
|
||||||
|
'根据公司内部研究数据,达菲林在治疗前列腺癌和子宫内膜异位症方面显示出良好的疗效和安全性。长期使用需要监测骨密度变化。建议患者在使用前进行全面的骨质疏松风险评估。',
|
||||||
|
'PRECAUTION', 'MODERATE', '益普生医学部', '2024-01-15 10:00:00', TRUE, NOW()),
|
||||||
|
|
||||||
|
(@drug_id, 'INTERNAL', '儿童性早熟治疗经验',
|
||||||
|
'在儿童性早熟治疗中,达菲林显示出良好的疗效。需要密切监测身高、骨龄、第二性征发育情况。治疗期间应每3-6个月评估一次治疗效果。',
|
||||||
|
'PEDIATRIC_USE', 'MILD', '益普生儿科专家组', '2024-02-20 14:30:00', TRUE, NOW()),
|
||||||
|
|
||||||
|
(@drug_id, 'INTERNAL', '常见不良反应及处理',
|
||||||
|
'最常见的不良反应包括:潮热(40-60%)、头痛(15-20%)、情绪波动(10-15%)、注射部位反应(5-10%)。大多数不良反应为轻到中度,可自行缓解。对于严重潮热,可考虑短期使用激素替代治疗。',
|
||||||
|
'ADVERSE_REACTION', 'MILD', '药物警戒部', '2024-03-10 09:15:00', TRUE, NOW()),
|
||||||
|
|
||||||
|
(@drug_id, 'INTERNAL', '达菲林的药物相互作用',
|
||||||
|
'达菲林与其他药物的相互作用较少。需注意:1. 与抗雄激素药物联用:前列腺癌治疗初期联合使用,可预防症状加重;2. 与激素替代治疗联用:子宫内膜异位症患者可以考虑"回补"治疗,减轻低雌激素症状;3. 与影响垂体功能的药物:可能影响疗效,需谨慎评估。',
|
||||||
|
'DRUG_INTERACTION', 'MODERATE', '药学部', '2024-03-20 10:30:00', TRUE, NOW()),
|
||||||
|
|
||||||
|
(@drug_id, 'INTERNAL', '妊娠期和哺乳期用药',
|
||||||
|
'禁止在妊娠期使用达菲林(用于辅助生殖除外)。动物研究显示对胎儿有不良影响。哺乳期妇女应停止哺乳。育龄期妇女在治疗期间应采取有效的非激素避孕措施。',
|
||||||
|
'PREGNANCY_LACTATION', 'CRITICAL', '妇产科专家组', '2024-01-05 14:00:00', TRUE, NOW()),
|
||||||
|
|
||||||
|
(@drug_id, 'INTERNAL', '老年患者用药注意事项',
|
||||||
|
'老年前列腺癌患者使用达菲林需特别注意:1. 骨质疏松风险更高,建议治疗前评估骨密度;2. 心血管疾病风险增加,需要监测血压、血脂;3. 代谢功能下降,注意血糖监测;4. 认知功能评估,警惕情绪和认知变化。剂量不需调整,但需加强监测。',
|
||||||
|
'GERIATRIC_USE', 'MODERATE', '老年医学科', '2024-02-15 09:45:00', TRUE, NOW());
|
||||||
|
|
||||||
|
-- 插入文献数据(LITERATURE)
|
||||||
|
INSERT INTO drug_safety_infos (drug_id, source, title, content, category, severity_level,
|
||||||
|
reference_url, reported_at, verified, created_at)
|
||||||
|
VALUES
|
||||||
|
(@drug_id, 'LITERATURE', '曲普瑞林治疗前列腺癌的长期随访研究',
|
||||||
|
'一项为期10年的随访研究显示,曲普瑞林在前列腺癌治疗中具有良好的长期安全性。主要不良反应包括骨质疏松(30%)、心血管事件(5-8%)和代谢综合征(10-15%)。建议定期监测骨密度、血脂和血糖水平。',
|
||||||
|
'LONG_TERM_EFFECT', 'MODERATE', 'https://pubmed.ncbi.nlm.nih.gov/example1', '2023-06-15 00:00:00', TRUE, NOW()),
|
||||||
|
|
||||||
|
(@drug_id, 'LITERATURE', '子宫内膜异位症患者使用GnRH激动剂的安全性评估',
|
||||||
|
'系统综述分析了15项临床试验共2,500例患者的数据。曲普瑞林治疗子宫内膜异位症有效且安全。最常见不良反应为低雌激素症状(潮热、阴道干燥、性欲减退)。建议治疗期限不超过6个月,或联合小剂量雌激素-孕激素回补治疗。',
|
||||||
|
'ADVERSE_REACTION', 'MILD', 'https://pubmed.ncbi.nlm.nih.gov/example2', '2023-09-20 00:00:00', TRUE, NOW()),
|
||||||
|
|
||||||
|
(@drug_id, 'LITERATURE', 'GnRH激动剂导致的症状加重现象',
|
||||||
|
'治疗初期(首次注射后1-2周)可能出现"症状加重"(tumor flare),表现为骨痛加剧、排尿困难恶化等。这是由于初期性激素水平升高所致。前列腺癌患者建议在开始治疗时联合使用抗雄激素药物,以预防症状加重。',
|
||||||
|
'WARNING', 'SEVERE', 'https://pubmed.ncbi.nlm.nih.gov/example3', '2023-11-05 00:00:00', TRUE, NOW()),
|
||||||
|
|
||||||
|
(@drug_id, 'LITERATURE', '儿童性早熟患者身高增长研究',
|
||||||
|
'多中心研究表明,及时使用曲普瑞林治疗中枢性性早熟可以改善最终成年身高。平均身高增加4-7cm。治疗期间需要监测生长速度、骨龄进展和青春期发育情况。',
|
||||||
|
'PEDIATRIC_USE', 'MILD', 'https://pubmed.ncbi.nlm.nih.gov/example4', '2024-01-10 00:00:00', TRUE, NOW());
|
||||||
|
|
||||||
|
-- 插入监管公开信息(REGULATORY)
|
||||||
|
INSERT INTO drug_safety_infos (drug_id, source, title, content, category, severity_level,
|
||||||
|
reference_url, reference_document, reported_at, verified, created_at)
|
||||||
|
VALUES
|
||||||
|
(@drug_id, 'REGULATORY', '国家药监局:GnRH激动剂类药物安全性信息通报',
|
||||||
|
'国家药品监督管理局发布安全性信息通报,提醒医疗机构和患者关注GnRH激动剂类药物的以下风险:1. 骨质疏松和骨折风险;2. 心血管风险(包括心肌梗死、脑卒中);3. 糖尿病和代谢综合征风险;4. 情绪障碍和抑郁风险。建议在使用前进行风险评估,治疗期间密切监测。',
|
||||||
|
'WARNING', 'MODERATE', 'https://www.nmpa.gov.cn/example1', 'NMPA-2023-0156', '2023-08-15 00:00:00', TRUE, NOW()),
|
||||||
|
|
||||||
|
(@drug_id, 'REGULATORY', '药品说明书修订通知 - 增加骨质疏松警示',
|
||||||
|
'根据国家药监局要求,达菲林说明书已更新,在【注意事项】中增加骨质疏松相关警示:长期使用GnRH激动剂会导致骨密度下降。治疗前应评估骨质疏松风险因素,治疗期间建议补充钙剂和维生素D,必要时进行骨密度检查。',
|
||||||
|
'PRECAUTION', 'MODERATE', 'https://www.nmpa.gov.cn/example2', 'NMPA-2024-0023', '2024-02-01 00:00:00', TRUE, NOW()),
|
||||||
|
|
||||||
|
(@drug_id, 'REGULATORY', 'FDA:曲普瑞林的心血管风险评估',
|
||||||
|
'FDA药物安全通讯指出,GnRH激动剂类药物可能增加心血管疾病风险,特别是在已有心血管疾病或危险因素的患者中。建议在开始治疗前评估心血管风险,治疗期间监测血压、血脂和血糖。对于高风险患者,需要权衡获益与风险。',
|
||||||
|
'WARNING', 'SEVERE', 'https://www.fda.gov/example1', 'FDA-2023-D-4567', '2023-10-20 00:00:00', TRUE, NOW()),
|
||||||
|
|
||||||
|
(@drug_id, 'REGULATORY', 'EMA:儿童性早熟治疗建议更新',
|
||||||
|
'欧洲药品管理局发布儿童性早熟治疗指南更新。强调应严格掌握适应症,仅用于真性性早熟。治疗期间需要定期监测:身高和体重、骨龄、性腺激素水平、第二性征发育情况。建议由儿科内分泌专家进行治疗和随访。',
|
||||||
|
'PEDIATRIC_USE', 'MODERATE', 'https://www.ema.europa.eu/example1', 'EMA-2024-0089', '2024-03-15 00:00:00', TRUE, NOW()),
|
||||||
|
|
||||||
|
(@drug_id, 'REGULATORY', '肝肾功能不全患者用药',
|
||||||
|
'轻至中度肝肾功能不全患者无需调整剂量。严重肝肾功能不全患者使用经验有限,应谨慎使用并密切监测。目前尚无充分的药代动力学数据支持在严重肝肾功能不全患者中的使用。',
|
||||||
|
'HEPATIC_IMPAIRMENT', 'MODERATE', 'https://www.nmpa.gov.cn/example3', 'NMPA-2024-0045', '2024-03-10 11:20:00', TRUE, NOW());
|
||||||
|
|
||||||
|
-- 插入自媒体信息(SOCIAL_MEDIA)
|
||||||
|
INSERT INTO drug_safety_infos (drug_id, source, title, content, category, severity_level,
|
||||||
|
reported_by, reported_at, verified, created_at)
|
||||||
|
VALUES
|
||||||
|
(@drug_id, 'SOCIAL_MEDIA', '患者分享:达菲林治疗子宫内膜异位症的体验',
|
||||||
|
'多位患者在健康论坛分享使用达菲林治疗子宫内膜异位症的经历。常见反馈:疼痛症状明显改善(80%以上患者),但潮热、出汗等不适较为普遍(约60%)。部分患者报告情绪波动、失眠等症状。大多数患者表示症状可以耐受,停药后逐渐恢复正常。',
|
||||||
|
'ADVERSE_REACTION', 'MILD', '患者论坛收集', '2024-01-20 00:00:00', FALSE, NOW()),
|
||||||
|
|
||||||
|
(@drug_id, 'SOCIAL_MEDIA', '家长分享:孩子使用达菲林治疗性早熟的经验',
|
||||||
|
'家长群体反馈,儿童使用达菲林治疗性早熟效果良好,第二性征发育得到有效控制。主要关注点包括:注射疼痛(可以通过正确的注射技术缓解)、治疗费用、是否影响最终身高等。医生建议按时注射、定期随访非常重要。',
|
||||||
|
'PEDIATRIC_USE', 'MILD', '患儿家长群', '2024-02-10 00:00:00', FALSE, NOW()),
|
||||||
|
|
||||||
|
(@drug_id, 'SOCIAL_MEDIA', '患者报告:治疗初期症状加重的担忧',
|
||||||
|
'部分前列腺癌患者在社交媒体上分享,开始使用达菲林后1-2周内出现骨痛加剧、排尿更困难等情况,引起担忧。医学专家回应这是正常的"症状加重"现象,是药物起效的表现,通常2-4周后会缓解。强调需要在医生指导下使用,必要时联合其他药物预防。',
|
||||||
|
'ADVERSE_REACTION', 'MODERATE', '医疗社交平台', '2024-03-01 00:00:00', FALSE, NOW()),
|
||||||
|
|
||||||
|
(@drug_id, 'SOCIAL_MEDIA', '长期使用者分享:骨质健康管理经验',
|
||||||
|
'长期使用达菲林的患者分享骨质健康管理经验:坚持补充钙剂和维生素D、进行适量的负重运动(如快走、慢跑)、定期检查骨密度。部分患者在医生建议下使用双膦酸盐类药物预防骨质疏松。强调与医生保持良好沟通、定期复查的重要性。',
|
||||||
|
'PRECAUTION', 'MODERATE', '患者互助社区', '2024-03-25 00:00:00', FALSE, NOW());
|
||||||
|
|
||||||
|
-- 插入临床试验数据(CLINICAL_TRIAL)
|
||||||
|
INSERT INTO drug_safety_infos (drug_id, source, title, content, category, severity_level,
|
||||||
|
reference_url, reported_at, verified, created_at)
|
||||||
|
VALUES
|
||||||
|
(@drug_id, 'CLINICAL_TRIAL', '达菲林治疗前列腺癌III期临床试验安全性结果',
|
||||||
|
'多中心随机对照III期临床试验,纳入560例局部晚期或转移性前列腺癌患者。安全性分析显示:最常见不良反应为潮热(56.7%)、注射部位反应(12.3%)、疲劳(18.9%)、骨痛(15.4%)。严重不良事件发生率为8.2%,主要为骨折(2.1%)和心血管事件(3.5%)。总体安全性良好,与既往研究一致。',
|
||||||
|
'ADVERSE_REACTION', 'MODERATE', 'https://clinicaltrials.gov/ct2/show/NCT00000001', '2023-07-30 00:00:00', TRUE, NOW()),
|
||||||
|
|
||||||
|
(@drug_id, 'CLINICAL_TRIAL', '子宫内膜异位症治疗的安全性和有效性研究',
|
||||||
|
'II期临床试验评估达菲林治疗中重度子宫内膜异位症的安全性。120例患者接受6个月治疗。安全性结果:潮热(65%)、头痛(25%)、情绪改变(18%)、阴道干燥(22%)。骨密度平均下降3.5%,停药后6-12个月内大部分恢复。未发现严重安全性问题。',
|
||||||
|
'ADVERSE_REACTION', 'MILD', 'https://clinicaltrials.gov/ct2/show/NCT00000002', '2023-11-18 00:00:00', TRUE, NOW()),
|
||||||
|
|
||||||
|
(@drug_id, 'CLINICAL_TRIAL', '儿童中枢性性早熟长期治疗随访研究',
|
||||||
|
'前瞻性队列研究,纳入180例中枢性性早熟患儿,接受达菲林治疗平均2.5年。安全性监测显示:生长速度下降(预期效果)、体重增加(35%)、注射部位无菌性脓肿(3例,1.7%)。未发现严重不良反应。骨密度、代谢指标均在正常范围。最终身高较预测身高平均增加5.2cm。',
|
||||||
|
'PEDIATRIC_USE', 'MILD', 'https://clinicaltrials.gov/ct2/show/NCT00000003', '2024-02-28 00:00:00', TRUE, NOW());
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
|
|
||||||
|
-- ============================================
|
||||||
|
-- 初始化完成提示
|
||||||
|
-- ============================================
|
||||||
|
SELECT '药物模块初始化完成!' AS message,
|
||||||
|
(SELECT COUNT(*) FROM drugs WHERE drug_code = 'DAFE001') AS drug_count,
|
||||||
|
(SELECT COUNT(*) FROM drug_safety_infos WHERE drug_id = @drug_id) AS safety_info_count;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -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;
|
||||||
|
|
||||||
|
|
@ -101,6 +101,95 @@ CREATE TABLE IF NOT EXISTS audit_logs (
|
||||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE SET NULL
|
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE SET NULL
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
-- 药物表
|
||||||
|
CREATE TABLE IF NOT EXISTS drugs (
|
||||||
|
id BIGINT PRIMARY KEY AUTO_INCREMENT,
|
||||||
|
drug_code VARCHAR(50) NOT NULL UNIQUE,
|
||||||
|
generic_name VARCHAR(200) NOT NULL,
|
||||||
|
trade_name VARCHAR(200),
|
||||||
|
active_ingredient VARCHAR(200),
|
||||||
|
description TEXT,
|
||||||
|
manufacturer VARCHAR(200),
|
||||||
|
approval_number VARCHAR(100),
|
||||||
|
indications TEXT,
|
||||||
|
dosage_and_administration TEXT,
|
||||||
|
contraindications TEXT,
|
||||||
|
therapeutic_class VARCHAR(100),
|
||||||
|
atc_code VARCHAR(20),
|
||||||
|
status VARCHAR(20) NOT NULL DEFAULT 'ACTIVE' COMMENT 'ACTIVE, SUSPENDED, WITHDRAWN',
|
||||||
|
created_at DATETIME NOT NULL,
|
||||||
|
updated_at DATETIME,
|
||||||
|
INDEX idx_drug_code (drug_code),
|
||||||
|
INDEX idx_generic_name (generic_name),
|
||||||
|
INDEX idx_trade_name (trade_name),
|
||||||
|
INDEX idx_therapeutic_class (therapeutic_class),
|
||||||
|
INDEX idx_status (status)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
-- 药物安全信息表
|
||||||
|
CREATE TABLE IF NOT EXISTS drug_safety_infos (
|
||||||
|
id BIGINT PRIMARY KEY AUTO_INCREMENT,
|
||||||
|
drug_id BIGINT NOT NULL,
|
||||||
|
source VARCHAR(30) NOT NULL COMMENT 'INTERNAL, LITERATURE, REGULATORY, SOCIAL_MEDIA, CLINICAL_TRIAL',
|
||||||
|
title VARCHAR(500) NOT NULL,
|
||||||
|
content TEXT,
|
||||||
|
category VARCHAR(50) NOT NULL COMMENT 'ADVERSE_REACTION, DRUG_INTERACTION, CONTRAINDICATION, etc.',
|
||||||
|
severity_level VARCHAR(20) COMMENT 'MILD, MODERATE, SEVERE, CRITICAL',
|
||||||
|
reference_url VARCHAR(500),
|
||||||
|
reference_document VARCHAR(500),
|
||||||
|
reported_by VARCHAR(100),
|
||||||
|
reported_at DATETIME,
|
||||||
|
additional_info TEXT COMMENT 'JSON格式的附加信息',
|
||||||
|
verified BOOLEAN DEFAULT FALSE,
|
||||||
|
verified_by VARCHAR(100),
|
||||||
|
verified_at DATETIME,
|
||||||
|
created_at DATETIME NOT NULL,
|
||||||
|
updated_at DATETIME,
|
||||||
|
INDEX idx_drug_id (drug_id),
|
||||||
|
INDEX idx_source (source),
|
||||||
|
INDEX idx_category (category),
|
||||||
|
INDEX idx_severity_level (severity_level),
|
||||||
|
INDEX idx_verified (verified),
|
||||||
|
FOREIGN KEY (drug_id) REFERENCES drugs(id) ON DELETE CASCADE
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
-- 临床试验表
|
||||||
|
CREATE TABLE IF NOT EXISTS clinical_trials (
|
||||||
|
id BIGINT PRIMARY KEY AUTO_INCREMENT,
|
||||||
|
inquiry_id BIGINT COMMENT '关联的查询请求ID',
|
||||||
|
nct_id VARCHAR(50) NOT NULL COMMENT 'ClinicalTrials.gov唯一标识',
|
||||||
|
study_title TEXT COMMENT '研究标题',
|
||||||
|
brief_title VARCHAR(500) COMMENT '简短标题',
|
||||||
|
official_title TEXT COMMENT '官方标题',
|
||||||
|
overall_status VARCHAR(50) COMMENT '研究状态',
|
||||||
|
start_date VARCHAR(50) COMMENT '开始日期',
|
||||||
|
completion_date VARCHAR(50) COMMENT '完成日期',
|
||||||
|
study_type VARCHAR(50) COMMENT '研究类型',
|
||||||
|
phase VARCHAR(50) COMMENT '研究阶段',
|
||||||
|
enrollment INT COMMENT '入组人数',
|
||||||
|
conditions TEXT COMMENT '适应症(JSON数组)',
|
||||||
|
interventions TEXT COMMENT '干预措施(JSON数组)',
|
||||||
|
sponsor VARCHAR(500) COMMENT '主要研究者/机构',
|
||||||
|
collaborators TEXT COMMENT '合作者(JSON数组)',
|
||||||
|
locations TEXT COMMENT '研究地点(JSON数组)',
|
||||||
|
brief_summary TEXT COMMENT '简要摘要',
|
||||||
|
detailed_description LONGTEXT COMMENT '详细描述',
|
||||||
|
primary_outcome TEXT COMMENT '主要终点',
|
||||||
|
secondary_outcome TEXT COMMENT '次要终点',
|
||||||
|
eligibility_criteria TEXT COMMENT '入选标准',
|
||||||
|
url VARCHAR(500) COMMENT 'ClinicalTrials.gov链接',
|
||||||
|
raw_data LONGTEXT COMMENT '原始API返回数据(JSON格式)',
|
||||||
|
created_at DATETIME NOT NULL,
|
||||||
|
updated_at DATETIME,
|
||||||
|
INDEX idx_inquiry_id (inquiry_id),
|
||||||
|
INDEX idx_nct_id (nct_id),
|
||||||
|
INDEX idx_overall_status (overall_status),
|
||||||
|
INDEX idx_phase (phase),
|
||||||
|
INDEX idx_created_at (created_at),
|
||||||
|
UNIQUE KEY uk_inquiry_nct (inquiry_id, nct_id),
|
||||||
|
FOREIGN KEY (inquiry_id) REFERENCES inquiry_requests(id) ON DELETE CASCADE
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
-- 插入默认管理员用户 (密码: admin123, 实际使用时应该加密)
|
-- 插入默认管理员用户 (密码: admin123, 实际使用时应该加密)
|
||||||
INSERT INTO users (username, password, full_name, email, role, enabled, created_at)
|
INSERT INTO users (username, password, full_name, email, role, enabled, created_at)
|
||||||
VALUES ('admin', '$2a$10$rK8WpQYJzxJ8Y5X5YqFvRO5K7K5K7K5K7K5K7K5K7K5K7K5K7K5K7',
|
VALUES ('admin', '$2a$10$rK8WpQYJzxJ8Y5X5YqFvRO5K7K5K7K5K7K5K7K5K7K5K7K5K7K5K7',
|
||||||
|
|
|
||||||
|
|
@ -1,79 +0,0 @@
|
||||||
version: '3.8'
|
|
||||||
|
|
||||||
services:
|
|
||||||
# MySQL数据库
|
|
||||||
mysql:
|
|
||||||
image: mysql:8.0
|
|
||||||
container_name: medical-info-mysql
|
|
||||||
restart: always
|
|
||||||
environment:
|
|
||||||
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD:-root123}
|
|
||||||
MYSQL_DATABASE: medical_info_system
|
|
||||||
MYSQL_USER: ${MYSQL_USER:-medical_user}
|
|
||||||
MYSQL_PASSWORD: ${MYSQL_PASSWORD:-medical_pass}
|
|
||||||
TZ: Asia/Shanghai
|
|
||||||
ports:
|
|
||||||
- "3306:3306"
|
|
||||||
volumes:
|
|
||||||
- mysql-data:/var/lib/mysql
|
|
||||||
- ./database/schema.sql:/docker-entrypoint-initdb.d/1-schema.sql
|
|
||||||
- ./database/sample_data.sql:/docker-entrypoint-initdb.d/2-sample_data.sql
|
|
||||||
command: --default-authentication-plugin=mysql_native_password
|
|
||||||
networks:
|
|
||||||
- medical-info-network
|
|
||||||
|
|
||||||
# 后端Spring Boot应用
|
|
||||||
backend:
|
|
||||||
build:
|
|
||||||
context: ./backend
|
|
||||||
dockerfile: Dockerfile
|
|
||||||
container_name: medical-info-backend
|
|
||||||
restart: always
|
|
||||||
environment:
|
|
||||||
SPRING_PROFILES_ACTIVE: ${SPRING_PROFILES_ACTIVE:-prod}
|
|
||||||
DB_HOST: mysql
|
|
||||||
DB_PORT: 3306
|
|
||||||
DB_NAME: medical_info_system
|
|
||||||
DB_USERNAME: ${MYSQL_USER:-medical_user}
|
|
||||||
DB_PASSWORD: ${MYSQL_PASSWORD:-medical_pass}
|
|
||||||
JWT_SECRET: ${JWT_SECRET:-your-secret-key-change-in-production}
|
|
||||||
DIFY_API_URL: ${DIFY_API_URL:-https://api.dify.ai/v1}
|
|
||||||
DIFY_API_KEY: ${DIFY_API_KEY}
|
|
||||||
LLM_API_URL: ${LLM_API_URL:-https://api.openai.com/v1}
|
|
||||||
LLM_API_KEY: ${LLM_API_KEY}
|
|
||||||
LLM_MODEL: ${LLM_MODEL:-gpt-4}
|
|
||||||
ports:
|
|
||||||
- "8080:8080"
|
|
||||||
volumes:
|
|
||||||
- ./downloads:/app/downloads
|
|
||||||
- ./logs:/app/logs
|
|
||||||
depends_on:
|
|
||||||
- mysql
|
|
||||||
networks:
|
|
||||||
- medical-info-network
|
|
||||||
|
|
||||||
# 前端Vue3应用
|
|
||||||
frontend:
|
|
||||||
build:
|
|
||||||
context: ./frontend
|
|
||||||
dockerfile: Dockerfile
|
|
||||||
container_name: medical-info-frontend
|
|
||||||
restart: always
|
|
||||||
ports:
|
|
||||||
- "80:80"
|
|
||||||
depends_on:
|
|
||||||
- backend
|
|
||||||
networks:
|
|
||||||
- medical-info-network
|
|
||||||
|
|
||||||
volumes:
|
|
||||||
mysql-data:
|
|
||||||
driver: local
|
|
||||||
|
|
||||||
networks:
|
|
||||||
medical-info-network:
|
|
||||||
driver: bridge
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -10,6 +10,7 @@ SPRING_PROFILES_ACTIVE=prod
|
||||||
JWT_SECRET=your-secret-key-change-in-production-environment
|
JWT_SECRET=your-secret-key-change-in-production-environment
|
||||||
|
|
||||||
# Dify配置
|
# Dify配置
|
||||||
|
# 请将下面的API Key替换为您实际的Dify API Key
|
||||||
DIFY_API_URL=https://api.dify.ai/v1
|
DIFY_API_URL=https://api.dify.ai/v1
|
||||||
DIFY_API_KEY=your-dify-api-key-here
|
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";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -0,0 +1,139 @@
|
||||||
|
import request from '@/utils/request'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取所有药物列表
|
||||||
|
*/
|
||||||
|
export function getAllDrugs() {
|
||||||
|
return request({
|
||||||
|
url: '/drugs',
|
||||||
|
method: 'get'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据ID获取药物详情(包含所有安全信息)
|
||||||
|
*/
|
||||||
|
export function getDrugById(id) {
|
||||||
|
return request({
|
||||||
|
url: `/drugs/${id}`,
|
||||||
|
method: 'get'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据药物编码获取药物详情
|
||||||
|
*/
|
||||||
|
export function getDrugByCode(drugCode) {
|
||||||
|
return request({
|
||||||
|
url: `/drugs/code/${drugCode}`,
|
||||||
|
method: 'get'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 搜索药物
|
||||||
|
*/
|
||||||
|
export function searchDrugs(keyword) {
|
||||||
|
return request({
|
||||||
|
url: '/drugs/search',
|
||||||
|
method: 'get',
|
||||||
|
params: { keyword }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建药物
|
||||||
|
*/
|
||||||
|
export function createDrug(data) {
|
||||||
|
return request({
|
||||||
|
url: '/drugs',
|
||||||
|
method: 'post',
|
||||||
|
data
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新药物
|
||||||
|
*/
|
||||||
|
export function updateDrug(id, data) {
|
||||||
|
return request({
|
||||||
|
url: `/drugs/${id}`,
|
||||||
|
method: 'put',
|
||||||
|
data
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除药物
|
||||||
|
*/
|
||||||
|
export function deleteDrug(id) {
|
||||||
|
return request({
|
||||||
|
url: `/drugs/${id}`,
|
||||||
|
method: 'delete'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取药物的所有安全信息
|
||||||
|
*/
|
||||||
|
export function getDrugSafetyInfos(id) {
|
||||||
|
return request({
|
||||||
|
url: `/drugs/${id}/safety-infos`,
|
||||||
|
method: 'get'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据来源获取安全信息
|
||||||
|
*/
|
||||||
|
export function getDrugSafetyInfosBySource(id, source) {
|
||||||
|
return request({
|
||||||
|
url: `/drugs/${id}/safety-infos/source/${source}`,
|
||||||
|
method: 'get'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加安全信息
|
||||||
|
*/
|
||||||
|
export function addSafetyInfo(id, data) {
|
||||||
|
return request({
|
||||||
|
url: `/drugs/${id}/safety-infos`,
|
||||||
|
method: 'post',
|
||||||
|
data
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新安全信息
|
||||||
|
*/
|
||||||
|
export function updateSafetyInfo(safetyInfoId, data) {
|
||||||
|
return request({
|
||||||
|
url: `/drugs/safety-infos/${safetyInfoId}`,
|
||||||
|
method: 'put',
|
||||||
|
data
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除安全信息
|
||||||
|
*/
|
||||||
|
export function deleteSafetyInfo(safetyInfoId) {
|
||||||
|
return request({
|
||||||
|
url: `/drugs/safety-infos/${safetyInfoId}`,
|
||||||
|
method: 'delete'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证安全信息
|
||||||
|
*/
|
||||||
|
export function verifySafetyInfo(safetyInfoId, verifiedBy) {
|
||||||
|
return request({
|
||||||
|
url: `/drugs/safety-infos/${safetyInfoId}/verify`,
|
||||||
|
method: 'post',
|
||||||
|
params: { verifiedBy }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -99,6 +99,89 @@ export function completeInquiry(id) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 搜索临床试验
|
||||||
|
*/
|
||||||
|
export function searchClinicalTrials(id, keyword) {
|
||||||
|
return request({
|
||||||
|
url: `/inquiries/${id}/clinical-trials/search`,
|
||||||
|
method: 'post',
|
||||||
|
params: { keyword }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取临床试验列表
|
||||||
|
*/
|
||||||
|
export function getClinicalTrials(id) {
|
||||||
|
return request({
|
||||||
|
url: `/inquiries/${id}/clinical-trials`,
|
||||||
|
method: 'get'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 导出临床试验为CSV
|
||||||
|
*/
|
||||||
|
export function exportClinicalTrials(id) {
|
||||||
|
return `/inquiries/${id}/clinical-trials/export`
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除临床试验数据
|
||||||
|
*/
|
||||||
|
export function deleteClinicalTrials(id) {
|
||||||
|
return request({
|
||||||
|
url: `/inquiries/${id}/clinical-trials`,
|
||||||
|
method: 'delete'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 基于AI识别的关键词执行自动检索
|
||||||
|
*/
|
||||||
|
export function performAutoSearch(id) {
|
||||||
|
return request({
|
||||||
|
url: `/inquiries/${id}/auto-search`,
|
||||||
|
method: 'post'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 执行完整的AI工作流
|
||||||
|
* 1. 提取关键词
|
||||||
|
* 2. 自动检索(临床试验、知识库)
|
||||||
|
* 3. 生成回复
|
||||||
|
*/
|
||||||
|
export function executeFullWorkflow(id) {
|
||||||
|
return request({
|
||||||
|
url: `/inquiries/${id}/full-workflow`,
|
||||||
|
method: 'post'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 确认和更新关键词
|
||||||
|
*/
|
||||||
|
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'),
|
component: () => import('@/views/inquiry/InquiryDetail.vue'),
|
||||||
meta: { title: '查询详情', icon: 'View' },
|
meta: { title: '查询详情', icon: 'View' },
|
||||||
hidden: true
|
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' }
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
@ -62,6 +96,27 @@ const routes = [
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/drug',
|
||||||
|
component: Layout,
|
||||||
|
redirect: '/drug/list',
|
||||||
|
meta: { title: '药物信息', icon: 'Medicine' },
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: 'list',
|
||||||
|
name: 'DrugList',
|
||||||
|
component: () => import('@/views/drug/DrugList.vue'),
|
||||||
|
meta: { title: '药物列表', icon: 'List' }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'detail/:id',
|
||||||
|
name: 'DrugDetail',
|
||||||
|
component: () => import('@/views/drug/DrugDetail.vue'),
|
||||||
|
meta: { title: '药物详情', icon: 'View' },
|
||||||
|
hidden: true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: '/system',
|
path: '/system',
|
||||||
component: Layout,
|
component: Layout,
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,224 @@
|
||||||
|
<template>
|
||||||
|
<div class="drug-detail-container" v-loading="loading">
|
||||||
|
<!-- 头部操作栏 -->
|
||||||
|
<div class="header-actions">
|
||||||
|
<el-button icon="el-icon-back" @click="goBack">返回列表</el-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 药物基本信息 -->
|
||||||
|
<el-card class="info-card" v-if="drug">
|
||||||
|
<template #header>
|
||||||
|
<div class="card-header">
|
||||||
|
<span class="title">药物基本信息</span>
|
||||||
|
<el-tag :type="getStatusType(drug.status)" size="large">
|
||||||
|
{{ getStatusText(drug.status) }}
|
||||||
|
</el-tag>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<el-descriptions :column="2" border>
|
||||||
|
<el-descriptions-item label="药物编码">{{ drug.drugCode }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="通用名">{{ drug.genericName }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="商品名">{{ drug.tradeName || '-' }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="活性成分">{{ drug.activeIngredient || '-' }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="生产厂家">{{ drug.manufacturer || '-' }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="批准文号">{{ drug.approvalNumber || '-' }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="治疗分类">{{ drug.therapeuticClass || '-' }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="ATC编码">{{ drug.atcCode || '-' }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="药物描述" :span="2">
|
||||||
|
{{ drug.description || '-' }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="适应症" :span="2">
|
||||||
|
<div class="text-content">{{ drug.indications || '-' }}</div>
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="用法用量" :span="2">
|
||||||
|
<div class="text-content">{{ drug.dosageAndAdministration || '-' }}</div>
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="禁忌症" :span="2">
|
||||||
|
<div class="text-content">{{ drug.contraindications || '-' }}</div>
|
||||||
|
</el-descriptions-item>
|
||||||
|
</el-descriptions>
|
||||||
|
</el-card>
|
||||||
|
|
||||||
|
<!-- 安全信息分类标签 -->
|
||||||
|
<el-card class="safety-nav-card" v-if="drug">
|
||||||
|
<template #header>
|
||||||
|
<div class="card-header">
|
||||||
|
<span class="title">安全性信息</span>
|
||||||
|
<span class="subtitle">按信息来源分类展示</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<el-tabs v-model="activeSource" type="border-card">
|
||||||
|
<el-tab-pane label="内部数据" name="INTERNAL">
|
||||||
|
<div class="source-description">
|
||||||
|
<el-icon class="el-icon-office-building" />
|
||||||
|
<span>来自企业内部研究数据和历史回复</span>
|
||||||
|
</div>
|
||||||
|
<safety-info-list :infos="getInfosBySource('INTERNAL')" />
|
||||||
|
</el-tab-pane>
|
||||||
|
|
||||||
|
<el-tab-pane label="文献数据" name="LITERATURE">
|
||||||
|
<div class="source-description">
|
||||||
|
<el-icon class="el-icon-reading" />
|
||||||
|
<span>来自学术期刊、临床研究等公开发表的文献</span>
|
||||||
|
</div>
|
||||||
|
<safety-info-list :infos="getInfosBySource('LITERATURE')" />
|
||||||
|
</el-tab-pane>
|
||||||
|
|
||||||
|
<el-tab-pane label="监管公开信息" name="REGULATORY">
|
||||||
|
<div class="source-description">
|
||||||
|
<el-icon class="el-icon-document-checked" />
|
||||||
|
<span>来自药品监管部门的公开通报和警示信息</span>
|
||||||
|
</div>
|
||||||
|
<safety-info-list :infos="getInfosBySource('REGULATORY')" />
|
||||||
|
</el-tab-pane>
|
||||||
|
|
||||||
|
<el-tab-pane label="自媒体信息" name="SOCIAL_MEDIA">
|
||||||
|
<div class="source-description">
|
||||||
|
<el-icon class="el-icon-chat-line-round" />
|
||||||
|
<span>来自社交媒体、患者论坛等渠道的信息(未经验证)</span>
|
||||||
|
</div>
|
||||||
|
<safety-info-list :infos="getInfosBySource('SOCIAL_MEDIA')" />
|
||||||
|
</el-tab-pane>
|
||||||
|
|
||||||
|
<el-tab-pane label="临床试验" name="CLINICAL_TRIAL">
|
||||||
|
<div class="source-description">
|
||||||
|
<el-icon class="el-icon-data-analysis" />
|
||||||
|
<span>来自临床试验的安全性数据</span>
|
||||||
|
</div>
|
||||||
|
<safety-info-list :infos="getInfosBySource('CLINICAL_TRIAL')" />
|
||||||
|
</el-tab-pane>
|
||||||
|
</el-tabs>
|
||||||
|
</el-card>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { getDrugById } from '@/api/drug'
|
||||||
|
import SafetyInfoList from './components/SafetyInfoList.vue'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'DrugDetail',
|
||||||
|
components: {
|
||||||
|
SafetyInfoList
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
drug: null,
|
||||||
|
safetyInfos: [],
|
||||||
|
loading: false,
|
||||||
|
activeSource: 'INTERNAL'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.loadDrugDetail()
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
async loadDrugDetail() {
|
||||||
|
const id = this.$route.params.id
|
||||||
|
this.loading = true
|
||||||
|
try {
|
||||||
|
const data = await getDrugById(id)
|
||||||
|
this.drug = data
|
||||||
|
this.safetyInfos = this.drug.safetyInfos || []
|
||||||
|
} catch (error) {
|
||||||
|
this.$message.error('加载药物详情失败')
|
||||||
|
console.error(error)
|
||||||
|
} finally {
|
||||||
|
this.loading = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
getInfosBySource(source) {
|
||||||
|
return this.safetyInfos.filter(info => info.source === source)
|
||||||
|
},
|
||||||
|
goBack() {
|
||||||
|
this.$router.push({ name: 'DrugList' })
|
||||||
|
},
|
||||||
|
getStatusType(status) {
|
||||||
|
const statusMap = {
|
||||||
|
ACTIVE: 'success',
|
||||||
|
SUSPENDED: 'warning',
|
||||||
|
WITHDRAWN: 'danger'
|
||||||
|
}
|
||||||
|
return statusMap[status] || 'info'
|
||||||
|
},
|
||||||
|
getStatusText(status) {
|
||||||
|
const textMap = {
|
||||||
|
ACTIVE: '在市',
|
||||||
|
SUSPENDED: '暂停',
|
||||||
|
WITHDRAWN: '撤市'
|
||||||
|
}
|
||||||
|
return textMap[status] || status
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.drug-detail-container {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-actions {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-card {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-header .title {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #303133;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-header .subtitle {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #909399;
|
||||||
|
margin-left: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-content {
|
||||||
|
white-space: pre-wrap;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.safety-nav-card {
|
||||||
|
min-height: 400px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.source-description {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 10px 15px;
|
||||||
|
background-color: #f5f7fa;
|
||||||
|
border-radius: 4px;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
color: #606266;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.source-description i {
|
||||||
|
margin-right: 8px;
|
||||||
|
font-size: 18px;
|
||||||
|
color: #409eff;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-tabs--border-card) {
|
||||||
|
box-shadow: none;
|
||||||
|
border: 1px solid #dcdfe6;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-tab-pane) {
|
||||||
|
padding: 15px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
|
@ -0,0 +1,190 @@
|
||||||
|
<template>
|
||||||
|
<div class="drug-list-container">
|
||||||
|
<el-card class="header-card">
|
||||||
|
<div class="header-content">
|
||||||
|
<h2>药物信息管理</h2>
|
||||||
|
<div class="search-bar">
|
||||||
|
<el-input
|
||||||
|
v-model="searchKeyword"
|
||||||
|
placeholder="搜索药物名称、成分..."
|
||||||
|
prefix-icon="el-icon-search"
|
||||||
|
@keyup.enter="handleSearch"
|
||||||
|
clearable
|
||||||
|
@clear="loadDrugs"
|
||||||
|
/>
|
||||||
|
<el-button type="primary" @click="handleSearch">搜索</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
|
||||||
|
<el-card class="table-card">
|
||||||
|
<el-table
|
||||||
|
:data="drugList"
|
||||||
|
style="width: 100%"
|
||||||
|
v-loading="loading"
|
||||||
|
@row-click="handleRowClick"
|
||||||
|
:row-class-name="tableRowClassName"
|
||||||
|
>
|
||||||
|
<el-table-column prop="drugCode" label="药物编码" width="120" />
|
||||||
|
<el-table-column prop="genericName" label="通用名" width="200" />
|
||||||
|
<el-table-column prop="tradeName" label="商品名" width="150" />
|
||||||
|
<el-table-column prop="activeIngredient" label="活性成分" width="180" />
|
||||||
|
<el-table-column prop="manufacturer" label="生产厂家" width="180" />
|
||||||
|
<el-table-column prop="therapeuticClass" label="治疗分类" width="150" />
|
||||||
|
<el-table-column prop="status" label="状态" width="100">
|
||||||
|
<template #default="scope">
|
||||||
|
<el-tag :type="getStatusType(scope.row.status)">
|
||||||
|
{{ getStatusText(scope.row.status) }}
|
||||||
|
</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="操作" width="200" fixed="right">
|
||||||
|
<template #default="scope">
|
||||||
|
<el-button type="primary" size="small" @click.stop="viewDetail(scope.row.id)">
|
||||||
|
查看详情
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
|
||||||
|
<el-pagination
|
||||||
|
v-if="total > 0"
|
||||||
|
@current-change="handlePageChange"
|
||||||
|
:current-page="currentPage"
|
||||||
|
:page-size="pageSize"
|
||||||
|
layout="total, prev, pager, next"
|
||||||
|
:total="total"
|
||||||
|
class="pagination"
|
||||||
|
/>
|
||||||
|
</el-card>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { getAllDrugs, searchDrugs } from '@/api/drug'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'DrugList',
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
drugList: [],
|
||||||
|
loading: false,
|
||||||
|
searchKeyword: '',
|
||||||
|
currentPage: 1,
|
||||||
|
pageSize: 10,
|
||||||
|
total: 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.loadDrugs()
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
async loadDrugs() {
|
||||||
|
this.loading = true
|
||||||
|
try {
|
||||||
|
const data = await getAllDrugs()
|
||||||
|
this.drugList = data || []
|
||||||
|
this.total = this.drugList.length
|
||||||
|
} catch (error) {
|
||||||
|
this.$message.error('加载药物列表失败')
|
||||||
|
console.error(error)
|
||||||
|
} finally {
|
||||||
|
this.loading = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async handleSearch() {
|
||||||
|
if (!this.searchKeyword.trim()) {
|
||||||
|
this.loadDrugs()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.loading = true
|
||||||
|
try {
|
||||||
|
const data = await searchDrugs(this.searchKeyword)
|
||||||
|
this.drugList = data || []
|
||||||
|
this.total = this.drugList.length
|
||||||
|
this.currentPage = 1
|
||||||
|
} catch (error) {
|
||||||
|
this.$message.error('搜索失败')
|
||||||
|
console.error(error)
|
||||||
|
} finally {
|
||||||
|
this.loading = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handleRowClick(row) {
|
||||||
|
this.viewDetail(row.id)
|
||||||
|
},
|
||||||
|
viewDetail(id) {
|
||||||
|
this.$router.push({ name: 'DrugDetail', params: { id } })
|
||||||
|
},
|
||||||
|
handlePageChange(page) {
|
||||||
|
this.currentPage = page
|
||||||
|
},
|
||||||
|
tableRowClassName() {
|
||||||
|
return 'clickable-row'
|
||||||
|
},
|
||||||
|
getStatusType(status) {
|
||||||
|
const statusMap = {
|
||||||
|
ACTIVE: 'success',
|
||||||
|
SUSPENDED: 'warning',
|
||||||
|
WITHDRAWN: 'danger'
|
||||||
|
}
|
||||||
|
return statusMap[status] || 'info'
|
||||||
|
},
|
||||||
|
getStatusText(status) {
|
||||||
|
const textMap = {
|
||||||
|
ACTIVE: '在市',
|
||||||
|
SUSPENDED: '暂停',
|
||||||
|
WITHDRAWN: '撤市'
|
||||||
|
}
|
||||||
|
return textMap[status] || status
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.drug-list-container {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-card {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-content {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-content h2 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 24px;
|
||||||
|
color: #303133;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-bar {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
width: 400px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-card {
|
||||||
|
min-height: 600px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination {
|
||||||
|
margin-top: 20px;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.clickable-row) {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.clickable-row:hover) {
|
||||||
|
background-color: #f5f7fa;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
|
@ -0,0 +1,266 @@
|
||||||
|
<template>
|
||||||
|
<div class="safety-info-list">
|
||||||
|
<div v-if="infos.length === 0" class="empty-state">
|
||||||
|
<el-empty description="暂无相关安全信息" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else class="info-items">
|
||||||
|
<el-card
|
||||||
|
v-for="info in infos"
|
||||||
|
:key="info.id"
|
||||||
|
class="info-card"
|
||||||
|
:class="getSeverityClass(info.severityLevel)"
|
||||||
|
shadow="hover"
|
||||||
|
>
|
||||||
|
<div class="info-header">
|
||||||
|
<div class="left-section">
|
||||||
|
<h3 class="info-title">{{ info.title }}</h3>
|
||||||
|
<div class="info-meta">
|
||||||
|
<el-tag size="small" :type="getCategoryType(info.category)">
|
||||||
|
{{ getCategoryText(info.category) }}
|
||||||
|
</el-tag>
|
||||||
|
<el-tag
|
||||||
|
v-if="info.severityLevel"
|
||||||
|
size="small"
|
||||||
|
:type="getSeverityType(info.severityLevel)"
|
||||||
|
>
|
||||||
|
{{ getSeverityText(info.severityLevel) }}
|
||||||
|
</el-tag>
|
||||||
|
<el-tag v-if="info.verified" size="small" type="success">
|
||||||
|
<i class="el-icon-circle-check"></i> 已验证
|
||||||
|
</el-tag>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="info-content">
|
||||||
|
{{ info.content }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="info-footer">
|
||||||
|
<div class="footer-left">
|
||||||
|
<span v-if="info.reportedBy" class="meta-item">
|
||||||
|
<i class="el-icon-user"></i>
|
||||||
|
{{ info.reportedBy }}
|
||||||
|
</span>
|
||||||
|
<span v-if="info.reportedAt" class="meta-item">
|
||||||
|
<i class="el-icon-time"></i>
|
||||||
|
{{ formatDate(info.reportedAt) }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="footer-right">
|
||||||
|
<el-button
|
||||||
|
v-if="info.referenceUrl"
|
||||||
|
type="text"
|
||||||
|
icon="el-icon-link"
|
||||||
|
@click="openLink(info.referenceUrl)"
|
||||||
|
>
|
||||||
|
查看来源
|
||||||
|
</el-button>
|
||||||
|
<el-button
|
||||||
|
v-if="info.referenceDocument"
|
||||||
|
type="text"
|
||||||
|
icon="el-icon-document"
|
||||||
|
>
|
||||||
|
{{ info.referenceDocument }}
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="info.verified && info.verifiedBy" class="verification-info">
|
||||||
|
<i class="el-icon-success"></i>
|
||||||
|
由 {{ info.verifiedBy }} 于 {{ formatDate(info.verifiedAt) }} 验证
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'SafetyInfoList',
|
||||||
|
props: {
|
||||||
|
infos: {
|
||||||
|
type: Array,
|
||||||
|
default: () => []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
getCategoryType(category) {
|
||||||
|
const typeMap = {
|
||||||
|
ADVERSE_REACTION: 'danger',
|
||||||
|
DRUG_INTERACTION: 'warning',
|
||||||
|
CONTRAINDICATION: 'danger',
|
||||||
|
WARNING: 'danger',
|
||||||
|
PRECAUTION: 'warning',
|
||||||
|
SPECIAL_POPULATION: 'info',
|
||||||
|
PEDIATRIC_USE: 'info',
|
||||||
|
GERIATRIC_USE: 'info',
|
||||||
|
PREGNANCY_LACTATION: 'warning',
|
||||||
|
LONG_TERM_EFFECT: 'info',
|
||||||
|
HEPATIC_IMPAIRMENT: 'warning',
|
||||||
|
RENAL_IMPAIRMENT: 'warning'
|
||||||
|
}
|
||||||
|
return typeMap[category] || 'info'
|
||||||
|
},
|
||||||
|
getCategoryText(category) {
|
||||||
|
const textMap = {
|
||||||
|
ADVERSE_REACTION: '不良反应',
|
||||||
|
DRUG_INTERACTION: '药物相互作用',
|
||||||
|
CONTRAINDICATION: '禁忌症',
|
||||||
|
SPECIAL_POPULATION: '特殊人群',
|
||||||
|
OVERDOSE: '过量',
|
||||||
|
WITHDRAWAL_SYMPTOM: '停药症状',
|
||||||
|
LONG_TERM_EFFECT: '长期使用',
|
||||||
|
PREGNANCY_LACTATION: '妊娠哺乳',
|
||||||
|
PEDIATRIC_USE: '儿童用药',
|
||||||
|
GERIATRIC_USE: '老年用药',
|
||||||
|
HEPATIC_IMPAIRMENT: '肝功能不全',
|
||||||
|
RENAL_IMPAIRMENT: '肾功能不全',
|
||||||
|
PRECAUTION: '注意事项',
|
||||||
|
WARNING: '警告'
|
||||||
|
}
|
||||||
|
return textMap[category] || category
|
||||||
|
},
|
||||||
|
getSeverityType(level) {
|
||||||
|
const typeMap = {
|
||||||
|
MILD: 'success',
|
||||||
|
MODERATE: 'warning',
|
||||||
|
SEVERE: 'danger',
|
||||||
|
CRITICAL: 'danger'
|
||||||
|
}
|
||||||
|
return typeMap[level] || 'info'
|
||||||
|
},
|
||||||
|
getSeverityText(level) {
|
||||||
|
const textMap = {
|
||||||
|
MILD: '轻度',
|
||||||
|
MODERATE: '中度',
|
||||||
|
SEVERE: '重度',
|
||||||
|
CRITICAL: '严重'
|
||||||
|
}
|
||||||
|
return textMap[level] || level
|
||||||
|
},
|
||||||
|
getSeverityClass(level) {
|
||||||
|
return level ? `severity-${level.toLowerCase()}` : ''
|
||||||
|
},
|
||||||
|
formatDate(dateString) {
|
||||||
|
if (!dateString) return ''
|
||||||
|
const date = new Date(dateString)
|
||||||
|
return date.toLocaleDateString('zh-CN')
|
||||||
|
},
|
||||||
|
openLink(url) {
|
||||||
|
window.open(url, '_blank')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.safety-info-list {
|
||||||
|
min-height: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state {
|
||||||
|
padding: 40px 0;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-items {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-card {
|
||||||
|
transition: all 0.3s;
|
||||||
|
border-left: 4px solid #e4e7ed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-card.severity-mild {
|
||||||
|
border-left-color: #67c23a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-card.severity-moderate {
|
||||||
|
border-left-color: #e6a23c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-card.severity-severe {
|
||||||
|
border-left-color: #f56c6c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-card.severity-critical {
|
||||||
|
border-left-color: #f56c6c;
|
||||||
|
background-color: #fef0f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: flex-start;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-title {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #303133;
|
||||||
|
margin: 0 0 8px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-meta {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-content {
|
||||||
|
color: #606266;
|
||||||
|
line-height: 1.8;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-footer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding-top: 12px;
|
||||||
|
border-top: 1px solid #ebeef5;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #909399;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-left {
|
||||||
|
display: flex;
|
||||||
|
gap: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-right {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.meta-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.verification-info {
|
||||||
|
margin-top: 10px;
|
||||||
|
padding: 8px 12px;
|
||||||
|
background-color: #f0f9ff;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #67c23a;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.verification-info i {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -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>
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -13,7 +13,7 @@
|
||||||
:auto-upload="false"
|
:auto-upload="false"
|
||||||
:on-change="handleFileChange"
|
:on-change="handleFileChange"
|
||||||
:limit="1"
|
:limit="1"
|
||||||
accept=".xlsx,.xls"
|
accept=".xlsx,.xls,.txt,.docx,.doc"
|
||||||
>
|
>
|
||||||
<el-icon class="el-icon--upload"><upload-filled /></el-icon>
|
<el-icon class="el-icon--upload"><upload-filled /></el-icon>
|
||||||
<div class="el-upload__text">
|
<div class="el-upload__text">
|
||||||
|
|
@ -21,7 +21,7 @@
|
||||||
</div>
|
</div>
|
||||||
<template #tip>
|
<template #tip>
|
||||||
<div class="el-upload__tip">
|
<div class="el-upload__tip">
|
||||||
只能上传 xlsx/xls 文件,且不超过10MB
|
请上传客户咨询的原始信息;只能上传 xlsx/xls/docx/doc/txt 文件,且不超过10MB;
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</el-upload>
|
</el-upload>
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,14 @@
|
||||||
<el-descriptions-item label="请求编号">
|
<el-descriptions-item label="请求编号">
|
||||||
{{ inquiry.requestNumber }}
|
{{ inquiry.requestNumber }}
|
||||||
</el-descriptions-item>
|
</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 }}
|
{{ inquiry.customerName }}
|
||||||
</el-descriptions-item>
|
</el-descriptions-item>
|
||||||
<el-descriptions-item label="客户邮箱">
|
<el-descriptions-item label="客户邮箱">
|
||||||
|
|
@ -36,16 +43,61 @@
|
||||||
|
|
||||||
<el-divider />
|
<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">
|
<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'"
|
||||||
|
type="primary"
|
||||||
|
icon="MagicStick"
|
||||||
|
@click="handleFullWorkflow"
|
||||||
|
:loading="processing"
|
||||||
|
>
|
||||||
|
一键执行AI流程
|
||||||
|
</el-button>
|
||||||
|
|
||||||
<el-button
|
<el-button
|
||||||
v-if="inquiry.status === 'PENDING'"
|
v-if="inquiry.status === 'PENDING'"
|
||||||
type="primary"
|
type="primary"
|
||||||
|
|
@ -55,6 +107,16 @@
|
||||||
提取关键词
|
提取关键词
|
||||||
</el-button>
|
</el-button>
|
||||||
|
|
||||||
|
<el-button
|
||||||
|
v-if="inquiry.status === 'KEYWORD_EXTRACTED'"
|
||||||
|
type="success"
|
||||||
|
icon="Search"
|
||||||
|
@click="handleAutoSearch"
|
||||||
|
:loading="processing"
|
||||||
|
>
|
||||||
|
智能自动检索
|
||||||
|
</el-button>
|
||||||
|
|
||||||
<el-button
|
<el-button
|
||||||
v-if="inquiry.status === 'KEYWORD_EXTRACTED'"
|
v-if="inquiry.status === 'KEYWORD_EXTRACTED'"
|
||||||
type="primary"
|
type="primary"
|
||||||
|
|
@ -112,10 +174,25 @@
|
||||||
<el-divider />
|
<el-divider />
|
||||||
|
|
||||||
<div v-if="inquiry.keywords" class="result-section">
|
<div v-if="inquiry.keywords" class="result-section">
|
||||||
<h3>提取的关键词</h3>
|
<h3>AI识别结果</h3>
|
||||||
<el-tag v-for="(keyword, index) in parseKeywords(inquiry.keywords)" :key="index" style="margin-right: 10px;">
|
<div v-if="isStructuredKeywords(inquiry.keywords)" class="keywords-structured">
|
||||||
{{ keyword }}
|
<el-descriptions :column="3" border>
|
||||||
</el-tag>
|
<el-descriptions-item label="药物中文名">
|
||||||
|
<el-tag type="success">{{ getKeywordField(inquiry.keywords, 'drugNameChinese') }}</el-tag>
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="药物英文名">
|
||||||
|
<el-tag type="primary">{{ getKeywordField(inquiry.keywords, 'drugNameEnglish') }}</el-tag>
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="查询项目">
|
||||||
|
<el-tag type="warning">{{ getKeywordField(inquiry.keywords, 'requestItem') || '未指定' }}</el-tag>
|
||||||
|
</el-descriptions-item>
|
||||||
|
</el-descriptions>
|
||||||
|
</div>
|
||||||
|
<div v-else>
|
||||||
|
<el-tag v-for="(keyword, index) in parseKeywords(inquiry.keywords)" :key="index" style="margin-right: 10px;">
|
||||||
|
{{ keyword }}
|
||||||
|
</el-tag>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="inquiry.searchResults" class="result-section">
|
<div v-if="inquiry.searchResults" class="result-section">
|
||||||
|
|
@ -130,6 +207,156 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<el-divider />
|
||||||
|
|
||||||
|
<!-- 临床试验部分 -->
|
||||||
|
<div class="result-section">
|
||||||
|
<div class="section-header">
|
||||||
|
<h3>临床试验信息 (ClinicalTrials.gov)</h3>
|
||||||
|
<div class="section-actions">
|
||||||
|
<el-input
|
||||||
|
v-model="clinicalTrialKeyword"
|
||||||
|
placeholder="输入药品名称(英文)"
|
||||||
|
style="width: 300px; margin-right: 10px;"
|
||||||
|
@keyup.enter="handleSearchClinicalTrials"
|
||||||
|
/>
|
||||||
|
<el-button
|
||||||
|
type="primary"
|
||||||
|
@click="handleSearchClinicalTrials"
|
||||||
|
:loading="searchingClinicalTrials"
|
||||||
|
:disabled="!clinicalTrialKeyword"
|
||||||
|
>
|
||||||
|
搜索临床试验
|
||||||
|
</el-button>
|
||||||
|
<el-button
|
||||||
|
v-if="clinicalTrials.length > 0"
|
||||||
|
type="success"
|
||||||
|
@click="handleExportClinicalTrials"
|
||||||
|
icon="Download"
|
||||||
|
>
|
||||||
|
导出CSV
|
||||||
|
</el-button>
|
||||||
|
<el-button
|
||||||
|
v-if="clinicalTrials.length > 0"
|
||||||
|
@click="loadClinicalTrials"
|
||||||
|
icon="Refresh"
|
||||||
|
>
|
||||||
|
刷新
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="clinicalTrials.length > 0" style="margin-top: 20px;">
|
||||||
|
<el-alert
|
||||||
|
:title="`共找到 ${clinicalTrials.length} 个临床试验`"
|
||||||
|
type="info"
|
||||||
|
:closable="false"
|
||||||
|
style="margin-bottom: 15px;"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<el-table :data="clinicalTrials" border stripe style="width: 100%">
|
||||||
|
<el-table-column type="expand">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<div style="padding: 20px;">
|
||||||
|
<el-descriptions :column="2" border>
|
||||||
|
<el-descriptions-item label="NCT ID">
|
||||||
|
<a :href="row.url" target="_blank" style="color: #409EFF;">
|
||||||
|
{{ row.nctId }}
|
||||||
|
</a>
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="研究状态">
|
||||||
|
<el-tag :type="getStatusTagType(row.overallStatus)">
|
||||||
|
{{ row.overallStatus }}
|
||||||
|
</el-tag>
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="研究类型">
|
||||||
|
{{ row.studyType }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="研究阶段">
|
||||||
|
{{ row.phase || 'N/A' }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="开始日期">
|
||||||
|
{{ row.startDate || 'N/A' }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="完成日期">
|
||||||
|
{{ row.completionDate || 'N/A' }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="入组人数">
|
||||||
|
{{ row.enrollment || 'N/A' }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="主要研究者">
|
||||||
|
{{ row.sponsor }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="适应症" :span="2">
|
||||||
|
<el-tag
|
||||||
|
v-for="(condition, index) in row.conditions"
|
||||||
|
:key="index"
|
||||||
|
style="margin-right: 5px; margin-bottom: 5px;"
|
||||||
|
>
|
||||||
|
{{ condition }}
|
||||||
|
</el-tag>
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="干预措施" :span="2">
|
||||||
|
<el-tag
|
||||||
|
v-for="(intervention, index) in row.interventions"
|
||||||
|
:key="index"
|
||||||
|
type="success"
|
||||||
|
style="margin-right: 5px; margin-bottom: 5px;"
|
||||||
|
>
|
||||||
|
{{ intervention }}
|
||||||
|
</el-tag>
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="研究摘要" :span="2">
|
||||||
|
<div style="white-space: pre-wrap; max-height: 200px; overflow-y: auto;">
|
||||||
|
{{ row.briefSummary || 'N/A' }}
|
||||||
|
</div>
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="研究地点" :span="2">
|
||||||
|
<div v-if="row.locations && row.locations.length > 0">
|
||||||
|
<div
|
||||||
|
v-for="(location, index) in row.locations.slice(0, 5)"
|
||||||
|
:key="index"
|
||||||
|
style="margin-bottom: 5px;"
|
||||||
|
>
|
||||||
|
{{ location.facility }} - {{ location.city }}, {{ location.country }}
|
||||||
|
</div>
|
||||||
|
<div v-if="row.locations.length > 5" style="color: #909399;">
|
||||||
|
... 和其他 {{ row.locations.length - 5 }} 个地点
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<span v-else>N/A</span>
|
||||||
|
</el-descriptions-item>
|
||||||
|
</el-descriptions>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="nctId" label="NCT ID" width="130">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<a :href="row.url" target="_blank" style="color: #409EFF;">
|
||||||
|
{{ row.nctId }}
|
||||||
|
</a>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="studyTitle" label="研究标题" min-width="300" show-overflow-tooltip />
|
||||||
|
<el-table-column prop="overallStatus" label="状态" width="120">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tag :type="getStatusTagType(row.overallStatus)" size="small">
|
||||||
|
{{ row.overallStatus }}
|
||||||
|
</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="phase" label="阶段" width="100" />
|
||||||
|
<el-table-column prop="enrollment" label="入组人数" width="100" />
|
||||||
|
<el-table-column prop="sponsor" label="主要研究者" min-width="200" show-overflow-tooltip />
|
||||||
|
</el-table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-empty
|
||||||
|
v-else-if="!searchingClinicalTrials"
|
||||||
|
description="暂无临床试验数据,请输入药品名称进行搜索"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="result-section">
|
<div class="result-section">
|
||||||
<el-button @click="goBack">返回列表</el-button>
|
<el-button @click="goBack">返回列表</el-button>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -146,7 +373,12 @@ import {
|
||||||
performSearch,
|
performSearch,
|
||||||
generateResponse,
|
generateResponse,
|
||||||
reviewResponse,
|
reviewResponse,
|
||||||
completeInquiry
|
completeInquiry,
|
||||||
|
searchClinicalTrials,
|
||||||
|
getClinicalTrials,
|
||||||
|
exportClinicalTrials,
|
||||||
|
performAutoSearch,
|
||||||
|
executeFullWorkflow
|
||||||
} from '@/api/inquiry'
|
} from '@/api/inquiry'
|
||||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
|
|
||||||
|
|
@ -156,6 +388,9 @@ const route = useRoute()
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const processing = ref(false)
|
const processing = ref(false)
|
||||||
const inquiry = ref({})
|
const inquiry = ref({})
|
||||||
|
const clinicalTrials = ref([])
|
||||||
|
const clinicalTrialKeyword = ref('')
|
||||||
|
const searchingClinicalTrials = ref(false)
|
||||||
|
|
||||||
const currentStep = computed(() => {
|
const currentStep = computed(() => {
|
||||||
const stepMap = {
|
const stepMap = {
|
||||||
|
|
@ -172,6 +407,18 @@ const currentStep = computed(() => {
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
loadDetail()
|
loadDetail()
|
||||||
|
loadClinicalTrials()
|
||||||
|
// 自动填充关键词作为搜索词
|
||||||
|
if (inquiry.value.keywords) {
|
||||||
|
try {
|
||||||
|
const keywords = JSON.parse(inquiry.value.keywords)
|
||||||
|
if (Array.isArray(keywords) && keywords.length > 0) {
|
||||||
|
clinicalTrialKeyword.value = keywords[0]
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// 忽略解析错误
|
||||||
|
}
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const loadDetail = async () => {
|
const loadDetail = async () => {
|
||||||
|
|
@ -186,6 +433,15 @@ const loadDetail = async () => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const loadClinicalTrials = async () => {
|
||||||
|
try {
|
||||||
|
const id = route.params.id
|
||||||
|
clinicalTrials.value = await getClinicalTrials(id)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载临床试验数据失败', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const handleExtractKeywords = async () => {
|
const handleExtractKeywords = async () => {
|
||||||
processing.value = true
|
processing.value = true
|
||||||
try {
|
try {
|
||||||
|
|
@ -321,6 +577,108 @@ const formatJSON = (jsonStr) => {
|
||||||
return jsonStr
|
return jsonStr
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleSearchClinicalTrials = async () => {
|
||||||
|
if (!clinicalTrialKeyword.value.trim()) {
|
||||||
|
ElMessage.warning('请输入药品名称')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
searchingClinicalTrials.value = true
|
||||||
|
try {
|
||||||
|
await searchClinicalTrials(inquiry.value.id, clinicalTrialKeyword.value.trim())
|
||||||
|
ElMessage.success('临床试验搜索完成')
|
||||||
|
await loadClinicalTrials()
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error('搜索失败: ' + (error.message || '未知错误'))
|
||||||
|
} finally {
|
||||||
|
searchingClinicalTrials.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleExportClinicalTrials = () => {
|
||||||
|
const exportUrl = exportClinicalTrials(inquiry.value.id)
|
||||||
|
// 使用后端API的完整URL
|
||||||
|
const baseURL = import.meta.env.VITE_API_BASE_URL || 'http://localhost:8080'
|
||||||
|
window.open(baseURL + exportUrl, '_blank')
|
||||||
|
ElMessage.success('开始下载CSV文件')
|
||||||
|
}
|
||||||
|
|
||||||
|
const getStatusTagType = (status) => {
|
||||||
|
const statusMap = {
|
||||||
|
'COMPLETED': 'success',
|
||||||
|
'ACTIVE': 'primary',
|
||||||
|
'RECRUITING': 'warning',
|
||||||
|
'NOT_YET_RECRUITING': 'info',
|
||||||
|
'TERMINATED': 'danger',
|
||||||
|
'WITHDRAWN': 'danger',
|
||||||
|
'SUSPENDED': 'warning',
|
||||||
|
'ENROLLING_BY_INVITATION': 'warning'
|
||||||
|
}
|
||||||
|
return statusMap[status?.toUpperCase().replace(/ /g, '_')] || 'info'
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleFullWorkflow = async () => {
|
||||||
|
processing.value = true
|
||||||
|
try {
|
||||||
|
await executeFullWorkflow(inquiry.value.id)
|
||||||
|
ElMessage.success('AI智能流程执行完成!正在加载结果...')
|
||||||
|
await loadDetail()
|
||||||
|
await loadClinicalTrials()
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error('执行失败: ' + (error.message || '未知错误'))
|
||||||
|
} finally {
|
||||||
|
processing.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleAutoSearch = async () => {
|
||||||
|
processing.value = true
|
||||||
|
try {
|
||||||
|
await performAutoSearch(inquiry.value.id)
|
||||||
|
ElMessage.success('智能自动检索完成!')
|
||||||
|
await loadDetail()
|
||||||
|
await loadClinicalTrials()
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error('检索失败: ' + (error.message || '未知错误'))
|
||||||
|
} finally {
|
||||||
|
processing.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const isStructuredKeywords = (keywords) => {
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(keywords)
|
||||||
|
return parsed.drugNameChinese || parsed.drugNameEnglish || parsed.requestItem
|
||||||
|
} catch {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const getKeywordField = (keywords, field) => {
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(keywords)
|
||||||
|
return parsed[field] || ''
|
||||||
|
} catch {
|
||||||
|
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>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|
@ -358,6 +716,22 @@ const formatJSON = (jsonStr) => {
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.section-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-header h3 {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -1,97 +0,0 @@
|
||||||
@echo off
|
|
||||||
chcp 65001 >nul
|
|
||||||
REM 医学信息支持系统 - 本地开发启动脚本
|
|
||||||
|
|
||||||
echo ==================================
|
|
||||||
echo 医学信息支持系统 - 开发模式
|
|
||||||
echo ==================================
|
|
||||||
echo.
|
|
||||||
|
|
||||||
echo 📋 前置要求检查:
|
|
||||||
echo 1. MySQL 8.0+ 已安装并运行
|
|
||||||
echo 2. JDK 17+ 已安装
|
|
||||||
echo 3. Maven 3.8+ 已安装
|
|
||||||
echo 4. Node.js 18+ 已安装
|
|
||||||
echo 5. 数据库已创建并导入表结构
|
|
||||||
echo.
|
|
||||||
|
|
||||||
set /p confirm="确认已满足以上要求?(Y/N): "
|
|
||||||
if /i not "%confirm%"=="Y" (
|
|
||||||
echo.
|
|
||||||
echo 请先满足前置要求后再运行此脚本
|
|
||||||
echo.
|
|
||||||
echo 数据库创建步骤:
|
|
||||||
echo 1. 连接MySQL: mysql -u root -p
|
|
||||||
echo 2. 创建数据库: CREATE DATABASE medical_info_system;
|
|
||||||
echo 3. 导入表结构: SOURCE database/schema.sql;
|
|
||||||
echo 4. 导入示例数据: SOURCE database/sample_data.sql;
|
|
||||||
pause
|
|
||||||
exit /b 0
|
|
||||||
)
|
|
||||||
|
|
||||||
echo.
|
|
||||||
echo ================================
|
|
||||||
echo 启动服务
|
|
||||||
echo ================================
|
|
||||||
echo.
|
|
||||||
|
|
||||||
REM 检查后端目录
|
|
||||||
if not exist backend\pom.xml (
|
|
||||||
echo ❌ 错误: 找不到后端项目
|
|
||||||
pause
|
|
||||||
exit /b 1
|
|
||||||
)
|
|
||||||
|
|
||||||
REM 检查前端目录
|
|
||||||
if not exist frontend\package.json (
|
|
||||||
echo ❌ 错误: 找不到前端项目
|
|
||||||
pause
|
|
||||||
exit /b 1
|
|
||||||
)
|
|
||||||
|
|
||||||
echo 🚀 正在启动后端服务...
|
|
||||||
echo.
|
|
||||||
echo [后端] 将在新窗口启动,端口: 8080
|
|
||||||
start "医学系统-后端" cmd /k "cd backend && echo 正在启动Spring Boot... && mvn spring-boot:run"
|
|
||||||
|
|
||||||
echo.
|
|
||||||
echo ⏳ 等待后端启动(10秒)...
|
|
||||||
timeout /t 10 /nobreak >nul
|
|
||||||
|
|
||||||
echo.
|
|
||||||
echo 🚀 正在启动前端服务...
|
|
||||||
echo.
|
|
||||||
echo [前端] 将在新窗口启动,端口: 3000
|
|
||||||
|
|
||||||
REM 检查是否需要安装依赖
|
|
||||||
if not exist frontend\node_modules (
|
|
||||||
echo 首次运行,正在安装前端依赖...
|
|
||||||
start "医学系统-前端-安装" cmd /k "cd frontend && npm install && npm run dev"
|
|
||||||
) else (
|
|
||||||
start "医学系统-前端" cmd /k "cd frontend && npm run dev"
|
|
||||||
)
|
|
||||||
|
|
||||||
echo.
|
|
||||||
echo ================================
|
|
||||||
echo ✅ 服务启动中...
|
|
||||||
echo ================================
|
|
||||||
echo.
|
|
||||||
echo 请等待启动完成后访问:
|
|
||||||
echo.
|
|
||||||
echo 📱 前端开发服务器: http://localhost:3000
|
|
||||||
echo 🔧 后端API服务: http://localhost:8080/api
|
|
||||||
echo.
|
|
||||||
echo 默认登录账号:
|
|
||||||
echo 用户名: admin
|
|
||||||
echo 密码: admin123
|
|
||||||
echo.
|
|
||||||
echo ⚠️ 提示:
|
|
||||||
echo - 后端和前端分别在独立窗口运行
|
|
||||||
echo - 关闭窗口即停止服务
|
|
||||||
echo - 请确保MySQL服务正在运行
|
|
||||||
echo - 首次启动前端可能需要较长时间安装依赖
|
|
||||||
echo.
|
|
||||||
pause
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
74
start.bat
74
start.bat
|
|
@ -1,74 +0,0 @@
|
||||||
@echo off
|
|
||||||
chcp 65001 >nul
|
|
||||||
REM 医学信息支持系统 - Windows启动脚本
|
|
||||||
|
|
||||||
echo ==================================
|
|
||||||
echo 医学信息支持系统 - 启动脚本
|
|
||||||
echo ==================================
|
|
||||||
echo.
|
|
||||||
|
|
||||||
REM 检查Docker是否安装
|
|
||||||
where docker >nul 2>nul
|
|
||||||
if %ERRORLEVEL% NEQ 0 (
|
|
||||||
echo ❌ 错误: 未检测到Docker,请先安装Docker Desktop
|
|
||||||
pause
|
|
||||||
exit /b 1
|
|
||||||
)
|
|
||||||
|
|
||||||
REM 检查Docker Compose是否安装
|
|
||||||
where docker-compose >nul 2>nul
|
|
||||||
if %ERRORLEVEL% NEQ 0 (
|
|
||||||
echo ❌ 错误: 未检测到Docker Compose,请先安装Docker Compose
|
|
||||||
pause
|
|
||||||
exit /b 1
|
|
||||||
)
|
|
||||||
|
|
||||||
REM 检查.env文件是否存在
|
|
||||||
if not exist .env (
|
|
||||||
echo ⚠️ 警告: 未找到.env文件,正在复制示例配置...
|
|
||||||
copy env.example .env
|
|
||||||
echo ✅ 已创建.env文件,请编辑此文件配置API密钥等信息
|
|
||||||
echo.
|
|
||||||
echo 请编辑.env文件后重新运行此脚本
|
|
||||||
pause
|
|
||||||
exit /b 0
|
|
||||||
)
|
|
||||||
|
|
||||||
echo 📦 正在启动服务...
|
|
||||||
echo.
|
|
||||||
|
|
||||||
REM 启动Docker Compose
|
|
||||||
docker-compose up -d
|
|
||||||
|
|
||||||
REM 等待服务启动
|
|
||||||
echo.
|
|
||||||
echo ⏳ 等待服务启动...
|
|
||||||
timeout /t 10 /nobreak >nul
|
|
||||||
|
|
||||||
REM 检查服务状态
|
|
||||||
echo.
|
|
||||||
echo 📊 服务状态:
|
|
||||||
docker-compose ps
|
|
||||||
|
|
||||||
echo.
|
|
||||||
echo ==================================
|
|
||||||
echo ✅ 系统启动完成!
|
|
||||||
echo ==================================
|
|
||||||
echo.
|
|
||||||
echo 访问地址:
|
|
||||||
echo 前端: http://localhost
|
|
||||||
echo 后端API: http://localhost:8080/api
|
|
||||||
echo.
|
|
||||||
echo 默认账号:
|
|
||||||
echo 用户名: admin
|
|
||||||
echo 密码: admin123
|
|
||||||
echo.
|
|
||||||
echo 查看日志:
|
|
||||||
echo docker-compose logs -f
|
|
||||||
echo.
|
|
||||||
echo 停止服务:
|
|
||||||
echo docker-compose down
|
|
||||||
echo.
|
|
||||||
pause
|
|
||||||
|
|
||||||
|
|
||||||
70
start.sh
70
start.sh
|
|
@ -1,70 +0,0 @@
|
||||||
#!/bin/bash
|
|
||||||
|
|
||||||
# 医学信息支持系统 - 快速启动脚本
|
|
||||||
|
|
||||||
echo "=================================="
|
|
||||||
echo "医学信息支持系统 - 启动脚本"
|
|
||||||
echo "=================================="
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
# 检查Docker是否安装
|
|
||||||
if ! command -v docker &> /dev/null; then
|
|
||||||
echo "❌ 错误: 未检测到Docker,请先安装Docker"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# 检查Docker Compose是否安装
|
|
||||||
if ! command -v docker-compose &> /dev/null; then
|
|
||||||
echo "❌ 错误: 未检测到Docker Compose,请先安装Docker Compose"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# 检查.env文件是否存在
|
|
||||||
if [ ! -f .env ]; then
|
|
||||||
echo "⚠️ 警告: 未找到.env文件,正在复制示例配置..."
|
|
||||||
cp .env.example .env
|
|
||||||
echo "✅ 已创建.env文件,请编辑此文件配置API密钥等信息"
|
|
||||||
echo ""
|
|
||||||
echo "请编辑.env文件后重新运行此脚本"
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "📦 正在启动服务..."
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
# 启动Docker Compose
|
|
||||||
docker-compose up -d
|
|
||||||
|
|
||||||
# 等待服务启动
|
|
||||||
echo ""
|
|
||||||
echo "⏳ 等待服务启动..."
|
|
||||||
sleep 10
|
|
||||||
|
|
||||||
# 检查服务状态
|
|
||||||
echo ""
|
|
||||||
echo "📊 服务状态:"
|
|
||||||
docker-compose ps
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo "=================================="
|
|
||||||
echo "✅ 系统启动完成!"
|
|
||||||
echo "=================================="
|
|
||||||
echo ""
|
|
||||||
echo "访问地址:"
|
|
||||||
echo " 前端: http://localhost"
|
|
||||||
echo " 后端API: http://localhost:8080/api"
|
|
||||||
echo ""
|
|
||||||
echo "默认账号:"
|
|
||||||
echo " 用户名: admin"
|
|
||||||
echo " 密码: admin123"
|
|
||||||
echo ""
|
|
||||||
echo "查看日志:"
|
|
||||||
echo " docker-compose logs -f"
|
|
||||||
echo ""
|
|
||||||
echo "停止服务:"
|
|
||||||
echo " docker-compose down"
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
50
启动前后端.bat
50
启动前后端.bat
|
|
@ -1,50 +0,0 @@
|
||||||
@echo off
|
|
||||||
chcp 65001 >nul
|
|
||||||
REM 医学信息支持系统 - 前后端启动脚本
|
|
||||||
|
|
||||||
echo ==================================
|
|
||||||
echo 医学信息支持系统 - 启动前后端
|
|
||||||
echo ==================================
|
|
||||||
echo.
|
|
||||||
|
|
||||||
echo 📋 启动步骤:
|
|
||||||
echo 1. 启动后端服务 (Spring Boot)
|
|
||||||
echo 2. 启动前端服务 (Vue3)
|
|
||||||
echo.
|
|
||||||
|
|
||||||
echo 🚀 正在启动后端服务...
|
|
||||||
echo.
|
|
||||||
start "医学系统-后端" cmd /k "cd backend && echo 正在启动Spring Boot... && set JAVA_HOME=C:\Program Files\Java\jdk1.8.0_161 && set PATH=%JAVA_HOME%\bin;%PATH% && mvn spring-boot:run"
|
|
||||||
|
|
||||||
echo.
|
|
||||||
echo ⏳ 等待后端启动(5秒)...
|
|
||||||
timeout /t 5 /nobreak >nul
|
|
||||||
|
|
||||||
echo.
|
|
||||||
echo 🎨 正在启动前端服务...
|
|
||||||
echo.
|
|
||||||
start "医学系统-前端" cmd /k "cd frontend && echo 正在安装依赖... && npm install && echo 正在启动开发服务器... && npm run dev"
|
|
||||||
|
|
||||||
echo.
|
|
||||||
echo ================================
|
|
||||||
echo ✅ 服务启动中...
|
|
||||||
echo ================================
|
|
||||||
echo.
|
|
||||||
echo 请等待启动完成后访问:
|
|
||||||
echo.
|
|
||||||
echo 📱 前端开发服务器: http://localhost:3000
|
|
||||||
echo 🔧 后端API服务: http://localhost:8080/api
|
|
||||||
echo.
|
|
||||||
echo 默认登录账号:
|
|
||||||
echo 用户名: admin
|
|
||||||
echo 密码: admin123
|
|
||||||
echo.
|
|
||||||
echo ⚠️ 提示:
|
|
||||||
echo - 后端和前端分别在独立窗口运行
|
|
||||||
echo - 关闭窗口即停止服务
|
|
||||||
echo - 首次启动前端可能需要较长时间安装依赖
|
|
||||||
echo - 后端启动需要1-2分钟
|
|
||||||
echo.
|
|
||||||
pause
|
|
||||||
|
|
||||||
|
|
||||||
101
数据库初始化指南.txt
101
数据库初始化指南.txt
|
|
@ -1,101 +0,0 @@
|
||||||
====================================
|
|
||||||
医学信息支持系统 - 数据库初始化指南
|
|
||||||
====================================
|
|
||||||
|
|
||||||
步骤1: 确保MySQL已安装并运行
|
|
||||||
------------------------------
|
|
||||||
Windows:
|
|
||||||
- 打开"服务"(services.msc)
|
|
||||||
- 找到MySQL80服务,确保已启动
|
|
||||||
- 或在MySQL安装目录运行: net start mysql80
|
|
||||||
|
|
||||||
步骤2: 连接到MySQL
|
|
||||||
------------------------------
|
|
||||||
打开命令行,执行:
|
|
||||||
mysql -u root -p
|
|
||||||
|
|
||||||
输入MySQL的root密码
|
|
||||||
|
|
||||||
步骤3: 创建数据库
|
|
||||||
------------------------------
|
|
||||||
在MySQL命令行中执行:
|
|
||||||
|
|
||||||
CREATE DATABASE IF NOT EXISTS medical_info_system DEFAULT CHARACTER SET utf8mb4 DEFAULT COLLATE utf8mb4_unicode_ci;
|
|
||||||
|
|
||||||
USE medical_info_system;
|
|
||||||
|
|
||||||
步骤4: 导入表结构
|
|
||||||
------------------------------
|
|
||||||
方法A - 在MySQL命令行中:
|
|
||||||
SOURCE d:/SoftwarePrj/文献流程/database/schema.sql;
|
|
||||||
SOURCE d:/SoftwarePrj/文献流程/database/sample_data.sql;
|
|
||||||
|
|
||||||
方法B - 在系统命令行中:
|
|
||||||
cd d:\SoftwarePrj\文献流程\database
|
|
||||||
mysql -u root -p medical_info_system < schema.sql
|
|
||||||
mysql -u root -p medical_info_system < sample_data.sql
|
|
||||||
|
|
||||||
步骤5: 验证数据库
|
|
||||||
------------------------------
|
|
||||||
在MySQL命令行中执行:
|
|
||||||
|
|
||||||
USE medical_info_system;
|
|
||||||
SHOW TABLES;
|
|
||||||
|
|
||||||
应该看到以下表:
|
|
||||||
- users
|
|
||||||
- inquiry_requests
|
|
||||||
- knowledge_bases
|
|
||||||
- literatures
|
|
||||||
- audit_logs
|
|
||||||
|
|
||||||
步骤6: 配置后端数据库连接
|
|
||||||
------------------------------
|
|
||||||
编辑文件: backend\src\main\resources\application.yml
|
|
||||||
|
|
||||||
找到 datasource 部分,修改为您的MySQL配置:
|
|
||||||
|
|
||||||
spring:
|
|
||||||
datasource:
|
|
||||||
url: jdbc:mysql://localhost:3306/medical_info_system?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai
|
|
||||||
username: root # 修改为您的MySQL用户名
|
|
||||||
password: your_password # 修改为您的MySQL密码
|
|
||||||
|
|
||||||
步骤7: 启动系统
|
|
||||||
------------------------------
|
|
||||||
数据库初始化完成后,可以启动系统:
|
|
||||||
|
|
||||||
使用Docker:
|
|
||||||
双击 start.bat
|
|
||||||
|
|
||||||
本地开发:
|
|
||||||
双击 start-dev.bat
|
|
||||||
|
|
||||||
====================================
|
|
||||||
常见问题
|
|
||||||
====================================
|
|
||||||
|
|
||||||
Q1: 连接MySQL失败,提示"Access denied"
|
|
||||||
A1: 检查用户名和密码是否正确
|
|
||||||
|
|
||||||
Q2: 提示"Unknown database"
|
|
||||||
A2: 确认已执行步骤3创建数据库
|
|
||||||
|
|
||||||
Q3: 导入SQL文件失败
|
|
||||||
A3: 检查文件路径是否正确,确保使用绝对路径
|
|
||||||
|
|
||||||
Q4: 表已存在的警告
|
|
||||||
A4: 可以忽略,或先删除数据库重新创建:
|
|
||||||
DROP DATABASE medical_info_system;
|
|
||||||
然后重新执行步骤3-4
|
|
||||||
|
|
||||||
====================================
|
|
||||||
需要帮助?
|
|
||||||
====================================
|
|
||||||
|
|
||||||
1. 查看完整文档: DEPLOYMENT.md
|
|
||||||
2. 查看快速指南: START_GUIDE.md
|
|
||||||
3. 查看项目结构: PROJECT_STRUCTURE.md
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
126
方案二执行完成报告.md
126
方案二执行完成报告.md
|
|
@ -1,126 +0,0 @@
|
||||||
# 方案二执行完成报告
|
|
||||||
|
|
||||||
## ✅ 执行状态:成功完成
|
|
||||||
|
|
||||||
### 📋 执行步骤总结
|
|
||||||
|
|
||||||
#### 1️⃣ **问题诊断**
|
|
||||||
- **原始问题**: Spring Boot 3.2.0 需要 Java 17,但系统使用 Java 8
|
|
||||||
- **错误信息**: `class file version 61.0` vs `class file version 52.0`
|
|
||||||
|
|
||||||
#### 2️⃣ **降级Spring Boot版本**
|
|
||||||
- ✅ 备份原始 `pom.xml` → `pom-backup.xml`
|
|
||||||
- ✅ 使用 `pom-java8.xml` 替换,降级到 Spring Boot 2.7.18
|
|
||||||
- ✅ 修改Java版本为8,更新依赖版本
|
|
||||||
|
|
||||||
#### 3️⃣ **解决Java环境问题**
|
|
||||||
- **问题**: Maven使用JRE而非JDK
|
|
||||||
- **解决**: 临时设置 `JAVA_HOME` 指向正确的JDK路径
|
|
||||||
- ✅ 验证: `mvn -version` 显示使用 JDK 1.8.0_161
|
|
||||||
|
|
||||||
#### 4️⃣ **修复语法兼容性**
|
|
||||||
- **问题**: Java 14+ 的 switch 表达式语法
|
|
||||||
- **解决**: 修改 `ExcelParserServiceImpl.java` 为传统 switch 语句
|
|
||||||
- ✅ 修复位置: 第63-68行
|
|
||||||
|
|
||||||
#### 5️⃣ **修复JPA包名兼容性**
|
|
||||||
- **问题**: Spring Boot 2.7 使用 `javax.persistence`,代码使用 `jakarta.persistence`
|
|
||||||
- **解决**: 批量替换所有实体类的import语句
|
|
||||||
- ✅ 修复文件:
|
|
||||||
- `InquiryRequest.java`
|
|
||||||
- `KnowledgeBase.java`
|
|
||||||
- `Literature.java`
|
|
||||||
- `User.java`
|
|
||||||
- `AuditLog.java`
|
|
||||||
|
|
||||||
#### 6️⃣ **编译和启动**
|
|
||||||
- ✅ `mvn clean` - 清理成功
|
|
||||||
- ✅ `mvn compile` - 编译成功
|
|
||||||
- ✅ `mvn spring-boot:run` - 后端启动中
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🎯 当前状态
|
|
||||||
|
|
||||||
### ✅ 已完成
|
|
||||||
- [x] Spring Boot 降级到 2.7.18
|
|
||||||
- [x] Java 8 兼容性修复
|
|
||||||
- [x] 语法兼容性修复
|
|
||||||
- [x] JPA包名兼容性修复
|
|
||||||
- [x] 项目编译成功
|
|
||||||
- [x] 后端服务启动中
|
|
||||||
|
|
||||||
### 🔄 进行中
|
|
||||||
- [ ] 后端服务完全启动(需要等待)
|
|
||||||
- [ ] 前端服务启动
|
|
||||||
- [ ] 数据库连接测试
|
|
||||||
|
|
||||||
### ⏳ 待完成
|
|
||||||
- [ ] 验证API接口
|
|
||||||
- [ ] 测试系统功能
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📊 技术变更总结
|
|
||||||
|
|
||||||
### 版本变更
|
|
||||||
| 组件 | 原版本 | 新版本 | 原因 |
|
|
||||||
|------|--------|--------|------|
|
|
||||||
| Spring Boot | 3.2.0 | 2.7.18 | Java 8 兼容 |
|
|
||||||
| Java | 17 | 8 | 系统环境 |
|
|
||||||
| JPA | jakarta.* | javax.* | Spring Boot 2.x 兼容 |
|
|
||||||
|
|
||||||
### 代码修改
|
|
||||||
1. **ExcelParserServiceImpl.java**: switch表达式 → 传统switch语句
|
|
||||||
2. **所有Entity类**: jakarta.persistence → javax.persistence
|
|
||||||
3. **pom.xml**: 完整的依赖版本降级
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🚀 下一步操作
|
|
||||||
|
|
||||||
### 1. 等待后端启动完成
|
|
||||||
后端服务正在启动中,通常需要30-60秒。
|
|
||||||
|
|
||||||
### 2. 启动前端服务
|
|
||||||
在新的命令行窗口中执行:
|
|
||||||
```bash
|
|
||||||
cd frontend
|
|
||||||
npm install
|
|
||||||
npm run dev
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. 验证系统
|
|
||||||
- 后端API: http://localhost:8080/api
|
|
||||||
- 前端页面: http://localhost:3000
|
|
||||||
- 默认账号: admin / admin123
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📚 相关文档
|
|
||||||
|
|
||||||
- `Java版本问题解决指南.txt` - 详细的问题分析
|
|
||||||
- `Java环境问题解决.txt` - Java环境配置指南
|
|
||||||
- `START_GUIDE.md` - 快速启动指南
|
|
||||||
- `DEPLOYMENT.md` - 完整部署文档
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ⚠️ 注意事项
|
|
||||||
|
|
||||||
1. **临时环境变量**: 当前JAVA_HOME设置仅在当前会话有效
|
|
||||||
2. **永久配置**: 建议按 `Java环境问题解决.txt` 进行永久配置
|
|
||||||
3. **功能限制**: Spring Boot 2.7 缺少一些3.x的新特性
|
|
||||||
4. **升级建议**: 长期建议升级到Java 17 + Spring Boot 3.x
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🎉 成功指标
|
|
||||||
|
|
||||||
- ✅ 编译无错误
|
|
||||||
- ✅ 后端服务启动
|
|
||||||
- ✅ 使用Java 8环境
|
|
||||||
- ✅ 保持原有功能完整性
|
|
||||||
|
|
||||||
**方案二执行成功!系统已成功降级并启动。**
|
|
||||||
|
|
||||||
Loading…
Reference in New Issue