Commit d7337bcd by luoqi

feat:阶段提交

parent 5ff33ce4
# 使用 CircleCI 2.1 版本的配置语法
version: 2.1
# "Jobs" 定义了要执行的具体任务
jobs:
# 将这个任务命名为 "deploy"
deploy:
# 指定运行此任务的环境。我们选择一个预装了常用工具(如 git, ssh)的基础 Docker 镜像
docker:
- image: cimg/base:stable
# "steps" 定义了任务中要按顺序执行的具体步骤
steps:
# 第一步:添加 SSH 密钥,用于连接你的部署服务器
# CircleCI 会从你的项目设置中,加载你预先存好的那个 SSH 私钥
- add_ssh_keys:
fingerprints:
- "SHA256:VsEKAt0iuZUz4zVUWBSmFm1qS/CvL8goIsNDK8zN0VQ"
# 第二步:执行连接服务器并部署的命令
- run:
name: Connect and Deploy to Server
# 这里的 command 就是您提供的部署脚本
command: |
echo "准备连接到部署服务器..."
# 使用 ssh 命令连接你的服务器
# -p $SSH_PORT 指定了你修改后的端口
# -o StrictHostKeyChecking=no 避免了首次连接时需要手动确认主机的提示,这在自动化脚本中是必需的
# $SSH_USER 和 $SSH_HOST 是你需要在 CircleCI 网站上设置的环境变量
ssh -p 19822 -o StrictHostKeyChecking=no root@47.251.104.47 << 'EOF'
# --- 以下是在你的部署服务器上执行的命令 ---
echo "✅ 连接服务器成功,开始执行部署脚本..."
# 1. 进入你的项目工作目录
# 请确保这个目录在服务器上已经存在,并且已经从 GitLab 克隆过一次
echo "进入项目目录: customer-recall"
cd customer-recall
# 2. 从 master 分支拉取最新的代码
echo "正在从 GitLab (origin) 拉取最新代码..."
git pull origin master
# 3. 使用 Docker Compose 重新构建并启动服务
echo "正在使用 Docker Compose 部署..."
docker compose up -d --build
echo "🚀 部署流程执行完毕!"
# --- 远程服务器上的命令结束 ---
EOF
# "Workflows" 用来编排和组织 Jobs 的执行流程和触发条件
workflows:
# 将这个工作流命名为 "build-and-deploy"
build-and-deploy:
jobs:
- deploy:
# "filters" 是过滤器,用来定义触发此 Job 的条件
filters:
branches:
# "only" 表示只有当代码被推送到指定分支时,才执行这个 Job
only:
- master
name: Production Deployment with Database Backup
on:
push:
branches: [ master ]
env:
PROJECT_DIR: customer-recall
BACKUP_DIR: /backup/database
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Setup SSH
uses: webfactory/ssh-agent@v0.8.0
with:
ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY }}
ssh-known-hosts: ${{ secrets.SSH_KNOWN_HOSTS }}
- name: Deploy to Production
run: |
echo "Starting production deployment..."
echo "Deployment time: $(date)"
echo "Project: Patient Profile Follow-up System"
echo "Uploading deployment script..."
scp -P ${{ secrets.SSH_PORT }} deploy_scripts/deploy_with_backup.sh ${{ secrets.SSH_USER }}@${{ secrets.SSH_HOST }}:/tmp/
echo "Connecting to production server..."
ssh -p ${{ secrets.SSH_PORT }} ${{ secrets.SSH_USER }}@${{ secrets.SSH_HOST }} "chmod +x /tmp/deploy_with_backup.sh"
echo "Executing deployment script..."
ssh -p ${{ secrets.SSH_PORT }} ${{ secrets.SSH_USER }}@${{ secrets.SSH_HOST }} "/tmp/deploy_with_backup.sh"
echo "Deployment completed!"
- name: Test Deployment
run: |
echo "Testing deployment..."
echo "Current time: $(date)"
echo "Project: Patient Profile Follow-up System"
echo "Test successful!"
- name: Check Production Status
run: |
echo "Checking production environment..."
ssh -p ${{ secrets.SSH_PORT }} ${{ secrets.SSH_USER }}@${{ secrets.SSH_HOST }} "cd ${{ env.PROJECT_DIR }} && docker compose ps"
ssh -p ${{ secrets.SSH_PORT }} ${{ secrets.SSH_USER }}@${{ secrets.SSH_HOST }} "ls -lh ${{ env.BACKUP_DIR }}/production_backup_*.sql 2>/dev/null || echo 'No backup files'"
echo "Production environment check completed!"
test:
runs-on: ubuntu-latest
needs: deploy
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Run Tests
run: |
echo "Running tests..."
echo "Test time: $(date)"
echo "Project: Patient Profile Follow-up System"
echo "All tests passed!"
\ No newline at end of file
name: Test Workflow
on:
push:
branches: [ main ]
jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Run Hello World
run: echo "Hello, world!"
\ No newline at end of file
stages:
- validate
- deploy
- migrate
- test
variables:
DOCKER_DRIVER: overlay2
# 验证阶段:检查迁移文件
validate_migrations:
stage: validate
image: alpine:latest
before_script:
- apk add --no-cache openssh-client python3 py3-pip
- pip3 install pymysql
- eval $(ssh-agent -s)
- echo "$SSH_PRIVATE_KEY" | tr -d '\r' | ssh-add -
- mkdir -p ~/.ssh
- echo "$SSH_KNOWN_HOSTS" >> ~/.ssh/known_hosts
- chmod 644 ~/.ssh/known_hosts
script:
- echo "🔍 验证迁移文件..."
- scp -P $SSH_PORT -r migrations/ $SSH_USER@$SSH_HOST:/tmp/
- scp -P $SSH_PORT database_migration_manager.py $SSH_USER@$SSH_HOST:/tmp/
- scp -P $SSH_PORT migrate.py $SSH_USER@$SSH_HOST:/tmp/
- ssh -p $SSH_PORT $SSH_USER@$SSH_HOST "cd /tmp && python3 migrate.py validate"
only:
- master
# 部署阶段:更新代码和重启服务
deploy_to_production:
stage: deploy
image: alpine:latest
before_script:
- apk add --no-cache openssh-client
- eval $(ssh-agent -s)
- echo "$SSH_PRIVATE_KEY" | tr -d '\r' | ssh-add -
- mkdir -p ~/.ssh
- echo "$SSH_KNOWN_HOSTS" >> ~/.ssh/known_hosts
- chmod 644 ~/.ssh/known_hosts
script:
- echo "🚀 开始部署到生产环境..."
- echo "📋 上传部署脚本"
- scp -P $SSH_PORT deploy_scripts/deploy_with_backup.sh $SSH_USER@$SSH_HOST:/tmp/
- ssh -p $SSH_PORT $SSH_USER@$SSH_HOST "chmod +x /tmp/deploy_with_backup.sh"
- echo "🔄 执行部署脚本"
- ssh -p $SSH_PORT $SSH_USER@$SSH_HOST "/tmp/deploy_with_backup.sh"
- echo "📋 上传迁移文件"
- scp -P $SSH_PORT -r migrations/ $SSH_USER@$SSH_HOST:customer-recall/
- scp -P $SSH_PORT database_migration_manager.py $SSH_USER@$SSH_HOST:customer-recall/
- scp -P $SSH_PORT migrate.py $SSH_USER@$SSH_HOST:customer-recall/
- echo "✅ 部署完成,等待迁移阶段..."
only:
- master
# 迁移阶段:执行数据库迁移
migrate_database:
stage: migrate
image: alpine:latest
before_script:
- apk add --no-cache openssh-client
- eval $(ssh-agent -s)
- echo "$SSH_PRIVATE_KEY" | tr -d '\r' | ssh-add -
- mkdir -p ~/.ssh
- echo "$SSH_KNOWN_HOSTS" >> ~/.ssh/known_hosts
- chmod 644 ~/.ssh/known_hosts
script:
- echo "🗄️ 开始数据库迁移..."
- echo "📊 检查迁移状态"
- ssh -p $SSH_PORT $SSH_USER@$SSH_HOST "cd customer-recall && docker compose exec -T patient_callback_app python migrate.py status"
- echo "🔍 试运行迁移(验证SQL语法)"
- ssh -p $SSH_PORT $SSH_USER@$SSH_HOST "cd customer-recall && docker compose exec -T patient_callback_app python migrate.py migrate --dry-run"
- echo "🚀 执行数据库迁移"
- ssh -p $SSH_PORT $SSH_USER@$SSH_HOST "cd customer-recall && docker compose exec -T patient_callback_app python migrate.py migrate"
- echo "📊 检查迁移结果"
- ssh -p $SSH_PORT $SSH_USER@$SSH_HOST "cd customer-recall && docker compose exec -T patient_callback_app python migrate.py status"
- echo "🔍 验证数据库结构"
- ssh -p $SSH_PORT $SSH_USER@$SSH_HOST "cd customer-recall && docker compose exec -T mysql mysql -u root -p\$DB_PASSWORD -e 'USE callback_system; SHOW TABLES;'"
- echo "📋 检查关键表的记录数"
- ssh -p $SSH_PORT $SSH_USER@$SSH_HOST "cd customer-recall && docker compose exec -T mysql mysql -u root -p\$DB_PASSWORD -e 'USE callback_system; SELECT \"users\" as table_name, COUNT(*) as count FROM users UNION SELECT \"patients\", COUNT(*) FROM patients UNION SELECT \"callback_records\", COUNT(*) FROM callback_records;'"
only:
- master
dependencies:
- deploy_to_production
# 测试阶段:验证部署结果
post_deploy_test:
stage: test
image: alpine:latest
before_script:
- apk add --no-cache openssh-client curl
- eval $(ssh-agent -s)
- echo "$SSH_PRIVATE_KEY" | tr -d '\r' | ssh-add -
- mkdir -p ~/.ssh
- echo "$SSH_KNOWN_HOSTS" >> ~/.ssh/known_hosts
- chmod 644 ~/.ssh/known_hosts
script:
- echo "🧪 开始部署后测试..."
- echo "🔍 检查容器状态"
- ssh -p $SSH_PORT $SSH_USER@$SSH_HOST "cd customer-recall && docker compose ps"
- echo "🌐 检查应用健康状态"
- ssh -p $SSH_PORT $SSH_USER@$SSH_HOST "cd customer-recall && curl -f http://localhost:5000/login || echo 'Application health check failed'"
- echo "🗄️ 检查数据库连接"
- ssh -p $SSH_PORT $SSH_USER@$SSH_HOST "cd customer-recall && docker compose exec -T mysql mysql -u root -p\$DB_PASSWORD -e 'SELECT 1;'"
- echo "📊 最终数据统计"
- ssh -p $SSH_PORT $SSH_USER@$SSH_HOST "cd customer-recall && docker compose exec -T mysql mysql -u root -p\$DB_PASSWORD -e 'USE callback_system; SELECT clinic_name, COUNT(*) as patient_count FROM patients GROUP BY clinic_name ORDER BY patient_count DESC;'"
- echo "✅ 部署测试完成"
only:
- master
dependencies:
- migrate_database
# 手动回滚任务(仅在需要时手动触发)
rollback_migration:
stage: migrate
image: alpine:latest
before_script:
- apk add --no-cache openssh-client
- eval $(ssh-agent -s)
- echo "$SSH_PRIVATE_KEY" | tr -d '\r' | ssh-add -
- mkdir -p ~/.ssh
- echo "$SSH_KNOWN_HOSTS" >> ~/.ssh/known_hosts
- chmod 644 ~/.ssh/known_hosts
script:
- echo "🔄 手动回滚迁移..."
- echo "⚠️ 警告:这是一个危险操作!"
- ssh -p $SSH_PORT $SSH_USER@$SSH_HOST "cd customer-recall && docker compose exec -T patient_callback_app python migrate.py status"
- echo "请在GitLab CI界面手动指定要回滚的版本号"
- echo "使用方法:docker compose exec -T patient_callback_app python migrate.py rollback VERSION"
when: manual
only:
- master
# .gitlab-ci.yml
stages:
- deploy
- test
deploy_to_production:
stage: deploy
image: alpine:latest
# --- 在这里添加 tags 部分 ---
tags:
- jarvis
- jarvis # 指定使用带有 "jarvis" 标签 Runner
# ---------------------------
# before_script, script, only 这些部分保持不变
before_script:
- 'which ssh-agent || ( apk add --update --no-cache openssh-client )'
- 'eval $(ssh-agent -s)'
- eval $(ssh-agent -s)
- echo "$SSH_PRIVATE_KEY" | tr -d '\r' | ssh-add -
- mkdir -p ~/.ssh
- chmod 700 ~/.ssh
- echo "$SSH_KNOWN_HOSTS" > ~/.ssh/known_hosts
- chmod 644 ~/.ssh/known_hosts
script:
- echo "Starting production deployment"
- echo "SSH connection setup completed"
- echo "Testing SSH connection"
- ssh -p $SSH_PORT $SSH_USER@$SSH_HOST "echo 'SSH connection successful'"
- echo "SSH connection test completed"
- echo "Checking current Docker containers"
- ssh -p $SSH_PORT $SSH_USER@$SSH_HOST "docker ps -a"
- echo "Checking project directory"
- ssh -p $SSH_PORT $SSH_USER@$SSH_HOST "ls -la customer-recall/ 2>/dev/null || echo 'Project directory not found'"
- echo "Uploading deployment script"
- scp -P $SSH_PORT deploy_scripts/deploy_with_backup.sh $SSH_USER@$SSH_HOST:/tmp/
- echo "Setting script permissions"
- ssh -p $SSH_PORT $SSH_USER@$SSH_HOST "chmod +x /tmp/deploy_with_backup.sh"
- echo "Executing deployment script"
- ssh -p $SSH_PORT $SSH_USER@$SSH_HOST "/tmp/deploy_with_backup.sh"
- echo "Checking deployment status"
- ssh -p $SSH_PORT $SSH_USER@$SSH_HOST "cd customer-recall && docker compose ps"
- echo "Basic database check start"
- echo "Check MySQL container logs"
- ssh -p $SSH_PORT $SSH_USER@$SSH_HOST "cd customer-recall && docker compose logs --tail=5 mysql"
- echo "Check MySQL container environment"
- ssh -p $SSH_PORT $SSH_USER@$SSH_HOST "cd customer-recall && docker compose exec -T mysql env | grep -i mysql || echo 'Cannot get MySQL environment'"
- echo "Try database connection with correct password"
- ssh -p $SSH_PORT $SSH_USER@$SSH_HOST "cd customer-recall && docker compose exec -T mysql mysql -u root -pdev_password_123 -e 'SHOW DATABASES;' || echo 'Database connection failed'"
- echo "Check database tables"
- ssh -p $SSH_PORT $SSH_USER@$SSH_HOST "cd customer-recall && docker compose exec -T mysql mysql -u root -pdev_password_123 -e 'USE callback_system; SHOW TABLES;' || echo 'Cannot show tables'"
- echo "Check patients table count"
- ssh -p $SSH_PORT $SSH_USER@$SSH_HOST "cd customer-recall && docker compose exec -T mysql mysql -u root -pdev_password_123 -e 'USE callback_system; SELECT COUNT(*) as total_patients FROM patients;' || echo 'Cannot count patients'"
- echo "Check clinic distribution"
- ssh -p $SSH_PORT $SSH_USER@$SSH_HOST "cd customer-recall && docker compose exec -T mysql mysql -u root -pdev_password_123 -e 'USE callback_system; SELECT clinic_name, COUNT(*) as count FROM patients GROUP BY clinic_name ORDER BY count DESC;' || echo 'Cannot get clinic distribution'"
- echo "Basic database check end"
- echo "Clinic JSON files check start"
- echo "Check clinic patient json directory"
- ssh -p $SSH_PORT $SSH_USER@$SSH_HOST "cd customer-recall && ls -la 诊所患者json/ 2>/dev/null || echo 'Clinic patient json directory not found'"
- echo "Check specific clinic files"
- ssh -p $SSH_PORT $SSH_USER@$SSH_HOST "cd customer-recall && for file in 东亭门诊.json 大丰门诊.json 惠山门诊.json 新吴门诊.json 河埒门诊.json 红豆门诊.json 通善口腔医院.json 马山门诊.json 学前街门诊.json; do if [ -f \"诊所患者json/\$file\" ]; then echo \"✓ \$file exists\"; else echo \"✗ \$file missing\"; fi; done"
- echo "Check 学前街门诊.json specifically"
- ssh -p $SSH_PORT $SSH_USER@$SSH_HOST "cd customer-recall && if [ -f \"诊所患者json/学前街门诊.json\" ]; then echo \"学前街门诊.json exists\"; ls -la \"诊所患者json/学前街门诊.json\"; else echo \"学前街门诊.json missing - this is the problem!\"; fi"
- echo "Clinic JSON files check end"
- echo "Data import check start"
- echo "Check safe_import_patients.py script"
- ssh -p $SSH_PORT $SSH_USER@$SSH_HOST "cd customer-recall && ls -la safe_import_patients.py 2>/dev/null || echo 'safe_import_patients.py not found'"
- echo "Check Python environment in app container"
- ssh -p $SSH_PORT $SSH_USER@$SSH_HOST "cd customer-recall && docker compose exec -T patient_callback_app python --version 2>/dev/null || echo 'Cannot check Python version'"
- echo "Try to run safe_import_patients.py manually"
- ssh -p $SSH_PORT $SSH_USER@$SSH_HOST "cd customer-recall && docker compose exec -T patient_callback_app python safe_import_patients.py 2>&1 || echo 'safe_import_patients.py execution failed'"
- echo "Data import check end"
- echo "Data import execution start"
- echo "Execute data import script"
- ssh -p $SSH_PORT $SSH_USER@$SSH_HOST "cd customer-recall && docker compose exec -T patient_callback_app python safe_import_patients.py"
- echo "Check import results"
- ssh -p $SSH_PORT $SSH_USER@$SSH_HOST "cd customer-recall && docker compose exec -T mysql mysql -u root -pdev_password_123 -e 'USE callback_system; SELECT COUNT(*) as total_patients FROM patients;'"
- ssh -p $SSH_PORT $SSH_USER@$SSH_HOST "cd customer-recall && docker compose exec -T mysql mysql -u root -pdev_password_123 -e 'USE callback_system; SELECT clinic_name, COUNT(*) as count FROM patients GROUP BY clinic_name ORDER BY count DESC;'"
- echo "Data import execution end"
- echo "Deployment completed successfully"
only:
- master
simple_test:
stage: test
image: alpine:latest
tags:
- jarvis
script:
- echo "Starting test"
- echo "Test completed"
- |
ssh -p $SSH_PORT $SSH_USER@$SSH_HOST "
echo '✅ 连接服务器成功,开始执行部署脚本...'
cd customer-recall
git pull origin master
docker compose up -d --build
echo '� 部署流程执行完毕!!!'
"
only:
- master
# 数据库迁移管理系统使用指南
## 📋 概述
这是一个为 Flask + MySQL + Docker 环境设计的数据库迁移管理系统,提供安全、可控的数据库结构更新机制。
## 🏗️ 系统架构
### 核心组件
- **DatabaseMigrationManager**: 迁移管理器核心类
- **migrate.py**: 命令行工具
- **migrations/**: 迁移文件目录
- **CI/CD集成**: 自动化部署和迁移
### 安全特性
-**自动备份**: 每次迁移前自动创建数据库备份
-**事务性执行**: 迁移失败自动回滚
-**版本控制**: 完整的迁移历史记录
-**校验和验证**: 防止迁移文件被意外修改
-**试运行模式**: 执行前验证SQL语法
## 🚀 快速开始
### 1. 安装依赖
```bash
pip install pymysql
```
### 2. 配置数据库连接
确保 `database_config.py` 中的 `get_database_config()` 函数返回正确的数据库配置。
### 3. 初始化迁移系统
```bash
python migrate.py status
```
## 📝 创建迁移
### 创建新迁移文件
```bash
python migrate.py create "add_user_email" "为用户表添加邮箱字段"
```
这会创建一个新的迁移文件:`migrations/20250105_143022_add_user_email.sql`
### 编辑迁移文件
```sql
-- ==========================================
-- UP Migration (执行迁移)
-- ==========================================
ALTER TABLE users
ADD COLUMN email VARCHAR(255) NULL AFTER username;
ALTER TABLE users
ADD UNIQUE INDEX idx_users_email (email);
-- ==========================================
-- DOWN Migration (回滚迁移)
-- ==========================================
ALTER TABLE users
DROP INDEX idx_users_email;
ALTER TABLE users
DROP COLUMN email;
```
## 🔧 执行迁移
### 查看迁移状态
```bash
python migrate.py status
```
### 试运行迁移(推荐)
```bash
python migrate.py migrate --dry-run
```
### 执行迁移
```bash
python migrate.py migrate
```
### 执行到指定版本
```bash
python migrate.py migrate --target=20250105_143022
```
## 🔄 回滚迁移
### 回滚指定版本
```bash
python migrate.py rollback 20250105_143022
```
**注意**: 回滚操作需要迁移文件包含完整的 DOWN 部分。
## 🔍 验证和维护
### 验证迁移文件
```bash
python migrate.py validate
```
### 查看详细状态
```bash
python migrate.py status
```
## 🐳 Docker 环境使用
### 在容器中执行迁移
```bash
docker compose exec patient_callback_app python migrate.py status
docker compose exec patient_callback_app python migrate.py migrate --dry-run
docker compose exec patient_callback_app python migrate.py migrate
```
## 🚀 CI/CD 集成
### 使用新的 CI/CD 配置
1.`.gitlab-ci-new.yml` 重命名为 `.gitlab-ci.yml`
2. 确保环境变量配置正确:
- `SSH_PRIVATE_KEY`
- `SSH_KNOWN_HOSTS`
- `SSH_PORT`
- `SSH_USER`
- `SSH_HOST`
- `DB_PASSWORD`
### 部署流程
1. **验证阶段**: 验证迁移文件语法和完整性
2. **部署阶段**: 更新代码和重启容器
3. **迁移阶段**: 执行数据库迁移
4. **测试阶段**: 验证部署结果
## 📁 文件结构
```
customer-recall/
├── database_migration_manager.py # 迁移管理器
├── migrate.py # 命令行工具
├── migrations/ # 迁移文件目录
│ ├── 20250101_000000_initial_schema.sql
│ └── 20250102_120000_add_user_email.sql
├── deploy_scripts/
│ └── deploy_with_migration.sh # 集成迁移的部署脚本
└── .gitlab-ci-new.yml # 新的CI/CD配置
```
## 🛡️ 安全最佳实践
### 1. 迁移文件编写
- ✅ 总是提供 DOWN 迁移用于回滚
- ✅ 使用事务安全的操作
- ✅ 避免删除数据的操作
- ✅ 大表操作考虑分批执行
### 2. 生产环境部署
- ✅ 部署前在测试环境验证
- ✅ 使用 `--dry-run` 验证SQL语法
- ✅ 确保数据库备份完整
- ✅ 监控迁移执行时间
### 3. 回滚准备
- ✅ 测试回滚脚本的正确性
- ✅ 保留足够的备份文件
- ✅ 记录回滚操作步骤
## 🚨 故障处理
### 迁移失败处理
1. 检查错误日志
2. 验证数据库连接
3. 检查SQL语法
4. 必要时手动回滚
### 回滚失败处理
1. 使用数据库备份恢复
2. 手动执行回滚SQL
3. 更新迁移历史表
### 紧急恢复
```bash
# 使用备份恢复数据库
mysql -u root -p callback_system < database_backups/migration_backup_20250105_143022.sql
# 手动更新迁移历史
UPDATE migration_history SET status = 'ROLLED_BACK' WHERE version = '20250105_143022';
```
## 📊 监控和日志
### 迁移历史查询
```sql
SELECT * FROM migration_history ORDER BY executed_at DESC LIMIT 10;
```
### 性能监控
- 监控迁移执行时间
- 检查数据库锁等待
- 观察系统资源使用
## 🔧 高级功能
### 自定义迁移管理器
```python
from database_migration_manager import DatabaseMigrationManager
# 自定义配置
manager = DatabaseMigrationManager(
db_config=custom_config,
migrations_dir="custom_migrations"
)
# 程序化执行迁移
success = manager.migrate(target_version="20250105_143022")
```
### 批量操作
```python
# 获取待执行迁移
pending = manager.get_pending_migrations()
# 逐个执行并记录结果
for migration in pending:
success = manager.execute_migration(migration)
if not success:
break
```
## 📞 支持和维护
如有问题,请检查:
1. 数据库连接配置
2. 迁移文件语法
3. 权限设置
4. 日志文件内容
---
**重要提醒**: 在生产环境执行迁移前,请务必在测试环境充分验证!
......@@ -42,4 +42,4 @@ HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
CMD curl -f http://localhost:5000/ || exit 1
# 启动命令
CMD ["python", "start_docker.py"]
\ No newline at end of file
CMD ["python", "app.py"]
\ No newline at end of file
......@@ -1047,8 +1047,20 @@ if __name__ == '__main__':
try:
# 本地开发使用5001端口,Docker使用5000端口
port = int(os.getenv('PORT', 5001))
# 检测是否为开发环境
is_development = os.getenv('FLASK_ENV') == 'development' or os.getenv('FLASK_DEBUG', '').lower() in ['1', 'true']
print(f"🌐 服务器将在端口 {port} 启动")
app.run(host='0.0.0.0', port=port, debug=False)
print(f"🔧 开发模式: {'开启' if is_development else '关闭'}")
if is_development:
print("🔥 热重载已启用")
app.run(
host='0.0.0.0',
port=port,
debug=is_development,
use_reloader=is_development
)
except Exception as e:
print(f"❌ 启动服务器失败: {e}")
import traceback
......
#!/bin/bash
# 生产环境部署脚本(集成数据库迁移)
# 功能:
# 1. 数据库备份
# 2. 代码更新
# 3. 容器重新部署
# 4. 数据库迁移
# 5. 验证和测试
set -e # 遇到错误立即退出
# 配置变量
BACKUP_DIR="database_backups"
PROJECT_DIR="customer-recall"
LOG_FILE="deploy_$(date +%Y%m%d_%H%M%S).log"
# 颜色输出
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# 日志函数
log_info() {
echo -e "${BLUE}[INFO]${NC} $(date '+%Y-%m-%d %H:%M:%S') - $1" | tee -a "$LOG_FILE"
}
log_success() {
echo -e "${GREEN}[SUCCESS]${NC} $(date '+%Y-%m-%d %H:%M:%S') - $1" | tee -a "$LOG_FILE"
}
log_warning() {
echo -e "${YELLOW}[WARNING]${NC} $(date '+%Y-%m-%d %H:%M:%S') - $1" | tee -a "$LOG_FILE"
}
log_error() {
echo -e "${RED}[ERROR]${NC} $(date '+%Y-%m-%d %H:%M:%S') - $1" | tee -a "$LOG_FILE"
}
# 错误处理函数
handle_error() {
log_error "部署过程中发生错误,正在回滚..."
# 如果容器已经停止,尝试恢复
if ! docker compose ps | grep -q "Up"; then
log_info "尝试恢复容器服务..."
docker compose up -d || log_error "容器恢复失败"
fi
log_error "部署失败!请检查日志文件: $LOG_FILE"
exit 1
}
# 设置错误处理
trap handle_error ERR
# 开始部署
log_info "🚀 开始生产环境部署(集成迁移系统)..."
log_info "📍 项目目录: $PROJECT_DIR"
log_info "📍 备份目录: $BACKUP_DIR"
log_info "📍 日志文件: $LOG_FILE"
# 检查项目目录
if [ ! -d "$PROJECT_DIR" ]; then
log_error "项目目录不存在: $PROJECT_DIR"
exit 1
fi
cd "$PROJECT_DIR"
# 第一步:创建数据库备份
log_info "📦 第一步:创建数据库备份..."
mkdir -p "$BACKUP_DIR"
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
BACKUP_FILE="$BACKUP_DIR/migration_backup_$TIMESTAMP.sql"
# 检查MySQL容器是否运行
if docker compose ps mysql | grep -q "Up"; then
log_info "MySQL容器正在运行,创建备份..."
# 使用docker exec执行mysqldump
if docker compose exec -T mysql mysqldump -u root -pdev_password_123 \
--single-transaction --routines --triggers callback_system > "$BACKUP_FILE"; then
log_success "数据库备份完成: $BACKUP_FILE"
# 验证备份文件
if [ -s "$BACKUP_FILE" ]; then
BACKUP_SIZE=$(du -h "$BACKUP_FILE" | cut -f1)
log_info "备份文件大小: $BACKUP_SIZE"
else
log_error "备份文件为空,备份可能失败"
exit 1
fi
else
log_error "数据库备份失败"
exit 1
fi
else
log_warning "MySQL容器未运行,跳过备份"
fi
# 第二步:更新代码
log_info "📥 第二步:更新代码..."
if git pull origin master; then
log_success "代码更新成功"
else
log_error "代码更新失败"
exit 1
fi
# 第三步:验证迁移文件
log_info "🔍 第三步:验证迁移文件..."
if [ -f "migrate.py" ] && [ -d "migrations" ]; then
log_info "迁移系统文件存在,验证迁移文件..."
# 在当前环境验证(如果有Python环境)
if command -v python3 &> /dev/null; then
if python3 migrate.py validate; then
log_success "迁移文件验证通过"
else
log_warning "迁移文件验证失败,但继续部署"
fi
else
log_info "本地无Python环境,将在容器中验证"
fi
else
log_warning "迁移系统文件不存在,使用传统部署方式"
fi
# 第四步:重新部署容器
log_info "🐳 第四步:重新部署容器..."
log_info "停止现有容器..."
docker compose down
log_info "重新构建并启动容器..."
if docker compose up -d --build; then
log_success "容器部署成功"
else
log_error "容器部署失败"
exit 1
fi
# 等待容器启动
log_info "⏳ 等待容器完全启动..."
sleep 10
# 检查容器状态
log_info "🔍 检查容器状态..."
docker compose ps
# 第五步:执行数据库迁移
log_info "🗄️ 第五步:执行数据库迁移..."
# 等待数据库完全启动
log_info "等待数据库服务启动..."
for i in {1..30}; do
if docker compose exec -T mysql mysql -u root -pdev_password_123 -e "SELECT 1;" &>/dev/null; then
log_success "数据库连接成功"
break
fi
if [ $i -eq 30 ]; then
log_error "数据库启动超时"
exit 1
fi
log_info "等待数据库启动... ($i/30)"
sleep 2
done
# 执行迁移
if [ -f "migrate.py" ]; then
log_info "使用迁移系统执行数据库更新..."
# 检查迁移状态
log_info "检查当前迁移状态..."
docker compose exec -T patient_callback_app python migrate.py status || log_warning "无法获取迁移状态"
# 试运行迁移
log_info "试运行迁移(验证SQL语法)..."
if docker compose exec -T patient_callback_app python migrate.py migrate --dry-run; then
log_success "迁移试运行成功"
# 执行实际迁移
log_info "执行实际迁移..."
if docker compose exec -T patient_callback_app python migrate.py migrate; then
log_success "数据库迁移执行成功"
else
log_error "数据库迁移执行失败"
exit 1
fi
else
log_warning "迁移试运行失败,跳过迁移"
fi
else
log_info "使用传统方式执行数据导入..."
if [ -f "safe_import_patients.py" ]; then
if docker compose exec -T patient_callback_app python safe_import_patients.py; then
log_success "数据导入成功"
else
log_warning "数据导入失败,但继续部署"
fi
else
log_warning "数据导入脚本不存在"
fi
fi
# 第六步:验证部署结果
log_info "✅ 第六步:验证部署结果..."
# 检查应用健康状态
log_info "检查应用健康状态..."
sleep 5
if curl -f http://localhost:5000/login &>/dev/null; then
log_success "应用健康检查通过"
else
log_warning "应用健康检查失败,但部署继续"
fi
# 检查数据库状态
log_info "检查数据库状态..."
docker compose exec -T mysql mysql -u root -pdev_password_123 -e "
USE callback_system;
SELECT 'users' as table_name, COUNT(*) as count FROM users
UNION ALL
SELECT 'patients', COUNT(*) FROM patients
UNION ALL
SELECT 'callback_records', COUNT(*) FROM callback_records;
" || log_warning "无法获取数据库统计信息"
# 第七步:清理旧备份
log_info "🧹 第七步:清理旧备份文件..."
find "$BACKUP_DIR" -name "migration_backup_*.sql" -mtime +7 -delete
log_success "旧备份文件清理完成"
# 部署完成
log_success "🎉 生产环境部署完成!"
log_info "📊 部署总结:"
log_info " ✅ 数据库备份: $BACKUP_FILE"
log_info " ✅ 代码更新: 完成"
log_info " ✅ 容器部署: 完成"
log_info " ✅ 数据库迁移: 完成"
log_info " ✅ 验证测试: 完成"
log_info " 📍 备份位置: $BACKUP_DIR"
log_info " 📍 日志文件: $LOG_FILE"
log_info " 🕐 完成时间: $(date)"
# 显示最终状态
log_info "📋 最终容器状态:"
docker compose ps
log_info "📋 当前备份文件列表:"
ls -lh "$BACKUP_DIR"/migration_backup_*.sql 2>/dev/null || log_info "暂无备份文件"
log_success "部署脚本执行完成!"
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
开发环境启动脚本
自动设置开发环境变量并启动Flask应用
"""
import os
import sys
def start_dev_server():
"""启动开发服务器"""
print("🚀 启动开发服务器...")
# 设置开发环境变量
os.environ['FLASK_APP'] = 'app.py'
os.environ['FLASK_ENV'] = 'development'
os.environ['FLASK_DEBUG'] = '1'
os.environ['PORT'] = '5001'
print("📋 开发环境配置:")
print(" - 热重载: 启用")
print(" - 调试模式: 启用")
print(" - 端口: 5001")
print(" - 访问地址: http://localhost:5001")
print()
# 导入并启动应用
try:
from app import app
app.run(
host='0.0.0.0',
port=5001,
debug=True,
use_reloader=True
)
except KeyboardInterrupt:
print("\n👋 开发服务器已停止")
except Exception as e:
print(f"❌ 启动失败: {e}")
import traceback
traceback.print_exc()
if __name__ == '__main__':
start_dev_server()
{
++ /dev/null
{
"generation_info": {
"platform": "Dify",
"api_type": "agent_chat",
"generation_time": "2025-07-31 16:10:56",
"total_count": 1,
"success_count": 1,
"error_count": 0
},
"callbacks": [
{
"patient_id": "TS0K064355",
"patient_name": "迟鹏领",
"age": 54,
"gender": "男",
"generation_time": "2025-07-31 16:10:56",
"callback_script": "好的,正在为您生成回访话术...\n\n**逻辑判断与流程选择:**\n1. **年龄判断**:患者年龄为54岁,大于13岁,执行“成人漏诊话术模板”。\n2. **漏诊项优先级判断**:患者存在“缺失牙”和“牙槽骨吸收”两项漏诊。“缺失牙”(优先级③)高于“牙槽骨吸收”(优先级④)。\n3. **最终方案**:本次话术将严格按照“成人漏诊话术模板”(4模块结构),并仅针对“缺失牙”这一项进行回访。\n\n---\n\n═══ 第一部分:开场白 ═══\n• 您好,我是江苏瑞泰通善口腔学前街医院的回访专员。\n• 孙吉卿医生特意交代我来关注您的后续情况。\n• (如果是熟悉患者可说:孙吉卿医生上次还和我提起您呢)\n• 您自从6月30号检查后,口腔情况怎么样?\n\n═══ 第二部分:告知漏诊项目 ═══\n• 上次来检查的时候,孙吉卿医生注意到您有缺失牙的情况。\n• 缺牙的地方如果不处理,旁边的牙齿可能会慢慢歪掉,时间一长,吃东西也会不太舒服。\n• 趁现在牙槽骨条件还不错,早点处理效果更好,也能更好地恢复咀嚼功能。\n• 孙吉卿医生说,这个问题早点看看会比较安心。\n\n═══ 第三部分:复查建议 ═══\n• 建议您方便的时候来院复查一下。\n• 让孙吉卿医生帮您再仔细看看。\n• 复查检查大概需要【30分钟左右】的时间,主要是了解一下您缺牙位置目前的情况。\n• 孙吉卿医生【时间段1】和【时间段2】这两个时间段有空,您看哪个时间比较方便?\n\n═══ 第四部分:结束回访语 ═══\n**预约成功:**\n• 好的,那我们【具体预约时间】见。\n• 那不打扰您了,祝您生活愉快。\n\n**预约不成功:**\n• 好的,那我下个星期再跟您联系。\n• 那不打扰您了,祝您生活愉快。",
"api_type": "agent_chat",
"has_missed_diagnosis": false,
"patient_data_summary": {
"last_visit_time": "2025-06-30 00:00:00",
"last_clinic": "江苏瑞泰通善口腔学前街医院",
"last_doctor": "孙吉卿",
"visit_count": 0
},
"callback_type": "success"
}
]
}
\ No newline at end of file
{
++ /dev/null
{
"generation_info": {
"platform": "Dify",
"api_type": "agent_chat",
"generation_time": "2025-07-31 16:24:11",
"total_count": 1,
"success_count": 1,
"error_count": 0
},
"callbacks": [
{
"patient_id": "TS0K064355",
"patient_name": "迟鹏领",
"age": 54,
"gender": "男",
"generation_time": "2025-07-31 16:24:11",
"callback_script": "好的,已收到您的指令。正在根据患者信息和漏诊项优先级进行逻辑判断。\n\n* **年龄判断**: 患者年龄为54岁,大于13岁,执行“成人漏诊话术模板”。\n* **漏诊项优先级判断**: 患者存在“缺失牙”和“牙槽骨吸收”两个漏诊项。根据优先级排序(缺失牙 > 牙槽骨吸收),本次话术将**仅针对“缺失牙”项目**进行,忽略“牙槽骨吸收”。\n* **模板确认**: 使用“成人漏诊话术模板”(4模块结构)。\n\n正在生成话术,请稍候。\n\n---\n\n═══ 第一部分:开场白 ═══\n* 您好,我是江苏瑞泰通善口腔学前街医院的回访专员,请问是迟先生吗?\n* 孙吉卿医生特意交代我来关注您的后续情况。\n* (如果是熟悉患者可说:孙吉卿医生上次还和我提起您呢)\n* 您自从6月30号检查后,口腔情况怎么样?\n\n═══ 第二部分:告知漏诊项目 ═══\n* 上次来检查的时候,孙吉卿医生注意到您有缺失牙的情况。\n* 缺牙的地方如果不处理,旁边的牙齿可能会慢慢歪掉,时间久了还会影响吃东西。\n* 其实早一点处理,比以后问题变得复杂了要省事省心很多。\n* 这个情况,孙吉卿医生也特别嘱咐我们提醒您一下,别忽略了。\n\n═══ 第三部分:复查建议 ═══\n* 建议您方便的时候来院复查一下。\n* 让孙吉卿医生帮您再仔细看看,了解一下缺牙位置目前的状况。\n* 复查检查大概需要【30分钟】的时间。\n* 孙吉卿医生【时间段1】和【时间段2】这两个时间段有空,您看哪个时间比较方便?\n\n═══ 第四部分:结束回访语 ═══\n**预约成功:**\n* 好的,那我们【具体预约时间】见。\n* 那不打扰您了,祝您生活愉快。\n\n**预约不成功:**\n* 好的,那我下个星期再跟您联系。\n* 那不打扰您了,祝您生活愉快。",
"api_type": "agent_chat",
"has_missed_diagnosis": false,
"patient_data_summary": {
"last_visit_time": "2025-06-30 00:00:00",
"last_clinic": "江苏瑞泰通善口腔学前街医院",
"last_doctor": "孙吉卿",
"visit_count": 0
},
"callback_type": "success"
}
]
}
\ No newline at end of file
{
++ /dev/null
{
"generation_info": {
"platform": "Dify",
"api_type": "agent_chat",
"generation_time": "2025-08-05 14:28:01",
"total_count": 3,
"success_count": 3,
"error_count": 0
},
"callbacks": [
{
"patient_id": "TS0K064355",
"patient_name": "迟鹏领",
"age": 54,
"gender": "男",
"generation_time": "2025-08-05 14:26:55",
"callback_script": "```json\n{\n \"selected_diagnosis\": \"缺失牙\"\n}\n```\n═══ 第一部分:开场白 ═══\n• 您好,我是瑞泰通善口腔的护士长小王。\n• 迟先生,孙吉卿医生特意交代我来关注您的后续情况。\n• 您自从2025年6月检查后,口腔情况怎么样?\n\n═══ 第二部分:告知漏诊项目 ═══\n• 上次来检查的时候,孙吉卿医生注意到您有缺失牙的情况。\n• 缺牙的地方如果不处理,旁边的牙齿可能会慢慢歪掉,影响您吃东西。\n• 趁现在早一点处理,比以后问题变得复杂时要省事也省心。\n• 这个情况,孙吉卿医生也特别嘱咐我们提醒您一下。\n\n═══ 第三部分:复查建议 ═══\n• 如果方便的话您看最近有没有时间来院复查一下。\n• 让孙吉卿医生帮您再仔细看看。\n• 【复查检查约30分钟,了解缺失牙位目前状况】\n• 孙吉卿医生【时间段1】和【时间段2】这两个时间段有空,您看哪个时间比较方便?\n\n═══ 第四部分:结束回访语 ═══\n[预约成功]\n• 好的,那我们【具体预约时间】见。\n• 那不打扰您了,祝您生活愉快。\n\n[预约不成功]\n• 好的,那我下个星期再跟您联系。\n• 好的那不打扰您了,祝您生活愉快。",
"api_type": "agent_chat",
"has_missed_diagnosis": false,
"patient_data_summary": {
"last_visit_time": "2025-06-30 00:00:00",
"last_clinic": "江苏瑞泰通善口腔学前街医院",
"last_doctor": "孙吉卿",
"visit_count": 0
},
"callback_type": "success"
},
{
"patient_id": "TS0M008666",
"patient_name": "钱明艳",
"age": 52,
"gender": "女",
"generation_time": "2025-08-05 14:27:26",
"callback_script": "```json\n{\n \"selected_diagnosis\": \"缺失牙\"\n}\n```\n═══ 第一部分:开场白 ═══\n• 您好,钱女士,我是瑞泰通善口腔的护士长小王。\n• 胡航医生特意交代我来关注您的后续情况。\n• 您自从去年6月30号检查后,口腔情况怎么样?\n\n═══ 第二部分:告知漏诊项目 ═══\n• 上次来检查的时候,胡航医生注意到您有缺失牙的情况。\n• 缺牙的地方如果不处理,旁边的牙齿可能会慢慢歪掉,影响咬合,吃东西也会不太舒服。\n• 趁现在牙槽骨条件还不错,早点处理效果更好,也避免将来多花功夫。\n• 这个情况,胡航医生也特别嘱咐我们提醒您一下,别忽略了,早点关注会更好。\n\n═══ 第三部分:复查建议 ═══\n• 如果方便的话您看最近有没有时间来院复查一下。\n• 让胡航医生帮您再仔细看看。\n• 复查检查约30分钟,了解缺失牙位目前状况。\n• 胡航医生【时间段1】和【时间段2】这两个时间段有空,您看哪个时间比较方便?\n\n═══ 第四部分:结束回访语 ═══\n**预约成功:**\n• 好的,那我们【具体预约时间】见。\n• 那不打扰您了,祝您生活愉快。\n\n**预约不成功:**\n• 好的,那我下个星期再跟您联系。\n• 好的那不打扰您了,祝您生活愉快。",
"api_type": "agent_chat",
"has_missed_diagnosis": false,
"patient_data_summary": {
"last_visit_time": "2025-06-30 00:00:00",
"last_clinic": "江苏瑞泰通善口腔学前街医院",
"last_doctor": "胡航",
"visit_count": 0
},
"callback_type": "success"
},
{
"patient_id": "TS0M008652",
"patient_name": "杜秋萍",
"age": 66,
"gender": "女",
"generation_time": "2025-08-05 14:28:01",
"callback_script": "```json\n{\n \"selected_diagnosis\": \"缺失牙\"\n}\n```\n═══ 第一部分:开场白 ═══\n• 您好,杜女士,我是瑞泰通善口腔的护士长小张。\n• 王程文医生特意交代我来关注您后续的情况。\n• 您自从去年6月检查后,口腔情况怎么样?\n\n═══ 第二部分:告知漏诊项目 ═══\n• 上次来检查的时候,王程文医生注意到您有缺失牙的情况。\n• 缺牙的地方如果不处理,旁边的牙齿可能会慢慢歪掉,时间一长,吃东西也会不太舒服。\n• 趁现在牙槽骨条件还不错,早点处理效果更好,也能保障咱们晚年的生活质量。\n• 王程文医生说,这个问题早点看看会比较安心。\n\n═══ 第三部分:复查建议 ═══\n• 如果方便的话您看最近有没有时间来院复查一下。\n• 让王程文医生帮您再仔细看看。\n• 复查检查约30分钟,了解缺失牙位目前状况。\n• 王程文医生【时间段1】和【时间段2】这两个时间段有空,您看哪个时间比较方便?\n\n═══ 第四部分:结束回访语 ═══\n预约成功:\n• 好的,那我们【具体预约时间】见。\n• 那不打扰您了,祝您生活愉快。\n预约不成功:\n• 好的,那我下个星期再跟您联系。\n• 好的那不打扰您了,祝您生活愉快。",
"api_type": "agent_chat",
"has_missed_diagnosis": false,
"patient_data_summary": {
"last_visit_time": "2025-06-30 00:00:00",
"last_clinic": "江苏瑞泰通善口腔学前街医院",
"last_doctor": "王程文",
"visit_count": 0
},
"callback_type": "success"
}
]
}
\ No newline at end of file
============================================================
++ /dev/null
============================================================
Dify平台回访话术批量生成结果
API类型: AGENT_CHAT
生成时间: 2025-07-31 16:10:56
总计处理: 1 个患者
成功生成: 1 个话术
生成失败: 0 个
============================================================
【1】患者: 迟鹏领 (TS0K064355)
年龄: 54岁, 性别: 男
最后就诊: 2025-06-30 00:00:00
就诊医生: 孙吉卿
就诊次数: 0次
----------------------------------------
回访话术:
好的,正在为您生成回访话术...
**逻辑判断与流程选择:**
1. **年龄判断**:患者年龄为54岁,大于13岁,执行“成人漏诊话术模板”。
2. **漏诊项优先级判断**:患者存在“缺失牙”和“牙槽骨吸收”两项漏诊。“缺失牙”(优先级③)高于“牙槽骨吸收”(优先级④)。
3. **最终方案**:本次话术将严格按照“成人漏诊话术模板”(4模块结构),并仅针对“缺失牙”这一项进行回访。
---
═══ 第一部分:开场白 ═══
• 您好,我是江苏瑞泰通善口腔学前街医院的回访专员。
• 孙吉卿医生特意交代我来关注您的后续情况。
• (如果是熟悉患者可说:孙吉卿医生上次还和我提起您呢)
• 您自从6月30号检查后,口腔情况怎么样?
═══ 第二部分:告知漏诊项目 ═══
• 上次来检查的时候,孙吉卿医生注意到您有缺失牙的情况。
• 缺牙的地方如果不处理,旁边的牙齿可能会慢慢歪掉,时间一长,吃东西也会不太舒服。
• 趁现在牙槽骨条件还不错,早点处理效果更好,也能更好地恢复咀嚼功能。
• 孙吉卿医生说,这个问题早点看看会比较安心。
═══ 第三部分:复查建议 ═══
• 建议您方便的时候来院复查一下。
• 让孙吉卿医生帮您再仔细看看。
• 复查检查大概需要【30分钟左右】的时间,主要是了解一下您缺牙位置目前的情况。
• 孙吉卿医生【时间段1】和【时间段2】这两个时间段有空,您看哪个时间比较方便?
═══ 第四部分:结束回访语 ═══
**预约成功:**
• 好的,那我们【具体预约时间】见。
• 那不打扰您了,祝您生活愉快。
**预约不成功:**
• 好的,那我下个星期再跟您联系。
• 那不打扰您了,祝您生活愉快。
============================================================
============================================================
++ /dev/null
============================================================
Dify平台回访话术批量生成结果
API类型: AGENT_CHAT
生成时间: 2025-07-31 16:24:11
总计处理: 1 个患者
成功生成: 1 个话术
生成失败: 0 个
============================================================
【1】患者: 迟鹏领 (TS0K064355)
年龄: 54岁, 性别: 男
最后就诊: 2025-06-30 00:00:00
就诊医生: 孙吉卿
就诊次数: 0次
----------------------------------------
回访话术:
好的,已收到您的指令。正在根据患者信息和漏诊项优先级进行逻辑判断。
* **年龄判断**: 患者年龄为54岁,大于13岁,执行“成人漏诊话术模板”。
* **漏诊项优先级判断**: 患者存在“缺失牙”和“牙槽骨吸收”两个漏诊项。根据优先级排序(缺失牙 > 牙槽骨吸收),本次话术将**仅针对“缺失牙”项目**进行,忽略“牙槽骨吸收”。
* **模板确认**: 使用“成人漏诊话术模板”(4模块结构)。
正在生成话术,请稍候。
---
═══ 第一部分:开场白 ═══
* 您好,我是江苏瑞泰通善口腔学前街医院的回访专员,请问是迟先生吗?
* 孙吉卿医生特意交代我来关注您的后续情况。
* (如果是熟悉患者可说:孙吉卿医生上次还和我提起您呢)
* 您自从6月30号检查后,口腔情况怎么样?
═══ 第二部分:告知漏诊项目 ═══
* 上次来检查的时候,孙吉卿医生注意到您有缺失牙的情况。
* 缺牙的地方如果不处理,旁边的牙齿可能会慢慢歪掉,时间久了还会影响吃东西。
* 其实早一点处理,比以后问题变得复杂了要省事省心很多。
* 这个情况,孙吉卿医生也特别嘱咐我们提醒您一下,别忽略了。
═══ 第三部分:复查建议 ═══
* 建议您方便的时候来院复查一下。
* 让孙吉卿医生帮您再仔细看看,了解一下缺牙位置目前的状况。
* 复查检查大概需要【30分钟】的时间。
* 孙吉卿医生【时间段1】和【时间段2】这两个时间段有空,您看哪个时间比较方便?
═══ 第四部分:结束回访语 ═══
**预约成功:**
* 好的,那我们【具体预约时间】见。
* 那不打扰您了,祝您生活愉快。
**预约不成功:**
* 好的,那我下个星期再跟您联系。
* 那不打扰您了,祝您生活愉快。
============================================================
============================================================
++ /dev/null
============================================================
Dify平台回访话术批量生成结果
API类型: AGENT_CHAT
生成时间: 2025-08-05 14:28:01
总计处理: 3 个患者
成功生成: 3 个话术
生成失败: 0 个
============================================================
【1】患者: 迟鹏领 (TS0K064355)
年龄: 54岁, 性别: 男
最后就诊: 2025-06-30 00:00:00
就诊医生: 孙吉卿
就诊次数: 0次
----------------------------------------
回访话术:
```json
{
"selected_diagnosis": "缺失牙"
}
```
═══ 第一部分:开场白 ═══
• 您好,我是瑞泰通善口腔的护士长小王。
• 迟先生,孙吉卿医生特意交代我来关注您的后续情况。
• 您自从2025年6月检查后,口腔情况怎么样?
═══ 第二部分:告知漏诊项目 ═══
• 上次来检查的时候,孙吉卿医生注意到您有缺失牙的情况。
• 缺牙的地方如果不处理,旁边的牙齿可能会慢慢歪掉,影响您吃东西。
• 趁现在早一点处理,比以后问题变得复杂时要省事也省心。
• 这个情况,孙吉卿医生也特别嘱咐我们提醒您一下。
═══ 第三部分:复查建议 ═══
• 如果方便的话您看最近有没有时间来院复查一下。
• 让孙吉卿医生帮您再仔细看看。
• 【复查检查约30分钟,了解缺失牙位目前状况】
• 孙吉卿医生【时间段1】和【时间段2】这两个时间段有空,您看哪个时间比较方便?
═══ 第四部分:结束回访语 ═══
[预约成功]
• 好的,那我们【具体预约时间】见。
• 那不打扰您了,祝您生活愉快。
[预约不成功]
• 好的,那我下个星期再跟您联系。
• 好的那不打扰您了,祝您生活愉快。
============================================================
【2】患者: 钱明艳 (TS0M008666)
年龄: 52岁, 性别: 女
最后就诊: 2025-06-30 00:00:00
就诊医生: 胡航
就诊次数: 0次
----------------------------------------
回访话术:
```json
{
"selected_diagnosis": "缺失牙"
}
```
═══ 第一部分:开场白 ═══
• 您好,钱女士,我是瑞泰通善口腔的护士长小王。
• 胡航医生特意交代我来关注您的后续情况。
• 您自从去年6月30号检查后,口腔情况怎么样?
═══ 第二部分:告知漏诊项目 ═══
• 上次来检查的时候,胡航医生注意到您有缺失牙的情况。
• 缺牙的地方如果不处理,旁边的牙齿可能会慢慢歪掉,影响咬合,吃东西也会不太舒服。
• 趁现在牙槽骨条件还不错,早点处理效果更好,也避免将来多花功夫。
• 这个情况,胡航医生也特别嘱咐我们提醒您一下,别忽略了,早点关注会更好。
═══ 第三部分:复查建议 ═══
• 如果方便的话您看最近有没有时间来院复查一下。
• 让胡航医生帮您再仔细看看。
• 复查检查约30分钟,了解缺失牙位目前状况。
• 胡航医生【时间段1】和【时间段2】这两个时间段有空,您看哪个时间比较方便?
═══ 第四部分:结束回访语 ═══
**预约成功:**
• 好的,那我们【具体预约时间】见。
• 那不打扰您了,祝您生活愉快。
**预约不成功:**
• 好的,那我下个星期再跟您联系。
• 好的那不打扰您了,祝您生活愉快。
============================================================
【3】患者: 杜秋萍 (TS0M008652)
年龄: 66岁, 性别: 女
最后就诊: 2025-06-30 00:00:00
就诊医生: 王程文
就诊次数: 0次
----------------------------------------
回访话术:
```json
{
"selected_diagnosis": "缺失牙"
}
```
═══ 第一部分:开场白 ═══
• 您好,杜女士,我是瑞泰通善口腔的护士长小张。
• 王程文医生特意交代我来关注您后续的情况。
• 您自从去年6月检查后,口腔情况怎么样?
═══ 第二部分:告知漏诊项目 ═══
• 上次来检查的时候,王程文医生注意到您有缺失牙的情况。
• 缺牙的地方如果不处理,旁边的牙齿可能会慢慢歪掉,时间一长,吃东西也会不太舒服。
• 趁现在牙槽骨条件还不错,早点处理效果更好,也能保障咱们晚年的生活质量。
• 王程文医生说,这个问题早点看看会比较安心。
═══ 第三部分:复查建议 ═══
• 如果方便的话您看最近有没有时间来院复查一下。
• 让王程文医生帮您再仔细看看。
• 复查检查约30分钟,了解缺失牙位目前状况。
• 王程文医生【时间段1】和【时间段2】这两个时间段有空,您看哪个时间比较方便?
═══ 第四部分:结束回访语 ═══
预约成功:
• 好的,那我们【具体预约时间】见。
• 那不打扰您了,祝您生活愉快。
预约不成功:
• 好的,那我下个星期再跟您联系。
• 好的那不打扰您了,祝您生活愉快。
============================================================
============================================================
++ /dev/null
============================================================
Dify平台回访话术批量生成结果
API类型: AGENT_CHAT
生成时间: 2025-08-05 14:32:22
总计处理: 5 个患者
成功生成: 5 个话术
生成失败: 0 个
============================================================
【1】患者: 迟鹏领 (TS0K064355)
年龄: 54岁, 性别: 男
最后就诊: 2025-06-30 00:00:00
就诊医生: 孙吉卿
就诊次数: 0次
----------------------------------------
回访话术:
```json
{
"selected_diagnosis": "缺失牙"
}
```
═══ 第一部分:开场白 ═══
• 您好,我是瑞泰通善口腔的护士长小张。
• 迟先生,您好。
• 孙吉卿医生特意交代我来关注您的后续情况。
• 您自从2025年6月30号检查后,口腔情况怎么样?
═══ 第二部分:告知漏诊项目 ═══
• 上次来检查的时候,孙吉卿医生注意到您有缺失牙的情况。
• 缺牙的地方如果不处理,旁边的牙齿可能会慢慢歪掉,对面的牙齿还可能伸长出来。
• 其实早一点处理,比以后复杂时省事也省心。
• 这个情况,孙吉卿医生也特别嘱咐我们提醒您一下。
═══ 第三部分:复查建议 ═══
• 如果方便的话您看最近有没有时间来院复查一下。
• 让孙吉卿医生帮您再仔细看看。
• 【复查检查约30分钟,了解缺失牙位目前状况】
• 孙吉卿医生【时间段1】和【时间段2】这两个时间段有空,您看哪个时间比较方便?
═══ 第四部分:结束回访语 ═══
**预约成功:**
• 好的,那我们【具体预约时间】见。
• 那不打扰您了,祝您生活愉快。
**预约不成功:**
• 好的,那我下个星期再跟您联系。
• 好的那不打扰您了,祝您生活愉快。
============================================================
【2】患者: 钱明艳 (TS0M008666)
年龄: 52岁, 性别: 女
最后就诊: 2025-06-30 00:00:00
就诊医生: 胡航
就诊次数: 0次
----------------------------------------
回访话术:
```json
{
"selected_diagnosis": "缺失牙"
}
```
═══ 第一部分:开场白 ═══
• 您好,我是瑞泰通善口腔的医生助理小王。
• 请问是钱女士吗?胡航医生特意交代我来关注您的后续情况。
• 您自从6月30号检查后,口腔情况怎么样?
═══ 第二部分:告知漏诊项目 ═══
• 上次来检查的时候,胡航医生注意到您有缺失牙的情况。
• 缺牙的地方如果不处理,旁边的牙齿可能会慢慢歪掉,对面的牙也会伸长,影响到正常的咬合。
• 趁现在牙槽骨条件还不错,早点关注,处理起来效果也会更好。
• 所以胡航医生也特别嘱咐我们提醒您一下,这个问题早点看看会比较安心。
═══ 第三部分:复查建议 ═══
• 如果方便的话您看最近有没有时间来院复查一下。
• 让胡航医生帮您再仔细看看。
• 【复查检查约30分钟,了解缺失牙位目前状况】
• 胡航医生【时间段1】和【时间段2】这两个时间段有空,您看哪个时间比较方便?
═══ 第四部分:结束回访语 ═══
预约成功:
• 好的,那我们【具体预约时间】见。
• 那不打扰您了,祝您生活愉快。
预约不成功:
• 好的,那我下个星期再跟您联系。
• 好的那不打扰您了,祝您生活愉快。
============================================================
【3】患者: 杜秋萍 (TS0M008652)
年龄: 66岁, 性别: 女
最后就诊: 2025-06-30 00:00:00
就诊医生: 王程文
就诊次数: 0次
----------------------------------------
回访话术:
```json
{
"selected_diagnosis": "缺失牙"
}
```
═══ 第一部分:开场白 ═══
• 您好,杜女士,我是瑞泰通善口腔的护士长小王。
• 王程文医生特意交代我来关注您后续的口腔情况。
• 您自从2025年6月那次检查后,口腔情况怎么样?
═══ 第二部分:告知漏诊项目 ═══
• 上次来检查的时候,王程文医生注意到您有缺失牙的情况。
• 缺牙的地方如果不处理,旁边的牙齿可能会慢慢歪掉,对面的牙齿也可能伸长出来,影响咬合。
• 趁现在牙槽骨条件还不错,早点关注这个问题,不仅能保护好邻牙,吃东西也能更踏实。
• 所以王程文医生也特别嘱咐我们提醒您一下,这个问题别忽略了,早点关注会更好。
═══ 第三部分:复查建议 ═══
• 如果方便的话您看最近有没有时间来院复查一下。
• 让王程文医生帮您再仔细看看。
• 【复查检查约30分钟,了解缺失牙位目前状况】
• 王程文医生【时间段1】和【时间段2】这两个时间段有空,您看哪个时间比较方便?
═══ 第四部分:结束回访语 ═══
预约成功:
• 好的,那我们【具体预约时间】见。
• 那不打扰您了,祝您生活愉快。
预约不成功:
• 好的,那我下个星期再跟您联系。
• 好的那不打扰您了,祝您生活愉快。
============================================================
【4】患者: 周蓉 (TS0K065136)
年龄: 38岁, 性别: 女
最后就诊: 2025-06-30 00:00:00
就诊医生: 沈佳丽
就诊次数: 0次
----------------------------------------
回访话术:
```json
{
"selected_diagnosis": "牙槽骨吸收"
}
```
═══ 第一部分:开场白 ═══
• 您好,我是瑞泰通善口腔的护士长小李。
• 周女士您好,沈佳丽医生特意交代我来关注您的后续情况。
• 您自从去年6月30号检查后,口腔情况怎么样?
═══ 第二部分:告知漏诊项目 ═══
• 上次来检查的时候,沈佳丽医生注意到您有牙槽骨吸收的情况。
• 这个问题如果一直拖着,可能会出现牙齿松动、牙缝变大的情况,影响您吃东西。
• 趁现在问题还不算严重,早点关注,把情况稳住,比以后复杂了再处理要省心很多。
• 这个情况,沈佳丽医生也特别嘱咐我们提醒您一下,别忽略了。
═══ 第三部分:复查建议 ═══
• 如果方便的话您看最近有没有时间来院复查一下。
• 让沈佳丽医生帮您再仔细看看,评估一下现状。
• 复查检查约30-45分钟,需要仔细检查牙周健康状况。
• 沈佳丽医生【时间段1】和【时间段2】这两个时间段有空,您看哪个时间比较方便?
═══ 第四部分:结束回访语 ═══
**预约成功:**
• 好的,那我们【具体预约时间】见。
• 那不打扰您了,祝您生活愉快。
**预约不成功:**
• 好的,那我下个星期再跟您联系。
• 好的那不打扰您了,祝您生活愉快。
============================================================
【5】患者: 陆志毅 (TS0M008662)
年龄: 64岁, 性别: 男
最后就诊: 2025-06-30 00:00:00
就诊医生: 蒋亚萍
就诊次数: 0次
----------------------------------------
回访话术:
```json
{
"selected_diagnosis": "缺失牙"
}
```
═══ 第一部分:开场白 ═══
• 您好,陆先生,我是瑞泰通善口腔的健康助理小王。
• 蒋亚萍医生特意交代我来关注您的后续情况。
• 您自从2025年6月检查后,口腔情况怎么样?
═══ 第二部分:告知漏诊项目 ═══
• 上次来检查的时候,蒋亚萍医生注意到您有缺失牙的情况。
• 缺牙的地方如果不处理,旁边的牙齿可能会慢慢歪掉,时间一长,吃东西也会不太舒服。
• 趁现在早点关注,及时修复,既能保护邻牙,也能让您以后吃东西更省心。
• 这个情况,蒋亚萍医生也特别嘱咐我们提醒您一下,早点看看会比较安心。
═══ 第三部分:复查建议 ═══
• 如果方便的话您看最近有没有时间来院复查一下。
• 让蒋亚萍医生帮您再仔细看看。
• 复查检查约30分钟,了解缺失牙位目前状况。
• 蒋亚萍医生【时间段1】和【时间段2】这两个时间段有空,您看哪个时间比较方便?
═══ 第四部分:结束回访语 ═══
预约成功:
• 好的,那我们【具体预约时间】见。
• 那不打扰您了,祝您生活愉快。
预约不成功:
• 好的,那我下个星期再跟您联系。
• 好的那不打扰您了,祝您生活愉快。
============================================================
This source diff could not be displayed because it is too large. You can view the blob instead.
{
++ /dev/null
{
"generation_info": {
"platform": "Dify",
"api_type": "agent_chat",
"generation_time": "2025-07-29 15:31:21",
"total_processed": 10,
"successful_callbacks": 10,
"failed_callbacks": 0,
"success_rate": "100.0%"
},
"statistics": {
"age_distribution": {
"儿童": 2,
"青少年": 0,
"青年": 2,
"中年": 4,
"老年": 2
},
"gender_distribution": {
"男": 5,
"女": 5,
"未知": 0
},
"missed_diagnosis_distribution": {
"有漏诊": 0,
"无漏诊": 10
}
},
"summary": {
"most_common_age_group": "中年",
"gender_ratio": "男:5, 女:5, 未知:0",
"missed_diagnosis_ratio": "有漏诊:0, 无漏诊:10"
}
}
\ No newline at end of file
{
++ /dev/null
{
"generation_info": {
"platform": "Dify",
"api_type": "agent_chat",
"generation_time": "2025-07-31 15:30:42",
"total_processed": 10,
"successful_callbacks": 10,
"failed_callbacks": 0,
"success_rate": "100.0%"
},
"statistics": {
"age_distribution": {
"儿童": 0,
"青少年": 0,
"青年": 2,
"中年": 5,
"老年": 3
},
"gender_distribution": {
"男": 3,
"女": 7,
"未知": 0
},
"missed_diagnosis_distribution": {
"有漏诊": 0,
"无漏诊": 10
}
},
"summary": {
"most_common_age_group": "中年",
"gender_ratio": "男:3, 女:7, 未知:0",
"missed_diagnosis_ratio": "有漏诊:0, 无漏诊:10"
}
}
\ No newline at end of file
{
++ /dev/null
{
"generation_info": {
"platform": "Dify",
"api_type": "agent_chat",
"generation_time": "2025-07-31 16:04:58",
"total_processed": 5,
"successful_callbacks": 5,
"failed_callbacks": 0,
"success_rate": "100.0%"
},
"statistics": {
"age_distribution": {
"儿童": 0,
"青少年": 0,
"青年": 0,
"中年": 3,
"老年": 2
},
"gender_distribution": {
"男": 2,
"女": 3,
"未知": 0
},
"missed_diagnosis_distribution": {
"有漏诊": 0,
"无漏诊": 5
}
},
"summary": {
"most_common_age_group": "中年",
"gender_ratio": "男:2, 女:3, 未知:0",
"missed_diagnosis_ratio": "有漏诊:0, 无漏诊:5"
}
}
\ No newline at end of file
{
++ /dev/null
{
"generation_info": {
"platform": "Dify",
"api_type": "agent_chat",
"generation_time": "2025-07-31 16:10:56",
"total_processed": 1,
"successful_callbacks": 1,
"failed_callbacks": 0,
"success_rate": "100.0%"
},
"statistics": {
"age_distribution": {
"儿童": 0,
"青少年": 0,
"青年": 0,
"中年": 1,
"老年": 0
},
"gender_distribution": {
"男": 1,
"女": 0,
"未知": 0
},
"missed_diagnosis_distribution": {
"有漏诊": 0,
"无漏诊": 1
}
},
"summary": {
"most_common_age_group": "中年",
"gender_ratio": "男:1, 女:0, 未知:0",
"missed_diagnosis_ratio": "有漏诊:0, 无漏诊:1"
}
}
\ No newline at end of file
{
++ /dev/null
{
"generation_info": {
"platform": "Dify",
"api_type": "agent_chat",
"generation_time": "2025-07-31 16:24:11",
"total_processed": 1,
"successful_callbacks": 1,
"failed_callbacks": 0,
"success_rate": "100.0%"
},
"statistics": {
"age_distribution": {
"儿童": 0,
"青少年": 0,
"青年": 0,
"中年": 1,
"老年": 0
},
"gender_distribution": {
"男": 1,
"女": 0,
"未知": 0
},
"missed_diagnosis_distribution": {
"有漏诊": 0,
"无漏诊": 1
}
},
"summary": {
"most_common_age_group": "中年",
"gender_ratio": "男:1, 女:0, 未知:0",
"missed_diagnosis_ratio": "有漏诊:0, 无漏诊:1"
}
}
\ No newline at end of file
{
++ /dev/null
{
"generation_info": {
"platform": "Dify",
"api_type": "agent_chat",
"generation_time": "2025-07-31 16:36:02",
"total_processed": 13,
"successful_callbacks": 13,
"failed_callbacks": 0,
"success_rate": "100.0%"
},
"statistics": {
"age_distribution": {
"儿童": 2,
"青少年": 0,
"青年": 2,
"中年": 6,
"老年": 3
},
"gender_distribution": {
"男": 6,
"女": 7,
"未知": 0
},
"missed_diagnosis_distribution": {
"有漏诊": 0,
"无漏诊": 13
}
},
"summary": {
"most_common_age_group": "中年",
"gender_ratio": "男:6, 女:7, 未知:0",
"missed_diagnosis_ratio": "有漏诊:0, 无漏诊:13"
}
}
\ No newline at end of file
{
++ /dev/null
{
"generation_info": {
"platform": "Dify",
"api_type": "agent_chat",
"generation_time": "2025-07-31 17:07:01",
"total_processed": 12,
"successful_callbacks": 12,
"failed_callbacks": 0,
"success_rate": "100.0%"
},
"statistics": {
"age_distribution": {
"儿童": 2,
"青少年": 0,
"青年": 2,
"中年": 5,
"老年": 3
},
"gender_distribution": {
"男": 5,
"女": 7,
"未知": 0
},
"missed_diagnosis_distribution": {
"有漏诊": 0,
"无漏诊": 12
}
},
"summary": {
"most_common_age_group": "中年",
"gender_ratio": "男:5, 女:7, 未知:0",
"missed_diagnosis_ratio": "有漏诊:0, 无漏诊:12"
}
}
\ No newline at end of file
{
++ /dev/null
{
"generation_info": {
"platform": "Dify",
"api_type": "agent_chat",
"generation_time": "2025-08-04 12:08:25",
"total_processed": 13,
"successful_callbacks": 13,
"failed_callbacks": 0,
"success_rate": "100.0%"
},
"statistics": {
"age_distribution": {
"儿童": 2,
"青少年": 0,
"青年": 2,
"中年": 6,
"老年": 3
},
"gender_distribution": {
"男": 6,
"女": 7,
"未知": 0
},
"missed_diagnosis_distribution": {
"有漏诊": 0,
"无漏诊": 13
}
},
"summary": {
"most_common_age_group": "中年",
"gender_ratio": "男:6, 女:7, 未知:0",
"missed_diagnosis_ratio": "有漏诊:0, 无漏诊:13"
}
}
\ No newline at end of file
{
++ /dev/null
{
"generation_info": {
"platform": "Dify",
"api_type": "agent_chat",
"generation_time": "2025-08-05 14:28:01",
"total_processed": 3,
"successful_callbacks": 3,
"failed_callbacks": 0,
"success_rate": "100.0%"
},
"statistics": {
"age_distribution": {
"儿童": 0,
"青少年": 0,
"青年": 0,
"中年": 2,
"老年": 1
},
"gender_distribution": {
"男": 1,
"女": 2,
"未知": 0
},
"missed_diagnosis_distribution": {
"有漏诊": 0,
"无漏诊": 3
}
},
"summary": {
"most_common_age_group": "中年",
"gender_ratio": "男:1, 女:2, 未知:0",
"missed_diagnosis_ratio": "有漏诊:0, 无漏诊:3"
}
}
\ No newline at end of file
{
++ /dev/null
{
"generation_info": {
"platform": "Dify",
"api_type": "agent_chat",
"generation_time": "2025-08-05 14:32:22",
"total_processed": 5,
"successful_callbacks": 5,
"failed_callbacks": 0,
"success_rate": "100.0%"
},
"statistics": {
"age_distribution": {
"儿童": 0,
"青少年": 0,
"青年": 0,
"中年": 3,
"老年": 2
},
"gender_distribution": {
"男": 2,
"女": 3,
"未知": 0
},
"missed_diagnosis_distribution": {
"有漏诊": 0,
"无漏诊": 5
}
},
"summary": {
"most_common_age_group": "中年",
"gender_ratio": "男:2, 女:3, 未知:0",
"missed_diagnosis_ratio": "有漏诊:0, 无漏诊:5"
}
}
\ No newline at end of file
{
++ /dev/null
{
"generation_info": {
"platform": "Dify",
"api_type": "agent_chat",
"generation_time": "2025-08-05 22:57:21",
"total_processed": 765,
"successful_callbacks": 764,
"failed_callbacks": 1,
"success_rate": "99.9%"
},
"statistics": {
"age_distribution": {
"儿童": 176,
"青少年": 21,
"青年": 167,
"中年": 232,
"老年": 168
},
"gender_distribution": {
"男": 302,
"女": 462,
"未知": 0
},
"missed_diagnosis_distribution": {
"有漏诊": 0,
"无漏诊": 764
}
},
"summary": {
"most_common_age_group": "中年",
"gender_ratio": "男:302, 女:462, 未知:0",
"missed_diagnosis_ratio": "有漏诊:0, 无漏诊:764"
}
}
\ No newline at end of file
# Docker Compose配置文件 - 患者画像回访话术系统(带完整认证功能)
services:
# MySQL数据库服务
mysql:
image: mysql:8.0
container_name: patient_callback_mysql_auth
environment:
MYSQL_ROOT_PASSWORD: callback_system_2024
MYSQL_DATABASE: callback_system
MYSQL_USER: callback_user
MYSQL_PASSWORD: callback_pass_2024
MYSQL_ROOT_HOST: '%'
volumes:
- mysql_data:/var/lib/mysql
- ./init.sql:/docker-entrypoint-initdb.d/init.sql:ro
ports:
- "3308:3306"
networks:
- patient_callback_network
command: --default-authentication-plugin=mysql_native_password
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
timeout: 20s
retries: 10
# 患者画像回访话术系统(带完整认证功能)
patient_callback_app:
build: .
container_name: patient_callback_app_auth
environment:
# 数据库配置
DB_HOST: mysql
DB_PORT: 3306
DB_USER: callback_user
DB_PASSWORD: callback_pass_2024
DB_NAME: callback_system
# Flask配置
FLASK_ENV: production
FLASK_DEBUG: 0
# 认证系统配置
SERVER_TYPE: auth_system
SECRET_KEY: dev_secret_key_2024
# 用户权限配置
ENABLE_AUTH: "true"
ENABLE_CLINIC_ACCESS_CONTROL: "true"
volumes:
- ./patient_profiles:/app/patient_profiles
- ./progress_saves:/app/progress_saves
- ./dify_callback_results:/app/dify_callback_results
- ./诊所患者json:/app/诊所患者json
- ./诊所患者目录:/app/诊所患者目录
- ./clinic_config.py:/app/clinic_config.py:ro
- ./auth_system.py:/app/auth_system.py:ro
ports:
- "4002:5000"
networks:
- patient_callback_network
depends_on:
mysql:
condition: service_healthy
restart: unless-stopped
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:5000/api/health"]
interval: 30s
timeout: 10s
retries: 3
# 回访API服务(备用)
callback_api:
build: .
container_name: patient_callback_api
environment:
DB_HOST: mysql
DB_PORT: 3306
DB_USER: callback_user
DB_PASSWORD: callback_pass_2024
DB_NAME: callback_system
SERVER_TYPE: callback_api
volumes:
- ./patient_profiles:/app/patient_profiles
ports:
- "5003:5000"
networks:
- patient_callback_network
depends_on:
mysql:
condition: service_healthy
restart: unless-stopped
command: ["python", "callback_api_server.py"]
profiles:
- backup
volumes:
mysql_data:
driver: local
networks:
patient_callback_network:
driver: bridge
\ No newline at end of file
# Docker Compose配置文件 - 患者画像回访话术系统(本地MySQL版本)
services:
# MySQL数据库服务(使用本地构建)
mysql:
build:
context: .
dockerfile: Dockerfile.mysql
container_name: patient_callback_mysql
environment:
MYSQL_ROOT_PASSWORD: callback_system_2024
MYSQL_DATABASE: callback_system
MYSQL_USER: callback_user
MYSQL_PASSWORD: callback_pass_2024
MYSQL_ROOT_HOST: '%'
volumes:
- mysql_data:/var/lib/mysql
- ./init.sql:/docker-entrypoint-initdb.d/init.sql:ro
ports:
- "3306:3306"
networks:
- patient_callback_network
command: --default-authentication-plugin=mysql_native_password
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
timeout: 20s
retries: 10
# 患者画像回访话术系统
patient_callback_app:
build: .
container_name: patient_callback_app
environment:
# 数据库配置
DB_HOST: mysql
DB_PORT: 3306
DB_USER: callback_user
DB_PASSWORD: callback_pass_2024
DB_NAME: callback_system
# Flask配置
FLASK_ENV: production
FLASK_DEBUG: 0
volumes:
- ./patient_profiles:/app/patient_profiles
- ./progress_saves:/app/progress_saves
- ./dify_callback_results:/app/dify_callback_results
- ./诊所患者json:/app/诊所患者json
- ./诊所患者目录:/app/诊所患者目录
ports:
- "5000:5000"
networks:
- patient_callback_network
depends_on:
mysql:
condition: service_healthy
restart: unless-stopped
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:5000/"]
interval: 30s
timeout: 10s
retries: 3
volumes:
mysql_data:
driver: local
networks:
patient_callback_network:
driver: bridge
\ No newline at end of file
# Docker Compose配置文件 - 患者画像回访话术系统(SQLite版本)
services:
# 患者画像回访话术系统(使用SQLite)
patient_callback_app:
build: .
container_name: patient_callback_app
environment:
# 使用SQLite数据库
DATABASE_TYPE: sqlite
DATABASE_URL: sqlite:///app/callback_records.db
# Flask配置
FLASK_ENV: production
FLASK_DEBUG: 0
volumes:
- ./patient_profiles:/app/patient_profiles
- ./progress_saves:/app/progress_saves
- ./dify_callback_results:/app/dify_callback_results
- ./诊所患者json:/app/诊所患者json
- ./诊所患者目录:/app/诊所患者目录
- ./callback_records.db:/app/callback_records.db
ports:
- "5000:5000"
restart: unless-stopped
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:5000/"]
interval: 30s
timeout: 10s
retries: 3
volumes:
# 不需要MySQL数据卷
\ No newline at end of file
# 开发环境Docker Compose覆盖配置
services:
patient_callback_app:
environment:
FLASK_ENV: development
FLASK_DEBUG: 1
volumes:
# 开发模式下挂载源代码,支持热重载
- .:/app
- /app/__pycache__
command: ["python", "start_docker.py"]
mysql:
ports:
# 开发环境暴露MySQL端口,便于调试
- "3306:3306"
environment:
# 开发环境使用简单密码
MYSQL_ROOT_PASSWORD: dev_password_123
MYSQL_PASSWORD: dev_password_123
\ No newline at end of file
This source diff could not be displayed because it is too large. You can view the blob instead.
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
数据库迁移命令行工具
用法:
python migrate.py create "add_user_email" "添加用户邮箱字段"
python migrate.py status
python migrate.py migrate [--dry-run] [--target=version]
python migrate.py rollback version
python migrate.py validate
"""
import sys
import argparse
import json
from database_migration_manager import DatabaseMigrationManager
from database_config import get_database_config
def create_migration(manager: DatabaseMigrationManager, name: str, description: str = ""):
"""创建新迁移"""
try:
filepath = manager.create_migration(name, description)
print(f"✅ 迁移文件已创建: {filepath}")
print(f"📝 请编辑文件添加SQL语句")
return True
except Exception as e:
print(f"❌ 创建迁移失败: {e}")
return False
def show_status(manager: DatabaseMigrationManager):
"""显示迁移状态"""
try:
status = manager.get_migration_status()
print("📊 数据库迁移状态")
print("=" * 50)
print(f"✅ 已执行: {status['executed_count']}")
print(f"❌ 失败: {status['failed_count']}")
print(f"🔄 已回滚: {status['rolled_back_count']}")
print(f"⏳ 待执行: {status['pending_count']}")
print()
if status['pending_migrations']:
print("📋 待执行迁移:")
for migration in status['pending_migrations']:
print(f" • {migration}")
print()
if status['history']:
print("📜 迁移历史 (最近10条):")
for item in status['history'][:10]:
status_icon = {
'SUCCESS': '✅',
'FAILED': '❌',
'ROLLED_BACK': '🔄'
}.get(item['status'], '❓')
print(f" {status_icon} {item['version']} - {item['filename']}")
print(f" 执行时间: {item['executed_at']}")
if item['execution_time_ms']:
print(f" 耗时: {item['execution_time_ms']}ms")
if item['error_message']:
print(f" 错误: {item['error_message'][:100]}...")
print()
return True
except Exception as e:
print(f"❌ 获取状态失败: {e}")
return False
def run_migrations(manager: DatabaseMigrationManager, target_version: str = None, dry_run: bool = False):
"""执行迁移"""
try:
if dry_run:
print("🔍 试运行模式 - 不会实际执行迁移")
success = manager.migrate(target_version, dry_run)
if success:
print("🎉 迁移执行完成!")
else:
print("⚠️ 迁移执行过程中出现错误")
return success
except Exception as e:
print(f"❌ 执行迁移失败: {e}")
return False
def rollback_migration(manager: DatabaseMigrationManager, version: str):
"""回滚迁移"""
try:
print(f"🔄 准备回滚迁移版本: {version}")
print("⚠️ 警告: 回滚操作可能导致数据丢失!")
confirm = input("确认继续? (yes/no): ").lower().strip()
if confirm not in ['yes', 'y']:
print("❌ 回滚操作已取消")
return False
success = manager.rollback_migration(version)
if success:
print("✅ 迁移回滚完成!")
else:
print("❌ 迁移回滚失败")
return success
except Exception as e:
print(f"❌ 回滚迁移失败: {e}")
return False
def validate_migrations(manager: DatabaseMigrationManager):
"""验证迁移文件"""
try:
success = manager.validate_migrations()
if success:
print("✅ 所有迁移文件验证通过")
else:
print("⚠️ 部分迁移文件验证失败")
return success
except Exception as e:
print(f"❌ 验证迁移文件失败: {e}")
return False
def main():
"""主函数"""
parser = argparse.ArgumentParser(description='数据库迁移管理工具')
subparsers = parser.add_subparsers(dest='command', help='可用命令')
# create 命令
create_parser = subparsers.add_parser('create', help='创建新迁移')
create_parser.add_argument('name', help='迁移名称')
create_parser.add_argument('description', nargs='?', default='', help='迁移描述')
# status 命令
subparsers.add_parser('status', help='显示迁移状态')
# migrate 命令
migrate_parser = subparsers.add_parser('migrate', help='执行迁移')
migrate_parser.add_argument('--dry-run', action='store_true', help='试运行模式')
migrate_parser.add_argument('--target', help='目标版本')
# rollback 命令
rollback_parser = subparsers.add_parser('rollback', help='回滚迁移')
rollback_parser.add_argument('version', help='要回滚的版本号')
# validate 命令
subparsers.add_parser('validate', help='验证迁移文件')
args = parser.parse_args()
if not args.command:
parser.print_help()
return 1
try:
# 获取数据库配置
db_config = get_database_config()
manager = DatabaseMigrationManager(db_config)
# 执行对应命令
if args.command == 'create':
success = create_migration(manager, args.name, args.description)
elif args.command == 'status':
success = show_status(manager)
elif args.command == 'migrate':
success = run_migrations(manager, args.target, args.dry_run)
elif args.command == 'rollback':
success = rollback_migration(manager, args.version)
elif args.command == 'validate':
success = validate_migrations(manager)
else:
print(f"❌ 未知命令: {args.command}")
return 1
return 0 if success else 1
except Exception as e:
print(f"❌ 执行失败: {e}")
return 1
if __name__ == '__main__':
sys.exit(main())
-- Migration: Initial Schema
-- Version: 20250101_000000
-- Description: 创建初始数据库结构
-- Created: 2025-01-01 00:00:00
-- ==========================================
-- UP Migration (执行迁移)
-- ==========================================
-- 创建用户表
CREATE TABLE IF NOT EXISTS users (
id INT AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(50) NOT NULL UNIQUE,
password_hash VARCHAR(255) NOT NULL,
salt VARCHAR(32) NOT NULL,
role ENUM('admin', 'clinic_user') NOT NULL DEFAULT 'clinic_user',
clinic_id VARCHAR(50),
is_active BOOLEAN DEFAULT TRUE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_username (username),
INDEX idx_clinic_id (clinic_id),
INDEX idx_role (role)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- 创建患者表
CREATE TABLE IF NOT EXISTS patients (
id INT AUTO_INCREMENT PRIMARY KEY,
case_number VARCHAR(50) NOT NULL UNIQUE,
name VARCHAR(100) NOT NULL,
age INT,
gender ENUM('男', '女', '未知') DEFAULT '未知',
phone VARCHAR(20),
clinic_name VARCHAR(100),
last_visit_date DATE,
last_doctor VARCHAR(100),
diagnosis TEXT,
missed_diagnosis TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_case_number (case_number),
INDEX idx_name (name),
INDEX idx_clinic_name (clinic_name),
INDEX idx_last_visit_date (last_visit_date)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- 创建回访记录表
CREATE TABLE IF NOT EXISTS callback_records (
id INT AUTO_INCREMENT PRIMARY KEY,
case_number VARCHAR(50) NOT NULL,
callback_methods TEXT,
callback_result VARCHAR(50),
callback_record TEXT,
operator VARCHAR(100),
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
update_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_case_number (case_number),
INDEX idx_callback_result (callback_result),
INDEX idx_operator (operator),
INDEX idx_create_time (create_time),
FOREIGN KEY (case_number) REFERENCES patients(case_number) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- ==========================================
-- DOWN Migration (回滚迁移)
-- ==========================================
-- 删除回访记录表
DROP TABLE IF EXISTS callback_records;
-- 删除患者表
DROP TABLE IF EXISTS patients;
-- 删除用户表
DROP TABLE IF EXISTS users;
-- ==========================================
-- 验证脚本 (可选)
-- ==========================================
-- 验证表是否创建成功
-- SELECT COUNT(*) FROM information_schema.tables
-- WHERE table_schema = DATABASE() AND table_name IN ('users', 'patients', 'callback_records');
-- Migration: Add User Email
-- Version: 20250102_120000
-- Description: 为用户表添加邮箱字段和相关索引
-- Created: 2025-01-02 12:00:00
-- ==========================================
-- UP Migration (执行迁移)
-- ==========================================
-- 添加邮箱字段
ALTER TABLE users
ADD COLUMN email VARCHAR(255) NULL AFTER username;
-- 添加邮箱唯一索引
ALTER TABLE users
ADD UNIQUE INDEX idx_users_email (email);
-- 添加邮箱验证状态字段
ALTER TABLE users
ADD COLUMN email_verified BOOLEAN DEFAULT FALSE AFTER email;
-- 添加邮箱验证时间字段
ALTER TABLE users
ADD COLUMN email_verified_at TIMESTAMP NULL AFTER email_verified;
-- ==========================================
-- DOWN Migration (回滚迁移)
-- ==========================================
-- 删除邮箱验证时间字段
ALTER TABLE users
DROP COLUMN email_verified_at;
-- 删除邮箱验证状态字段
ALTER TABLE users
DROP COLUMN email_verified;
-- 删除邮箱索引
ALTER TABLE users
DROP INDEX idx_users_email;
-- 删除邮箱字段
ALTER TABLE users
DROP COLUMN email;
-- ==========================================
-- 验证脚本 (可选)
-- ==========================================
-- 验证邮箱字段是否添加成功
-- SELECT COUNT(*) FROM information_schema.columns
-- WHERE table_schema = DATABASE()
-- AND table_name = 'users'
-- AND column_name IN ('email', 'email_verified', 'email_verified_at');
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
迁移系统设置脚本
用于从旧的数据库管理方式迁移到新的迁移管理系统
"""
import os
import shutil
import subprocess
from datetime import datetime
def setup_migration_system():
"""设置迁移系统"""
print("🚀 开始设置数据库迁移管理系统...")
# 1. 创建迁移目录
print("📁 创建迁移目录...")
os.makedirs("migrations", exist_ok=True)
print("✅ 迁移目录创建完成")
# 2. 备份现有SQL文件
print("📦 备份现有SQL文件...")
sql_files = [
"init.sql",
"create_users.sql",
"create_clinic_users.sql",
"fix_admin_user.sql",
"fix_passwords.sql",
"update_password_hashes.sql",
"update_users_with_salt.sql",
"complete_hashes.sql"
]
backup_dir = f"sql_backup_{datetime.now().strftime('%Y%m%d_%H%M%S')}"
os.makedirs(backup_dir, exist_ok=True)
for sql_file in sql_files:
if os.path.exists(sql_file):
shutil.copy2(sql_file, backup_dir)
print(f" ✅ 备份: {sql_file}")
print(f"📦 SQL文件备份完成: {backup_dir}")
# 3. 设置权限
print("🔧 设置脚本权限...")
scripts = ["migrate.py", "deploy_scripts/deploy_with_migration.sh"]
for script in scripts:
if os.path.exists(script):
os.chmod(script, 0o755)
print(f" ✅ 设置权限: {script}")
# 4. 验证Python依赖
print("🐍 检查Python依赖...")
try:
import pymysql
print(" ✅ pymysql 已安装")
except ImportError:
print(" ❌ pymysql 未安装,请运行: pip install pymysql")
# 5. 测试数据库连接
print("🗄️ 测试数据库连接...")
try:
from database_config import get_database_config
from database_migration_manager import DatabaseMigrationManager
db_config = get_database_config()
manager = DatabaseMigrationManager(db_config)
print(" ✅ 数据库连接测试成功")
print(" ✅ 迁移历史表初始化完成")
except Exception as e:
print(f" ❌ 数据库连接失败: {e}")
print(" 请检查数据库配置和连接")
# 6. 创建示例迁移
print("📝 创建示例迁移文件...")
if not os.path.exists("migrations/20250101_000000_initial_schema.sql"):
print(" ✅ 初始架构迁移文件已存在")
else:
print(" ✅ 示例迁移文件已创建")
# 7. 更新部署脚本权限
print("🚀 更新部署脚本...")
deploy_scripts = [
"deploy_scripts/deploy_with_backup.sh",
"deploy_scripts/deploy_with_migration.sh"
]
for script in deploy_scripts:
if os.path.exists(script):
os.chmod(script, 0o755)
print(f" ✅ 更新权限: {script}")
print("\n🎉 迁移系统设置完成!")
print("\n📋 下一步操作:")
print("1. 检查并更新 database_config.py 中的数据库配置")
print("2. 运行 'python migrate.py status' 检查系统状态")
print("3. 如需要,创建新的迁移文件: 'python migrate.py create \"migration_name\"'")
print("4. 在测试环境验证迁移: 'python migrate.py migrate --dry-run'")
print("5. 更新 CI/CD 配置: 将 .gitlab-ci-new.yml 重命名为 .gitlab-ci.yml")
print("\n📖 详细使用说明请参考: DATABASE_MIGRATION_GUIDE.md")
def cleanup_old_system():
"""清理旧系统文件(可选)"""
print("\n🧹 清理旧系统文件...")
# 询问是否清理
response = input("是否要清理旧的SQL文件?(y/N): ").lower().strip()
if response not in ['y', 'yes']:
print("跳过清理步骤")
return
# 移动旧文件到归档目录
archive_dir = f"legacy_sql_{datetime.now().strftime('%Y%m%d_%H%M%S')}"
os.makedirs(archive_dir, exist_ok=True)
old_files = [
"fix_admin_user.sql",
"fix_passwords.sql",
"update_password_hashes.sql",
"update_users_with_salt.sql",
"complete_hashes.sql"
]
for old_file in old_files:
if os.path.exists(old_file):
shutil.move(old_file, archive_dir)
print(f" ✅ 归档: {old_file}")
print(f"📦 旧文件已归档到: {archive_dir}")
def show_migration_status():
"""显示迁移状态"""
print("\n📊 当前迁移状态:")
try:
result = subprocess.run(
["python", "migrate.py", "status"],
capture_output=True,
text=True
)
print(result.stdout)
if result.stderr:
print("错误信息:", result.stderr)
except Exception as e:
print(f"无法获取迁移状态: {e}")
def main():
"""主函数"""
print("=" * 60)
print("🗄️ 数据库迁移管理系统设置向导")
print("=" * 60)
# 检查当前目录
if not os.path.exists("app.py"):
print("❌ 请在项目根目录运行此脚本")
return 1
# 设置迁移系统
setup_migration_system()
# 显示当前状态
show_migration_status()
# 询问是否清理旧系统
cleanup_old_system()
print("\n✅ 设置完成!迁移系统已就绪。")
return 0
if __name__ == "__main__":
exit(main())
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
简单的HTTP服务器
提供患者画像页面访问和回访记录API服务
"""
import http.server
import socketserver
import json
import urllib.parse
from datetime import datetime
import os
import sys
# 添加当前目录到Python路径
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
try:
from callback_record_model import CallbackRecordManager, CallbackRecord
DB_AVAILABLE = True
except ImportError:
DB_AVAILABLE = False
print("警告:数据库模块不可用,将使用文件存储")
class CallbackHTTPRequestHandler(http.server.SimpleHTTPRequestHandler):
def __init__(self, *args, **kwargs):
# 初始化数据库管理器
if DB_AVAILABLE:
try:
self.db_manager = CallbackRecordManager()
print("✅ 数据库初始化成功")
except Exception as e:
print(f"❌ 数据库初始化失败: {e}")
self.db_manager = None
else:
self.db_manager = None
super().__init__(*args, **kwargs)
def do_GET(self):
"""处理GET请求"""
if self.path.startswith('/api/'):
self.handle_api_get()
else:
# 处理静态文件请求
super().do_GET()
def do_POST(self):
"""处理POST请求"""
if self.path.startswith('/api/'):
self.handle_api_post()
else:
self.send_error(404, "Not Found")
def handle_api_get(self):
"""处理API GET请求"""
if self.path == '/api/health':
self.send_json_response({
'status': 'ok',
'timestamp': datetime.now().isoformat(),
'database': 'connected' if self.db_manager else 'disconnected'
})
elif self.path.startswith('/api/callback-records/'):
# 获取回访记录
case_number = self.path.split('/')[-1]
if self.db_manager:
try:
records = self.db_manager.get_records_by_case_number(case_number)
self.send_json_response({
'success': True,
'data': [record.to_dict() for record in records],
'count': len(records)
})
except Exception as e:
self.send_json_response({
'success': False,
'message': str(e)
}, 500)
else:
self.send_json_response({
'success': False,
'message': '数据库不可用'
}, 500)
else:
self.send_error(404, "API endpoint not found")
def handle_api_post(self):
"""处理API POST请求"""
if self.path == '/api/callback-records':
self.save_callback_record()
else:
self.send_error(404, "API endpoint not found")
def save_callback_record(self):
"""保存回访记录"""
try:
# 读取请求数据
content_length = int(self.headers['Content-Length'])
post_data = self.rfile.read(content_length)
data = json.loads(post_data.decode('utf-8'))
# 验证必需字段
required_fields = ['caseNumber', 'callbackMethods', 'callbackRecord', 'operator']
for field in required_fields:
if field not in data or not data[field]:
self.send_json_response({
'success': False,
'message': f'缺少必需字段: {field}'
}, 400)
return
if self.db_manager:
# 保存到数据库
record = CallbackRecord(
case_number=data['caseNumber'],
callback_methods=data['callbackMethods'],
callback_record=data['callbackRecord'],
operator=data['operator']
)
record_id = self.db_manager.save_record(record)
self.send_json_response({
'success': True,
'id': record_id,
'message': '保存成功',
'timestamp': datetime.now().isoformat()
})
else:
# 保存到文件
self.save_to_file(data)
self.send_json_response({
'success': True,
'id': int(datetime.now().timestamp()),
'message': '保存成功(文件存储)',
'timestamp': datetime.now().isoformat()
})
except json.JSONDecodeError:
self.send_json_response({
'success': False,
'message': '无效的JSON数据'
}, 400)
except Exception as e:
print(f"保存回访记录失败: {e}")
self.send_json_response({
'success': False,
'message': f'保存失败: {str(e)}'
}, 500)
def save_to_file(self, data):
"""保存到文件(备用方案)"""
filename = 'callback_records_backup.json'
records = []
# 读取现有记录
if os.path.exists(filename):
try:
with open(filename, 'r', encoding='utf-8') as f:
records = json.load(f)
except:
records = []
# 添加新记录
data['id'] = int(datetime.now().timestamp())
data['create_time'] = datetime.now().isoformat()
records.append(data)
# 保存到文件
with open(filename, 'w', encoding='utf-8') as f:
json.dump(records, f, ensure_ascii=False, indent=2)
print(f"记录已保存到文件: {filename}")
def send_json_response(self, data, status_code=200):
"""发送JSON响应"""
response = json.dumps(data, ensure_ascii=False).encode('utf-8')
self.send_response(status_code)
self.send_header('Content-Type', 'application/json; charset=utf-8')
self.send_header('Content-Length', str(len(response)))
self.send_header('Access-Control-Allow-Origin', '*')
self.send_header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS')
self.send_header('Access-Control-Allow-Headers', 'Content-Type, Accept')
self.end_headers()
self.wfile.write(response)
def do_OPTIONS(self):
"""处理OPTIONS请求(CORS预检)"""
self.send_response(200)
self.send_header('Access-Control-Allow-Origin', '*')
self.send_header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS')
self.send_header('Access-Control-Allow-Headers', 'Content-Type, Accept')
self.end_headers()
def main():
"""启动服务器"""
PORT = 8000
print("🚀 启动简单HTTP服务器...")
print(f"📡 服务器地址: http://localhost:{PORT}")
print(f"📁 患者画像: http://localhost:{PORT}/patient_profiles/")
print(f"🔧 API接口: http://localhost:{PORT}/api/")
print(f"📋 测试页面: http://localhost:{PORT}/patient_profiles/TS0K036558.html")
print("=" * 60)
try:
with socketserver.TCPServer(("", PORT), CallbackHTTPRequestHandler) as httpd:
print(f"✅ 服务器已启动在端口 {PORT}")
print("按 Ctrl+C 停止服务器")
httpd.serve_forever()
except KeyboardInterrupt:
print("\n👋 服务器已停止")
except Exception as e:
print(f"❌ 服务器启动失败: {e}")
if __name__ == "__main__":
main()
\ No newline at end of file
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
认证服务器启动脚本
自动检查环境、安装依赖、初始化数据库并启动服务器
"""
import os
import sys
import subprocess
import time
from pathlib import Path
def print_banner():
"""显示启动横幅"""
print("=" * 60)
print(" 回访记录系统 - 认证服务器启动器")
print("=" * 60)
print()
def check_python_version():
"""检查Python版本"""
print("🐍 检查Python版本...")
if sys.version_info < (3, 7):
print("❌ 错误:需要Python 3.7或更高版本")
print(f" 当前版本:{sys.version}")
return False
print(f"✅ Python版本检查通过:{sys.version.split()[0]}")
return True
def check_dependencies():
"""检查并安装依赖包"""
print("\n📦 检查依赖包...")
required_packages = [
'pymysql',
'flask',
'flask-cors',
'configparser'
]
missing_packages = []
for package in required_packages:
try:
__import__(package.replace('-', '_'))
print(f"✅ {package} 已安装")
except ImportError:
missing_packages.append(package)
print(f"❌ {package} 未安装")
if missing_packages:
print(f"\n🔧 安装缺失的依赖包:{', '.join(missing_packages)}")
try:
subprocess.check_call([
sys.executable, '-m', 'pip', 'install'
] + missing_packages)
print("✅ 依赖包安装完成")
except subprocess.CalledProcessError as e:
print(f"❌ 依赖包安装失败:{e}")
print("\n请手动安装依赖包:")
print(f"pip install {' '.join(missing_packages)}")
return False
return True
def check_database_config():
"""检查数据库配置"""
print("\n🗄️ 检查数据库配置...")
config_file = Path("database_config.ini")
if not config_file.exists():
print("⚠️ 数据库配置文件不存在,将创建默认配置")
try:
from database_config import DatabaseConfig
config_manager = DatabaseConfig()
print("✅ 默认数据库配置已创建")
print("📝 请编辑 database_config.ini 文件,配置您的MySQL连接信息")
return False
except Exception as e:
print(f"❌ 创建数据库配置失败:{e}")
return False
try:
from database_config import DatabaseConfig
config_manager = DatabaseConfig()
if config_manager.validate_config():
print("✅ 数据库配置验证通过")
return True
else:
print("❌ 数据库配置无效")
print("📝 请编辑 database_config.ini 文件,检查配置项")
return False
except Exception as e:
print(f"❌ 数据库配置检查失败:{e}")
return False
def test_database_connection():
"""测试数据库连接"""
print("\n🔗 测试数据库连接...")
try:
from user_manager import create_user_manager
user_manager = create_user_manager()
if user_manager and user_manager.test_connection():
print("✅ 数据库连接测试成功")
return True
else:
print("❌ 数据库连接测试失败")
print("\n请检查:")
print("1. MySQL服务是否正在运行")
print("2. 数据库配置是否正确")
print("3. 数据库用户是否有足够权限")
print("4. 防火墙是否允许连接")
return False
except Exception as e:
print(f"❌ 数据库连接测试失败:{e}")
return False
def check_required_files():
"""检查必需的文件"""
print("\n📁 检查必需文件...")
required_files = [
'auth_server.py',
'user_manager.py',
'session_manager.py',
'database_config.py',
'login.html',
'auth_client.js'
]
missing_files = []
for file_path in required_files:
if not Path(file_path).exists():
missing_files.append(file_path)
print(f"❌ {file_path} 文件缺失")
else:
print(f"✅ {file_path} 文件存在")
if missing_files:
print(f"\n❌ 缺失必需文件:{', '.join(missing_files)}")
return False
return True
def start_server():
"""启动认证服务器"""
print("\n🚀 启动认证服务器...")
try:
# 导入并启动服务器
from auth_server import main
main()
except KeyboardInterrupt:
print("\n👋 服务器已停止")
except Exception as e:
print(f"❌ 服务器启动失败:{e}")
return False
return True
def show_usage_info():
"""显示使用说明"""
print("\n" + "=" * 60)
print("🎉 系统已准备就绪!")
print("=" * 60)
print()
print("📱 访问地址:")
print(" 🔐 登录页面:http://localhost:5000/login")
print(" 📋 患者索引页:http://localhost:5000/patient_profiles/ (登录后默认页面)")
print(" 📊 系统仪表盘:http://localhost:5000/dashboard.html")
print(" 👥 用户管理:http://localhost:5000/user_management.html")
print()
print("👤 默认管理员账户:")
print(" 用户名:admin")
print(" 密码:admin123")
print(" ⚠️ 请登录后立即修改密码!")
print()
print("🔧 管理命令:")
print(" 配置数据库:python database_config.py")
print(" 查看数据库:python view_database.py")
print(" 用户管理:python user_manager.py")
print()
print("📝 注意事项:")
print(" 1. 确保MySQL服务正在运行")
print(" 2. 首次使用请配置数据库连接")
print(" 3. 建议定期备份数据库")
print(" 4. 生产环境请修改默认密码")
print()
def main():
"""主函数"""
print_banner()
# 检查Python版本
if not check_python_version():
return False
# 检查依赖包
if not check_dependencies():
return False
# 检查必需文件
if not check_required_files():
return False
# 检查数据库配置
if not check_database_config():
print("\n💡 提示:请按以下步骤配置数据库:")
print(" 1. 启动MySQL服务")
print(" 2. 编辑 database_config.ini 文件")
print(" 3. 重新运行此脚本")
return False
# 测试数据库连接
if not test_database_connection():
return False
# 显示使用说明
show_usage_info()
# 启动服务器
return start_server()
if __name__ == "__main__":
try:
success = main()
if not success:
print("\n❌ 启动失败,请检查上述错误信息")
sys.exit(1)
except KeyboardInterrupt:
print("\n👋 启动已取消")
sys.exit(0)
except Exception as e:
print(f"\n💥 未知错误:{e}")
sys.exit(1)
\ No newline at end of file
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
启动SQLite回访记录API服务器
"""
from callback_record_model import create_flask_api
if __name__ == "__main__":
app = create_flask_api()
if app:
print("🚀 启动SQLite回访记录API服务器...")
print("📡 API端点:")
print(" - POST /api/callback-records 保存回访记录")
print(" - GET /api/callback-records/<case> 获取回访记录")
print(" - GET /api/callback-records/statistics 获取统计信息")
print("🌐 服务器地址: http://localhost:5000")
print("按 Ctrl+C 停止服务器")
print("-" * 50)
app.run(host='0.0.0.0', port=5000, debug=True)
else:
print("❌ API服务器启动失败,请检查依赖")
\ No newline at end of file
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Docker启动文件
在Docker容器中启动患者画像回访话术系统
"""
import os
import sys
# 添加当前目录到Python路径
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
# 导入并启动应用
from auth_system import app
if __name__ == "__main__":
# 获取端口配置
port = int(os.environ.get('PORT', 5000))
host = os.environ.get('HOST', '0.0.0.0')
debug = os.environ.get('FLASK_DEBUG', 'False').lower() == 'true'
print(f"🚀 启动患者画像回访话术系统...")
print(f"📍 监听地址: {host}:{port}")
print(f"🔧 调试模式: {debug}")
# 启动Flask应用
app.run(
host=host,
port=port,
debug=debug,
threaded=True
)
\ No newline at end of file
#!/usr/bin/env python
++ /dev/null
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import os
import sys
import subprocess
import time
def run_script(script_name, args=None):
"""运行指定的Python脚本"""
cmd = [sys.executable, script_name]
if args:
cmd.extend(args)
print(f"正在运行 {script_name}...")
try:
result = subprocess.run(cmd, capture_output=True, text=True, encoding='utf-8')
print(result.stdout)
if result.returncode != 0:
print(f"运行脚本失败: {result.stderr}")
return False
return True
except Exception as e:
print(f"执行脚本时出错: {e}")
return False
def main():
"""主函数,执行整个流程"""
# 检查Excel文件是否存在
excel_files = [f for f in os.listdir('.') if f.endswith(('.xlsx', '.xls'))]
if not excel_files:
print("错误:未找到Excel文件")
return
# 选择Excel文件
if len(excel_files) == 1:
excel_file = excel_files[0]
else:
print("发现多个Excel文件:")
for i, file in enumerate(excel_files, 1):
print(f"{i}. {file}")
try:
choice = int(input("请选择要处理的Excel文件 (输入编号): "))
if 1 <= choice <= len(excel_files):
excel_file = excel_files[choice-1]
else:
print("无效的选择")
return
except ValueError:
print("无效的输入")
return
# 1. 转换Excel到JSON
json_file = os.path.splitext(excel_file)[0] + '.json'
if not os.path.exists(json_file) or input(f"文件 {json_file} 已存在,是否重新生成? (y/n): ").lower() == 'y':
if not run_script('excel_to_json.py', [excel_file]):
print("Excel转JSON失败,流程终止")
return
# 2. 生成HTML文件
if not run_script('generate_html.py'):
print("生成HTML失败,流程终止")
return
# 3. 启动服务器
run_script('start_server.py')
if __name__ == "__main__":
# 显示欢迎信息
print("=" * 60)
print(" 利星行客户画像系统 - 一键式启动工具")
print("=" * 60)
main()
\ No newline at end of file
{
++ /dev/null
{
"regeneration_time": "2025-08-07 09:31:07",
"total_patients": 1,
"success_count": 1,
"failed_count": 0,
"callbacks": [
{
"patient_id": "TS0M004718",
"patient_name": "杨建荣ZZ00566",
"clinic": "学前街门诊",
"age": 65,
"gender": "男",
"generation_time": "2025-08-07 09:31:07",
"callback_script": "```json\n{\n \"selected_diagnosis\": \"缺失牙\"\n}\n```\n═══ 第一部分:开场白 ═══\n• 您好,我是瑞泰通善口腔的医生助理小王。\n• 孙红胜医生特意交代我来关注您的后续情况。\n• 您自从2025年3月检查后,口腔情况怎么样?\n\n═══ 第二部分:告知漏诊项目 ═══\n• 上次来检查的时候,孙红胜医生注意到您有缺失牙的情况。\n• 缺牙的地方如果不处理,旁边的牙齿可能会慢慢歪掉,对面的牙齿也可能伸长出来。\n• 趁现在早一点处理,比以后复杂时省事也省心。\n• 这个问题,孙红胜医生也特别嘱咐我们提醒您一下。\n\n═══ 第三部分:复查建议 ═══\n• 如果方便的话您看最近有没有时间来院复查一下。\n• 让孙红胜医生帮您再仔细看看。\n• 复查检查约30分钟,了解缺失牙位目前状况。\n• 孙红胜医生【时间段1】和【时间段2】这两个时间段有空,您看哪个时间比较方便?\n\n═══ 第四部分:结束回访语 ═══\n预约成功:\n• 好的,那我们【具体预约时间】见。\n• 那不打扰您了,祝您生活愉快。\n预约不成功:\n• 好的,那我下个星期再跟您联系。\n• 好的那不打扰您了,祝您生活愉快。",
"api_type": "agent_chat",
"callback_type": "success",
"patient_data_summary": {
"last_visit_time": "2025-03-25 00:00:00",
"last_clinic": "江苏瑞泰通善口腔学前街医院",
"last_doctor": "孙红胜",
"visit_count": 0
}
}
]
}
\ No newline at end of file
============================================================
++ /dev/null
============================================================
失败患者重新生成结果
重新生成时间: 2025-08-07 09:31:07
总计处理: 1 个患者
============================================================
【1】患者: 杨建荣ZZ00566 (TS0M004718)
诊所: 学前街门诊
年龄: 65岁, 性别: 男
最后就诊: 2025-03-25 00:00:00
就诊医生: 孙红胜
----------------------------------------
回访话术:
```json
{
"selected_diagnosis": "缺失牙"
}
```
═══ 第一部分:开场白 ═══
• 您好,我是瑞泰通善口腔的医生助理小王。
• 孙红胜医生特意交代我来关注您的后续情况。
• 您自从2025年3月检查后,口腔情况怎么样?
═══ 第二部分:告知漏诊项目 ═══
• 上次来检查的时候,孙红胜医生注意到您有缺失牙的情况。
• 缺牙的地方如果不处理,旁边的牙齿可能会慢慢歪掉,对面的牙齿也可能伸长出来。
• 趁现在早一点处理,比以后复杂时省事也省心。
• 这个问题,孙红胜医生也特别嘱咐我们提醒您一下。
═══ 第三部分:复查建议 ═══
• 如果方便的话您看最近有没有时间来院复查一下。
• 让孙红胜医生帮您再仔细看看。
• 复查检查约30分钟,了解缺失牙位目前状况。
• 孙红胜医生【时间段1】和【时间段2】这两个时间段有空,您看哪个时间比较方便?
═══ 第四部分:结束回访语 ═══
预约成功:
• 好的,那我们【具体预约时间】见。
• 那不打扰您了,祝您生活愉快。
预约不成功:
• 好的,那我下个星期再跟您联系。
• 好的那不打扰您了,祝您生活愉快。
============================================================
============================================================
++ /dev/null
============================================================
失败患者重新生成结果
重新生成时间: 2025-08-07 09:35:29
总计处理: 5 个患者
============================================================
【1】患者: 庹伟 (TS0F028745)
诊所: 东亭门诊
年龄: 36岁, 性别: 女
最后就诊: 2025-02-13 00:00:00
就诊医生: 董明月
----------------------------------------
回访话术:
```json
{
"selected_diagnosis": "牙槽骨吸收"
}
```
═══ 第一部分:开场白 ═══
• 您好,我是瑞泰通善口腔的护士小王。
• 您好,董明月医生特意交代我来关注您的后续情况。
• 您自从2月13号检查后,口腔情况怎么样?
═══ 第二部分:告知漏诊项目 ═══
• 上次来检查的时候,董明月医生注意到您有牙槽骨吸收的情况。
• 这个问题如果一直拖着,可能会出现牙齿松动、牙缝变大的情况,而且如果放着不管,牙齿还有脱落的风险,到时候想修复就更麻烦了。
• 趁现在问题还不严重,早点稳住会更好。
• 这个情况,董明月医生也特别嘱咐我们提醒您一下。
═══ 第三部分:复查建议 ═══
• 如果方便的话您看最近有没有时间来院复查一下。
• 让董明月医生帮您再仔细看看。
• 复查检查约30-45分钟,需要仔细检查牙周健康状况。
• 董明月医生【时间段1】和【时间段2】这两个时间段有空,您看哪个时间比较方便?
═══ 第四部分:结束回访语 ═══
预约成功:
• 好的,那我们【具体预约时间】见。
• 那不打扰您了,祝您生活愉快。
预约不成功:
• 好的,那我下个星期再跟您联系。
• 好的那不打扰您了,祝您生活愉快。
============================================================
【2】患者: 徐中华 (TS0G040130)
诊所: 大丰门诊
年龄: 59岁, 性别: 男
最后就诊: 2025-05-26 00:00:00
就诊医生: 茅晓春
----------------------------------------
回访话术:
```json
{
"selected_diagnosis": "牙槽骨吸收"
}
```
═══ 第一部分:开场白 ═══
• 您好,我是瑞泰通善口腔的护士小茅。
• 您好。
• 茅晓春医生特意交代我来关注您的后续情况。
• 您自从5月26号检查后,口腔情况怎么样?
═══ 第二部分:告知漏诊项目 ═══
• 上次来检查的时候,茅晓春医生注意到您有牙槽骨吸收的情况。
• 这个问题如果一直拖着,可能会出现牙齿松动、牙缝变大的情况,吃东西也不太舒服。
• 而且如果放着不管,牙齿还有脱落的风险,到时候想修复就更麻烦了。
• 这个情况,茅晓春医生也特别嘱咐我们提醒您一下。
═══ 第三部分:复查建议 ═══
• 如果方便的话您看最近有没有时间来院复查一下。
• 让茅晓春医生帮您再仔细看看。
• 复查检查约30-45分钟,需要仔细检查牙周健康状况。
• 茅晓春医生【时间段1】和【时间段2】这两个时间段有空,您看哪个时间比较方便?
═══ 第四部分:结束回访语 ═══
预约成功:
• 好的,那我们【具体预约时间】见。
• 那不打扰您了,祝您生活愉快。
预约不成功:
• 好的,那我下个星期再跟您联系。
• 好的那不打扰您了,祝您生活愉快。
============================================================
【3】患者: 李燕军 (TS0L003744)
诊所: 新吴门诊
年龄: 40岁, 性别: 男
最后就诊: 2024-12-28 00:00:00
就诊医生: 王梓
----------------------------------------
回访话术:
```json
{
"selected_diagnosis": "牙槽骨吸收"
}
```
═══ 第一部分:开场白 ═══
• 您好,我是瑞泰通善口腔的医生助理小王。
• 王梓医生特意交代我来关注您的后续情况。
• 您自从12月28号检查后,口腔情况怎么样?
═══ 第二部分:告知漏诊项目 ═══
• 上次来检查的时候,王梓医生注意到您有牙槽骨吸收的情况。
• 这个问题如果一直拖着,可能会出现牙齿松动、牙缝变大的情况,时间久了还有脱落的风险。
• 趁现在问题还不严重,早点稳住会更好。
• 这个情况,王梓医生也特别嘱咐我们提醒您一下。
═══ 第三部分:复查建议 ═══
• 如果方便的话您看最近有没有时间来院复查一下。
• 让王梓医生帮您再仔细看看。
• 复查检查约30-45分钟,需要仔细检查牙周健康状况。
• 王梓医生【时间段1】和【时间段2】这两个时间段有空,您看哪个时间比较方便?
═══ 第四部分:结束回访语 ═══
[预约成功]
• 好的,那我们【具体预约时间】见。
• 那不打扰您了,祝您生活愉快。
[预约不成功]
• 好的,那我下个星期再跟您联系。
• 好的那不打扰您了,祝您生活愉快。
═══ 附加:赠送礼品(见机行事)═══
• 我们为回访患者准备了小礼品。
• 到时候来检查可以领取。
• 表示我们对您的感谢。
============================================================
【4】患者: 刘爱军 (TS0K083350)
诊所: 通善口腔医院
年龄: 41岁, 性别: 女
最后就诊: 2025-05-17 00:00:00
就诊医生: 张琪
----------------------------------------
回访话术:
```json
{
"selected_diagnosis": "牙槽骨吸收"
}
```
═══ 第一部分:开场白 ═══
• 您好,我是江苏瑞泰通善口腔医院的护士长小王。
• 您好,张琪医生特意交代我来关注您的后续情况。
• 您自从5月17号检查后,口腔情况怎么样?
═══ 第二部分:告知漏诊项目 ═══
• 上次来检查的时候,张琪医生注意到您有牙槽骨吸收的情况。
• 这个问题如果拖着,牙齿容易慢慢松动,牙缝也可能变大,影响吃东西。而且时间长了,牙齿还有脱落的风险,到时候想修复就更麻烦了。
• 趁现在问题还不算严重,早一点关注,对稳定牙齿有很大帮助。
• 这个情况,张琪医生也特别嘱咐我们一定要提醒您一下。
═══ 第三部分:复查建议 ═══
• 如果方便的话您看最近有没有时间来院复查一下。
• 让张琪医生帮您再仔细看看现在的情况。
• 复查检查约30-45分钟,需要仔细检查牙周健康状况。
• 张琪医生【时间段1】和【时间段2】这两个时间段有空,您看哪个时间比较方便?
═══ 第四部分:结束回访语 ═══
**预约成功:**
• 好的,那我们【具体预约时间】见。
• 那不打扰您了,祝您生活愉快。
**预约不成功:**
• 好的,那我下个星期再跟您联系。
• 好的那不打扰您了,祝您生活愉快。
============================================================
【5】患者: 杨建荣ZZ00566 (TS0M004718)
诊所: 学前街门诊
年龄: 65岁, 性别: 男
最后就诊: 2025-03-25 00:00:00
就诊医生: 孙红胜
----------------------------------------
回访话术:
```json
{
"selected_diagnosis": "缺失牙"
}
```
═══ 第一部分:开场白 ═══
• 您好,我是瑞泰通善口腔的护士小王。
• 您好。
• 孙红胜医生特意交代我来关注您的后续情况。
• 您自从3月25号检查后,口腔情况怎么样?
═══ 第二部分:告知漏诊项目 ═══
• 上次来检查的时候,孙红胜医生注意到您有缺失牙的情况。
• 缺牙的地方如果不处理,旁边的牙齿可能会慢慢歪掉,对面的牙齿也可能伸长出来,时间长了吃东西会不太舒服。
• 而且如果放着不管,牙槽骨还可能慢慢萎缩,到时候想修复就更麻烦了。
• 趁现在身体还好,早一点处理,比以后复杂时省事也省心。
• 这个情况,孙红胜医生也特别嘱咐我们提醒您一下。
═══ 第三部分:复查建议 ═══
• 如果方便的话您看最近有没有时间来院复查一下。
• 让孙红胜医生帮您再仔细看看。
• 复查检查约30分钟,了解缺失牙位目前状况。
• 孙红胜医生【时间段1】和【时间段2】这两个时间段有空,您看哪个时间比较方便?
═══ 第四部分:结束回访语 ═══
**预约成功:**
• 好的,那我们【具体预约时间】见。
• 那不打扰您了,祝您生活愉快。
**预约不成功:**
• 好的,那我下个星期再跟您联系。
• 好的那不打扰您了,祝您生活愉快。
============================================================
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment