Commit 0741776b by luoqi

feat:整理代码

parent d7337bcd
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
# 数据库迁移管理系统使用指南
## 📋 概述
这是一个为 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. 日志文件内容
---
**重要提醒**: 在生产环境执行迁移前,请务必在测试环境充分验证!
...@@ -26,10 +26,10 @@ RUN pip install --no-cache-dir -r requirements.txt ...@@ -26,10 +26,10 @@ RUN pip install --no-cache-dir -r requirements.txt
COPY . . COPY . .
# 创建必要的目录 # 创建必要的目录
RUN mkdir -p progress_saves dify_callback_results RUN mkdir -p progress_saves data/callbacks data/patients/clinics data/patients/merged data/exports
# 设置权限 # 设置权限
RUN chmod +x *.py RUN chmod +x *.py database/scripts/entrypoint.sh database/scripts/*.py
# 暴露端口 # 暴露端口
EXPOSE 5000 EXPOSE 5000
...@@ -38,8 +38,8 @@ EXPOSE 5000 ...@@ -38,8 +38,8 @@ EXPOSE 5000
RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/* RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/*
# 健康检查 # 健康检查
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \ HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=3 \
CMD curl -f http://localhost:5000/ || exit 1 CMD curl -f http://localhost:5000/api/health || exit 1
# 启动命令 # 使用entrypoint.sh启动(零配置自动迁移)
CMD ["python", "app.py"] ENTRYPOINT ["./database/scripts/entrypoint.sh"]
\ No newline at end of file \ No newline at end of file
# MySQL Dockerfile - 基于官方MySQL镜像
FROM mysql:8.0
# 设置时区
ENV TZ=Asia/Shanghai
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
# 复制初始化脚本
COPY init.sql /docker-entrypoint-initdb.d/
# 设置权限
RUN chmod 644 /docker-entrypoint-initdb.d/init.sql
# 暴露端口
EXPOSE 3306
# 使用官方MySQL启动命令
CMD ["mysqld"]
\ No newline at end of file
No preview for this file type
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
通过Web API备份数据库
无需SSH权限,通过应用程序接口备份数据
"""
import os
import sys
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from auth_system import app
from flask import jsonify, request, send_file
import pymysql
import json
from datetime import datetime
import tempfile
import zipfile
# 数据库配置
DB_CONFIG = {
'host': 'localhost',
'port': 3306,
'user': 'callback_user',
'password': 'dev_password_123',
'database': 'callback_system',
'charset': 'utf8mb4'
}
def backup_database():
"""备份数据库到临时文件"""
try:
connection = pymysql.connect(**DB_CONFIG)
cursor = connection.cursor()
# 创建临时文件
temp_file = tempfile.NamedTemporaryFile(mode='w', suffix='.sql', delete=False, encoding='utf-8')
temp_file_path = temp_file.name
# 获取所有表名
cursor.execute("SHOW TABLES")
tables = [table[0] for table in cursor.fetchall()]
print(f"📋 开始备份 {len(tables)} 个表...")
# 写入备份头部信息
temp_file.write(f"-- 患者画像回访话术系统数据库备份\n")
temp_file.write(f"-- 备份时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n")
temp_file.write(f"-- 数据库: {DB_CONFIG['database']}\n\n")
# 备份每个表
for table_name in tables:
print(f" 📊 备份表: {table_name}")
# 获取表结构
cursor.execute(f"SHOW CREATE TABLE `{table_name}`")
create_table_sql = cursor.fetchone()[1]
temp_file.write(f"\n-- 表结构: {table_name}\n")
temp_file.write(f"DROP TABLE IF EXISTS `{table_name}`;\n")
temp_file.write(f"{create_table_sql};\n\n")
# 获取表数据
cursor.execute(f"SELECT COUNT(*) FROM `{table_name}`")
row_count = cursor.fetchone()[0]
if row_count > 0:
temp_file.write(f"-- 表数据: {table_name} ({row_count} 行)\n")
# 分批获取数据避免内存问题
batch_size = 1000
offset = 0
while offset < row_count:
cursor.execute(f"SELECT * FROM `{table_name}` LIMIT {batch_size} OFFSET {offset}")
rows = cursor.fetchall()
for row in rows:
# 构建INSERT语句
values = []
for value in row:
if value is None:
values.append('NULL')
elif isinstance(value, (int, float)):
values.append(str(value))
else:
# 转义字符串
escaped_value = str(value).replace("'", "''").replace("\\", "\\\\")
values.append(f"'{escaped_value}'")
temp_file.write(f"INSERT INTO `{table_name}` VALUES ({', '.join(values)});\n")
offset += batch_size
print(f" ✅ 已备份 {min(offset, row_count)}/{row_count} 行")
temp_file.write("\n")
temp_file.close()
connection.close()
print(f"✅ 数据库备份完成: {temp_file_path}")
return temp_file_path
except Exception as e:
print(f"❌ 数据库备份失败: {e}")
if 'connection' in locals():
connection.close()
return None
def create_backup_zip(backup_file_path):
"""创建备份压缩包"""
try:
# 创建临时压缩文件
zip_file = tempfile.NamedTemporaryFile(mode='wb', suffix='.zip', delete=False)
zip_file_path = zip_file.name
zip_file.close()
# 压缩备份文件
with zipfile.ZipFile(zip_file_path, 'w', zipfile.ZIP_DEFLATED) as zipf:
# 添加备份文件
backup_filename = os.path.basename(backup_file_path)
zipf.write(backup_file_path, backup_filename)
# 添加备份信息
info = {
'backup_time': datetime.now().isoformat(),
'database': DB_CONFIG['database'],
'tables_count': len([f for f in os.listdir('.') if f.endswith('.sql')]),
'backup_size': os.path.getsize(backup_file_path)
}
zipf.writestr('backup_info.json', json.dumps(info, ensure_ascii=False, indent=2))
print(f"✅ 备份压缩包创建完成: {zip_file_path}")
return zip_file_path
except Exception as e:
print(f"❌ 创建压缩包失败: {e}")
return None
# 添加备份API路由到Flask应用
@app.route('/api/backup/database', methods=['POST'])
def api_backup_database():
"""API接口:备份数据库"""
try:
print("🚀 收到数据库备份请求...")
# 执行备份
backup_file_path = backup_database()
if not backup_file_path:
return jsonify({'error': '数据库备份失败'}), 500
# 创建压缩包
zip_file_path = create_backup_zip(backup_file_path)
if not zip_file_path:
return jsonify({'error': '创建压缩包失败'}), 500
# 清理临时文件
os.unlink(backup_file_path)
# 返回下载链接
backup_filename = f"database_backup_{datetime.now().strftime('%Y%m%d_%H%M%S')}.zip"
return jsonify({
'success': True,
'message': '数据库备份成功',
'backup_file': backup_filename,
'download_url': f'/api/backup/download/{backup_filename}',
'backup_time': datetime.now().isoformat()
})
except Exception as e:
print(f"❌ API备份失败: {e}")
return jsonify({'error': f'备份失败: {str(e)}'}), 500
@app.route('/api/backup/download/<filename>')
def api_download_backup(filename):
"""API接口:下载备份文件"""
try:
# 查找最新的备份文件
backup_dir = tempfile.gettempdir()
backup_files = [f for f in os.listdir(backup_dir) if f.endswith('.zip') and 'database_backup' in f]
if not backup_files:
return jsonify({'error': '未找到备份文件'}), 404
# 使用最新的备份文件
latest_backup = max(backup_files, key=lambda f: os.path.getctime(os.path.join(backup_dir, f)))
backup_path = os.path.join(backup_dir, latest_backup)
return send_file(
backup_path,
as_attachment=True,
download_name=filename,
mimetype='application/zip'
)
except Exception as e:
print(f"❌ 下载备份文件失败: {e}")
return jsonify({'error': f'下载失败: {str(e)}'}), 500
@app.route('/api/backup/status')
def api_backup_status():
"""API接口:获取备份状态"""
try:
backup_dir = tempfile.gettempdir()
backup_files = [f for f in os.listdir(backup_dir) if f.endswith('.zip') and 'database_backup' in f]
backups = []
for backup_file in backup_files:
backup_path = os.path.join(backup_dir, backup_file)
backup_info = {
'filename': backup_file,
'size': os.path.getsize(backup_path),
'created_time': datetime.fromtimestamp(os.path.getctime(backup_path)).isoformat(),
'download_url': f'/api/backup/download/{backup_file}'
}
backups.append(backup_info)
# 按创建时间排序
backups.sort(key=lambda x: x['created_time'], reverse=True)
return jsonify({
'success': True,
'backups': backups,
'total_count': len(backups)
})
except Exception as e:
print(f"❌ 获取备份状态失败: {e}")
return jsonify({'error': f'获取状态失败: {str(e)}'}), 500
if __name__ == "__main__":
print("🔧 数据库备份API已添加到Flask应用")
print("📋 可用的API接口:")
print(" POST /api/backup/database - 创建数据库备份")
print(" GET /api/backup/status - 获取备份状态")
print(" GET /api/backup/download/* - 下载备份文件")
\ No newline at end of file
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
回访记录API服务器
提供MySQL数据库的HTTP API接口
"""
import os
import sys
from datetime import datetime
from flask import Flask, request, jsonify, send_from_directory
from flask_cors import CORS
# 添加当前目录到Python路径
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
try:
from callback_record_mysql import MySQLCallbackRecordManager, CallbackRecord
from database_config import DatabaseConfig
DEPENDENCIES_AVAILABLE = True
except ImportError as e:
DEPENDENCIES_AVAILABLE = False
IMPORT_ERROR = str(e)
app = Flask(__name__)
CORS(app) # 允许跨域请求
# 全局数据库管理器
db_manager = None
def initialize_database():
"""初始化数据库连接"""
global db_manager
if not DEPENDENCIES_AVAILABLE:
print(f"依赖库未安装: {IMPORT_ERROR}")
print("请运行: pip install pymysql flask flask-cors")
return False
try:
# 加载数据库配置
config_manager = DatabaseConfig()
if not config_manager.validate_config():
print("数据库配置无效,请运行 python database_config.py 进行配置")
return False
mysql_config = config_manager.get_mysql_config()
# 创建数据库管理器
db_manager = MySQLCallbackRecordManager(**mysql_config)
# 测试连接
if db_manager.test_connection():
print("✓ MySQL数据库连接成功")
return True
else:
print("✗ MySQL数据库连接失败")
return False
except Exception as e:
print(f"数据库初始化失败: {e}")
return False
@app.route('/api/callback-records', methods=['POST'])
def save_callback_record():
"""保存回访记录API"""
if not db_manager:
return jsonify({
'success': False,
'message': '数据库未初始化'
}), 500
try:
data = request.get_json()
# 验证必需字段
required_fields = ['caseNumber', 'callbackMethods', 'callbackRecord', 'operator']
for field in required_fields:
if field not in data or not data[field]:
return jsonify({
'success': False,
'message': f'缺少必需字段: {field}'
}), 400
# 创建记录对象
record = CallbackRecord(
case_number=data['caseNumber'],
callback_methods=data['callbackMethods'],
callback_record=data['callbackRecord'],
operator=data['operator']
)
# 保存到数据库
record_id = db_manager.save_record(record)
return jsonify({
'success': True,
'id': record_id,
'message': '保存成功',
'timestamp': datetime.now().isoformat()
})
except Exception as e:
print(f"保存回访记录失败: {e}")
return jsonify({
'success': False,
'message': f'保存失败: {str(e)}'
}), 500
@app.route('/api/callback-records/<case_number>', methods=['GET'])
def get_callback_records(case_number):
"""获取回访记录API"""
if not db_manager:
return jsonify({
'success': False,
'message': '数据库未初始化'
}), 500
try:
records = db_manager.get_records_by_case_number(case_number)
return jsonify({
'success': True,
'data': [record.to_dict() for record in records],
'count': len(records)
})
except Exception as e:
print(f"获取回访记录失败: {e}")
return jsonify({
'success': False,
'message': f'获取失败: {str(e)}'
}), 500
@app.route('/api/callback-records/statistics', methods=['GET'])
def get_statistics():
"""获取统计信息API"""
if not db_manager:
return jsonify({
'success': False,
'message': '数据库未初始化'
}), 500
try:
stats = db_manager.get_statistics()
return jsonify({
'success': True,
'data': stats
})
except Exception as e:
print(f"获取统计信息失败: {e}")
return jsonify({
'success': False,
'message': f'获取失败: {str(e)}'
}), 500
@app.route('/api/callback-records/<int:record_id>', methods=['DELETE'])
def delete_callback_record(record_id):
"""删除回访记录API"""
if not db_manager:
return jsonify({
'success': False,
'message': '数据库未初始化'
}), 500
try:
success = db_manager.delete_record(record_id)
if success:
return jsonify({
'success': True,
'message': '删除成功'
})
else:
return jsonify({
'success': False,
'message': '记录不存在'
}), 404
except Exception as e:
print(f"删除回访记录失败: {e}")
return jsonify({
'success': False,
'message': f'删除失败: {str(e)}'
}), 500
@app.route('/api/health', methods=['GET'])
def health_check():
"""健康检查API"""
status = {
'status': 'ok',
'timestamp': datetime.now().isoformat(),
'database': 'disconnected'
}
if db_manager and db_manager.test_connection():
status['database'] = 'connected'
return jsonify(status)
@app.route('/')
def index():
"""首页 - 显示API文档"""
return """
<h1>回访记录API服务器</h1>
<h2>可用接口:</h2>
<ul>
<li><strong>POST</strong> /api/callback-records - 保存回访记录</li>
<li><strong>GET</strong> /api/callback-records/&lt;case_number&gt; - 获取指定病历号的回访记录</li>
<li><strong>GET</strong> /api/callback-records/statistics - 获取统计信息</li>
<li><strong>DELETE</strong> /api/callback-records/&lt;record_id&gt; - 删除回访记录</li>
<li><strong>GET</strong> /api/health - 健康检查</li>
</ul>
<h2>使用说明:</h2>
<ol>
<li>确保MySQL数据库正在运行</li>
<li>配置数据库连接信息(运行 python database_config.py)</li>
<li>启动API服务器(运行 python callback_api_server.py)</li>
<li>在患者画像页面中使用回访记录功能</li>
</ol>
"""
# 静态文件服务 - 为患者画像页面提供服务
@app.route('/patient_profiles/<path:filename>')
def patient_profiles(filename):
"""提供患者画像页面文件"""
return send_from_directory('patient_profiles', filename)
@app.route('/callback_record.js')
def callback_js():
"""提供JavaScript文件"""
return send_from_directory('.', 'callback_record.js')
def main():
"""主函数"""
print("=== 回访记录API服务器 ===")
# 检查依赖
if not DEPENDENCIES_AVAILABLE:
print(f"错误: {IMPORT_ERROR}")
print("\n请安装所需依赖:")
print("pip install pymysql flask flask-cors")
return
# 初始化数据库
if not initialize_database():
print("数据库初始化失败,服务器无法启动")
print("\n请检查:")
print("1. MySQL服务是否正在运行")
print("2. 数据库配置是否正确(运行 python database_config.py 检查)")
print("3. 数据库用户是否有足够权限")
return
print("\n服务器启动中...")
print("API地址: http://localhost:5000/api/")
print("患者画像: http://localhost:5000/patient_profiles/")
print("按 Ctrl+C 停止服务器")
try:
# 启动Flask服务器
app.run(
host='0.0.0.0', # 允许外部访问
port=5000,
debug=True, # 开发模式
threaded=True # 多线程支持
)
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 -*-
"""
回访记录相关常量定义
包含回访不成功原因和AI反馈类型
"""
# 回访不成功的原因(简化版,控制在10个以内)
CALLBACK_FAILURE_REASONS = [
# 联系不上类(合并)
"患者电话无人接听/关机/停机",
"患者联系方式有误或已变更",
"患者微信/短信无回复",
# 拒绝回访类(合并)
"患者拒绝接受回访或表示不需要",
"患者对诊疗结果不满意",
# 时间不合适(合并)
"患者时间不便(工作/休息/外出)",
# 健康和治疗状况(合并)
"患者身体不适或已在其他医院治疗",
# 经济原因(合并)
"患者经济条件不允许或费用问题",
# 信任度问题(合并)
"患者对诊断结果有疑虑或不信任",
# 其他原因
"其他特殊情况"
]
# 放弃回访的原因(新增)
ABANDON_CALLBACK_REASONS = [
"有下次预约",
"漏诊项不存在",
"漏诊项已治疗",
"其他"
]
# AI错误反馈类型(简化版,控制在10个以内)
AI_FEEDBACK_TYPES = [
# 漏诊检测问题(合并)
"AI漏诊检测错误(检测不准确或遗漏)",
# 话术内容问题(合并)
"AI话术内容不合适(过于专业或过于简单)",
# 语言表达问题(合并)
"AI语言表达不自然或用词不当",
# 个性化问题(合并)
"AI内容缺乏个性化或与患者情况不符",
# 治疗建议问题(合并)
"AI治疗方案或复查时间建议不合理",
# 患者信息分析问题(合并)
"AI对患者年龄/性别/病史分析错误",
# 技术和格式问题(合并)
"AI生成内容格式错误或信息缺失",
# 数据解析问题(合并)
"AI未能正确解析患者数据或病历",
# 系统稳定性问题(合并)
"AI功能不稳定或响应速度慢",
# 其他问题
"其他AI相关问题"
]
# 回访状态
CALLBACK_STATUS_OPTIONS = [
"已回访",
"未回访"
]
# 回访方式选项
CALLBACK_METHODS = [
"打电话",
"发微信",
"发短信",
"视频通话",
"面谈"
]
# 回访结果选项(新增放弃回访)
CALLBACK_RESULT_OPTIONS = [
"成功",
"不成功",
"放弃回访"
]
\ No newline at end of file
/**
* 回访记录功能JavaScript
* 处理字符计数、保存等功能
*/
// 页面加载完成后初始化
document.addEventListener('DOMContentLoaded', function() {
initializeCallbackRecord();
});
/**
* 初始化回访记录功能
*/
function initializeCallbackRecord() {
// 初始化字符计数
initCharacterCount();
// 初始化表单验证
initFormValidation();
console.log('回访记录功能初始化完成');
}
/**
* 初始化字符计数功能
*/
function initCharacterCount() {
const textareas = document.querySelectorAll('textarea[name="callback-record"]');
textareas.forEach(textarea => {
const caseNumber = textarea.id.replace('callback-record-', '');
const countElement = document.getElementById(`char-count-${caseNumber}`);
if (countElement) {
// 初始化计数
updateCharacterCount(textarea.id);
// 监听输入事件
textarea.addEventListener('input', function() {
updateCharacterCount(this.id);
});
// 监听粘贴事件
textarea.addEventListener('paste', function() {
setTimeout(() => {
updateCharacterCount(this.id);
}, 0);
});
}
});
}
/**
* 更新字符计数
* @param {string} textareaId - 文本域ID
*/
function updateCharacterCount(textareaId) {
const textarea = document.getElementById(textareaId);
const caseNumber = textareaId.replace('callback-record-', '');
const countElement = document.getElementById(`char-count-${caseNumber}`);
if (textarea && countElement) {
const currentLength = textarea.value.length;
const maxLength = textarea.getAttribute('maxlength') || 500;
countElement.textContent = currentLength;
// 根据字符数改变颜色
if (currentLength > maxLength * 0.9) {
countElement.className = 'text-red-500 font-medium';
} else if (currentLength > maxLength * 0.7) {
countElement.className = 'text-yellow-600';
} else {
countElement.className = 'text-gray-500';
}
}
}
/**
* 初始化表单验证
*/
function initFormValidation() {
const textareas = document.querySelectorAll('textarea[name="callback-record"]');
textareas.forEach(textarea => {
textarea.addEventListener('blur', function() {
validateCallbackRecord(this);
});
});
}
/**
* 验证回访记录
* @param {HTMLElement} textarea - 文本域元素
*/
function validateCallbackRecord(textarea) {
const value = textarea.value.trim();
const minLength = 5; // 最少5个字符
// 移除之前的错误样式
textarea.classList.remove('border-red-500', 'border-green-500');
if (value.length > 0 && value.length < minLength) {
textarea.classList.add('border-red-500');
showToast('回访记录至少需要5个字符', 'warning');
} else if (value.length >= minLength) {
textarea.classList.add('border-green-500');
}
}
/**
* 保存回访记录
* @param {string} caseNumber - 病历号
*/
function saveCallbackRecord(caseNumber) {
const textarea = document.getElementById(`callback-record-${caseNumber}`);
const methodCheckboxes = document.querySelectorAll(`input[name="callback-method-${caseNumber}"]:checked`);
if (!textarea) {
showToast('找不到回访记录输入框', 'error');
return;
}
const record = textarea.value.trim();
if (!record) {
showToast('请输入回访记录内容', 'warning');
textarea.focus();
return;
}
if (record.length < 5) {
showToast('回访记录至少需要5个字符', 'warning');
textarea.focus();
return;
}
// 获取选中的回访方式
const methods = Array.from(methodCheckboxes).map(cb => cb.value);
if (methods.length === 0) {
showToast('请选择至少一种回访方式', 'warning');
return;
}
// 构建保存数据
const saveData = {
caseNumber: caseNumber,
callbackMethods: methods,
callbackRecord: record,
operator: '系统用户', // 可以根据实际情况修改
timestamp: new Date().toISOString()
};
// 显示保存中状态
const saveBtn = document.querySelector(`button[onclick="saveCallbackRecord('${caseNumber}')"]`);
const originalText = saveBtn.innerHTML;
saveBtn.innerHTML = '<i class="fas fa-spinner fa-spin mr-1"></i>保存中...';
saveBtn.disabled = true;
// 调用保存API
saveCallbackRecordToDatabase(saveData)
.then(result => {
showToast('回访记录保存成功', 'success');
console.log('保存成功:', result);
// 可以在这里添加保存成功后的处理逻辑
// 比如清空表单、显示保存时间等
})
.catch(error => {
console.error('保存失败:', error);
showToast(`保存失败: ${error.message}`, 'error');
})
.finally(() => {
// 恢复按钮状态
saveBtn.innerHTML = originalText;
saveBtn.disabled = false;
});
}
/**
* 保存回访记录到MySQL数据库
* @param {Object} data - 保存数据
* @returns {Promise} - 保存结果
*/
async function saveCallbackRecordToDatabase(data) {
console.log('准备保存到MySQL数据库的数据:', data);
try {
const response = await fetch('/api/callback-records', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json'
},
body: JSON.stringify(data)
});
if (!response.ok) {
const errorText = await response.text();
throw new Error(`HTTP ${response.status}: ${errorText}`);
}
const result = await response.json();
if (result.success) {
return result;
} else {
throw new Error(result.message || '保存失败');
}
} catch (error) {
console.error('保存回访记录到MySQL失败:', error);
// 如果是网络错误或API不可用,显示友好提示
if (error.name === 'TypeError' || error.message.includes('fetch')) {
throw new Error('无法连接到服务器,请检查网络连接或联系管理员');
}
throw error;
}
}
/**
* 设置下次回访(预留功能)
* @param {string} caseNumber - 病历号
*/
function scheduleNextCallback(caseNumber) {
showToast('此功能正在开发中...', 'info');
console.log('设置下次回访:', caseNumber);
}
/**
* 新建预约(预留功能)
* @param {string} caseNumber - 病历号
*/
function createAppointment(caseNumber) {
showToast('此功能正在开发中...', 'info');
console.log('新建预约:', caseNumber);
}
/**
* 显示提示消息
* @param {string} message - 消息内容
* @param {string} type - 消息类型 (success, error, warning, info)
*/
function showToast(message, type = 'info') {
// 创建提示元素
const toast = document.createElement('div');
toast.className = `fixed top-4 right-4 px-6 py-3 rounded-lg shadow-lg z-50 transition-all duration-300 transform translate-x-full`;
// 根据类型设置样式
switch (type) {
case 'success':
toast.classList.add('bg-green-500', 'text-white');
break;
case 'error':
toast.classList.add('bg-red-500', 'text-white');
break;
case 'warning':
toast.classList.add('bg-yellow-500', 'text-white');
break;
default:
toast.classList.add('bg-blue-500', 'text-white');
}
toast.textContent = message;
document.body.appendChild(toast);
// 显示动画
setTimeout(() => {
toast.classList.remove('translate-x-full');
}, 100);
// 自动隐藏
setTimeout(() => {
toast.classList.add('translate-x-full');
setTimeout(() => {
if (toast.parentNode) {
toast.parentNode.removeChild(toast);
}
}, 300);
}, 3000);
}
/**
* 获取认证令牌(如果需要的话)
* @returns {string} - 认证令牌
*/
function getAuthToken() {
// 这里可以实现获取认证令牌的逻辑
// 比如从localStorage、sessionStorage或cookie中获取
return '';
}
\ No newline at end of file
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
门诊配置文件
定义所有门诊的基本信息和映射关系
"""
# 门诊映射配置 (基于实际数据分析结果)
CLINIC_MAPPING = {
'学前街门诊': {
'clinic_id': 'clinic_xuexian',
'clinic_name': '学前街门诊',
'json_file': '诊所患者json/学前街门诊.json',
'folder_name': 'clinic_xuexian',
'description': '通善学前街门诊',
'expected_patients': 765
},
'大丰门诊': {
'clinic_id': 'clinic_dafeng',
'clinic_name': '大丰门诊',
'json_file': '诊所患者json/大丰门诊.json',
'folder_name': 'clinic_dafeng',
'description': '通善大丰门诊',
'expected_patients': 598
},
'东亭门诊': {
'clinic_id': 'clinic_dongting',
'clinic_name': '东亭门诊',
'json_file': '诊所患者json/东亭门诊.json',
'folder_name': 'clinic_dongting',
'description': '通善东亭门诊',
'expected_patients': 479
},
'河埒门诊': {
'clinic_id': 'clinic_helai',
'clinic_name': '河埒门诊',
'json_file': '诊所患者json/河埒门诊.json',
'folder_name': 'clinic_helai',
'description': '通善河埒门诊',
'expected_patients': 108
},
'红豆门诊': {
'clinic_id': 'clinic_hongdou',
'clinic_name': '红豆门诊',
'json_file': '诊所患者json/红豆门诊.json',
'folder_name': 'clinic_hongdou',
'description': '通善红豆门诊',
'expected_patients': 500
},
'惠山门诊': {
'clinic_id': 'clinic_huishan',
'clinic_name': '惠山门诊',
'json_file': '诊所患者json/惠山门诊.json',
'folder_name': 'clinic_huishan',
'description': '通善惠山门诊',
'expected_patients': 323
},
'马山门诊': {
'clinic_id': 'clinic_mashan',
'clinic_name': '马山门诊',
'json_file': '诊所患者json/马山门诊.json',
'folder_name': 'clinic_mashan',
'description': '通善马山门诊',
'expected_patients': 527
},
'通善口腔医院': {
'clinic_id': 'clinic_hospital',
'clinic_name': '通善口腔医院',
'json_file': '诊所患者json/通善口腔医院.json',
'folder_name': 'clinic_hospital',
'description': '通善口腔医院总院',
'expected_patients': 536
},
'新吴门诊': {
'clinic_id': 'clinic_xinwu',
'clinic_name': '新吴门诊',
'json_file': '诊所患者json/新吴门诊.json',
'folder_name': 'clinic_xinwu',
'description': '通善新吴门诊',
'expected_patients': 297
}
}
# 快速访问映射
CLINIC_ID_TO_NAME = {info['clinic_id']: info['clinic_name'] for info in CLINIC_MAPPING.values()}
CLINIC_ID_TO_JSON = {info['clinic_id']: info['json_file'] for info in CLINIC_MAPPING.values()}
CLINIC_ID_TO_FOLDER = {info['clinic_id']: info['folder_name'] for info in CLINIC_MAPPING.values()}
# 默认用户配置 (基于实际门诊用户)
DEFAULT_USERS = [
# 学前街门诊 (3名用户)
{
'username': 'jinqin',
'password': 'jinqin123',
'role': 'clinic_user',
'clinic_id': 'clinic_xuexian',
'real_name': '金沁',
'clinic_name': '学前街门诊'
},
{
'username': 'renshanshan',
'password': 'renshanshan123',
'role': 'clinic_user',
'clinic_id': 'clinic_xuexian',
'real_name': '任姗姗',
'clinic_name': '学前街门诊'
},
{
'username': 'shaojun',
'password': 'shaojun123',
'role': 'clinic_user',
'clinic_id': 'clinic_xuexian',
'real_name': '邵军',
'clinic_name': '学前街门诊'
},
# 新吴门诊 (1名用户)
{
'username': 'litingting',
'password': 'litingting123',
'role': 'clinic_user',
'clinic_id': 'clinic_xinwu',
'real_name': '李婷婷',
'clinic_name': '新吴门诊'
},
# 红豆门诊 (2名用户)
{
'username': 'maqiuyi',
'password': 'maqiuyi123',
'role': 'clinic_user',
'clinic_id': 'clinic_hongdou',
'real_name': '马秋怡',
'clinic_name': '红豆门诊'
},
{
'username': 'tangqimin',
'password': 'tangqimin123',
'role': 'clinic_user',
'clinic_id': 'clinic_hongdou',
'real_name': '汤其敏',
'clinic_name': '红豆门诊'
},
# 东亭门诊 (1名用户)
{
'username': 'yueling',
'password': 'yueling123',
'role': 'clinic_user',
'clinic_id': 'clinic_dongting',
'real_name': '岳玲',
'clinic_name': '东亭门诊'
},
# 马山门诊 (2名用户)
{
'username': 'jijunlin',
'password': 'jijunlin123',
'role': 'clinic_user',
'clinic_id': 'clinic_mashan',
'real_name': '季军林',
'clinic_name': '马山门诊'
},
{
'username': 'zhouliping',
'password': 'zhouliping123',
'role': 'clinic_user',
'clinic_id': 'clinic_mashan',
'real_name': '周丽萍',
'clinic_name': '马山门诊'
},
# 通善口腔医院总院 (2名用户)
{
'username': 'feimiaomiao',
'password': 'feimiaomiao123',
'role': 'clinic_user',
'clinic_id': 'clinic_hospital',
'real_name': '费苗苗',
'clinic_name': '通善口腔医院'
},
{
'username': 'chenxinyu',
'password': 'chenxinyu123',
'role': 'clinic_user',
'clinic_id': 'clinic_helai',
'real_name': '陈心语',
'clinic_name': '河埒门诊'
},
# 惠山门诊 (2名用户)
{
'username': 'yanghong',
'password': 'yanghong123',
'role': 'clinic_user',
'clinic_id': 'clinic_huishan',
'real_name': '杨红',
'clinic_name': '惠山门诊'
},
{
'username': 'panjinli',
'password': 'panjinli123',
'role': 'clinic_user',
'clinic_id': 'clinic_huishan',
'real_name': '潘金丽',
'clinic_name': '惠山门诊'
},
# 大丰门诊 (1名用户)
{
'username': 'chenlin',
'password': 'chenlin123',
'role': 'clinic_user',
'clinic_id': 'clinic_dafeng',
'real_name': '陈琳',
'clinic_name': '大丰门诊'
},
# 总部管理员 (最高权限)
{
'username': 'admin',
'password': 'admin123',
'role': 'admin',
'clinic_id': None,
'real_name': '系统管理员',
'clinic_name': '总部管理'
}
]
# 系统配置
SYSTEM_CONFIG = {
'total_clinics': len(CLINIC_MAPPING),
'total_expected_patients': sum(info['expected_patients'] for info in CLINIC_MAPPING.values()),
'base_url': '/patient_profiles',
'shared_resources_path': '/shared',
'admin_path': '/admin'
}
def get_clinic_info(clinic_id):
"""根据clinic_id获取门诊信息"""
for info in CLINIC_MAPPING.values():
if info['clinic_id'] == clinic_id:
return info
return None
def get_clinic_by_name(clinic_name):
"""根据门诊名称获取门诊信息"""
return CLINIC_MAPPING.get(clinic_name)
def get_all_clinic_ids():
"""获取所有门诊ID列表"""
return [info['clinic_id'] for info in CLINIC_MAPPING.values()]
def get_user_by_username(username):
"""根据用户名获取用户信息"""
for user in DEFAULT_USERS:
if user['username'] == username:
return user
return None
if __name__ == "__main__":
print("🏥 门诊配置信息:")
print(f"门诊总数: {SYSTEM_CONFIG['total_clinics']}")
print(f"预期患者总数: {SYSTEM_CONFIG['total_expected_patients']}")
print(f"用户总数: {len(DEFAULT_USERS)}")
print("\n📋 门诊列表:")
for clinic_id in get_all_clinic_ids():
info = get_clinic_info(clinic_id)
print(f" {info['clinic_name']} ({clinic_id}) - {info['expected_patients']} 人")
print("\n👥 用户列表:")
for user in DEFAULT_USERS:
clinic_name = user.get('clinic_name', '总部')
print(f" {user['username']} ({user['real_name']}) - {user['role']} - {clinic_name}")
\ No newline at end of file
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
应用配置管理
"""
import os
class Config:
"""基础配置"""
SECRET_KEY = os.environ.get('SECRET_KEY') or 'dev-secret-key-change-in-production'
# 数据库配置
DB_HOST = os.environ.get('DB_HOST', 'localhost')
DB_PORT = int(os.environ.get('DB_PORT', '3306'))
DB_USER = os.environ.get('DB_USER', 'callback_user')
DB_PASSWORD = os.environ.get('DB_PASSWORD', 'dev_password_123')
DB_NAME = os.environ.get('DB_NAME', 'callback_system')
DB_CHARSET = os.environ.get('DB_CHARSET', 'utf8mb4')
# SQLAlchemy 配置
SQLALCHEMY_DATABASE_URI = f"mysql+pymysql://{DB_USER}:{DB_PASSWORD}@{DB_HOST}:{DB_PORT}/{DB_NAME}?charset={DB_CHARSET}"
SQLALCHEMY_TRACK_MODIFICATIONS = False
SQLALCHEMY_ENGINE_OPTIONS = {
'pool_pre_ping': True,
'pool_recycle': 300,
}
class DevelopmentConfig(Config):
"""开发环境配置"""
DEBUG = True
FLASK_ENV = 'development'
class ProductionConfig(Config):
"""生产环境配置"""
DEBUG = False
FLASK_ENV = 'production'
# 配置映射
config = {
'development': DevelopmentConfig,
'production': ProductionConfig,
'default': DevelopmentConfig
}
# 门诊配置
CLINIC_CONFIG = {
'clinic_xuexian': {'clinic_name': '学前街门诊'},
'clinic_dafeng': {'clinic_name': '大丰门诊'},
'clinic_dongting': {'clinic_name': '东亭门诊'},
'clinic_helai': {'clinic_name': '河埒门诊'},
'clinic_hongdou': {'clinic_name': '红豆门诊'},
'clinic_huishan': {'clinic_name': '惠山门诊'},
'clinic_mashan': {'clinic_name': '马山门诊'},
'clinic_hospital': {'clinic_name': '通善口腔医院'},
'clinic_xinwu': {'clinic_name': '新吴门诊'}
}
# 用户配置
DEFAULT_USERS = [
# 学前街门诊 (3名用户)
{'username': 'jinqin', 'password': 'jinqin123', 'role': 'clinic_user', 'clinic_id': 'clinic_xuexian', 'real_name': '金沁', 'clinic_name': '学前街门诊'},
{'username': 'renshanshan', 'password': 'renshanshan123', 'role': 'clinic_user', 'clinic_id': 'clinic_xuexian', 'real_name': '任姗姗', 'clinic_name': '学前街门诊'},
{'username': 'shaojun', 'password': 'shaojun123', 'role': 'clinic_user', 'clinic_id': 'clinic_xuexian', 'real_name': '邵军', 'clinic_name': '学前街门诊'},
# 新吴门诊 (1名用户)
{'username': 'litingting', 'password': 'litingting123', 'role': 'clinic_user', 'clinic_id': 'clinic_xinwu', 'real_name': '李婷婷', 'clinic_name': '新吴门诊'},
# 红豆门诊 (2名用户)
{'username': 'maqiuyi', 'password': 'maqiuyi123', 'role': 'clinic_user', 'clinic_id': 'clinic_hongdou', 'real_name': '马秋怡', 'clinic_name': '红豆门诊'},
{'username': 'tangqimin', 'password': 'tangqimin123', 'role': 'clinic_user', 'clinic_id': 'clinic_hongdou', 'real_name': '汤其敏', 'clinic_name': '红豆门诊'},
# 东亭门诊 (1名用户)
{'username': 'yueling', 'password': 'yueling123', 'role': 'clinic_user', 'clinic_id': 'clinic_dongting', 'real_name': '岳玲', 'clinic_name': '东亭门诊'},
# 马山门诊 (2名用户)
{'username': 'jijunlin', 'password': 'jijunlin123', 'role': 'clinic_user', 'clinic_id': 'clinic_mashan', 'real_name': '季军林', 'clinic_name': '马山门诊'},
{'username': 'zhouliping', 'password': 'zhouliping123', 'role': 'clinic_user', 'clinic_id': 'clinic_mashan', 'real_name': '周丽萍', 'clinic_name': '马山门诊'},
# 通善口腔医院总院 (2名用户)
{'username': 'feimiaomiao', 'password': 'feimiaomiao123', 'role': 'clinic_user', 'clinic_id': 'clinic_hospital', 'real_name': '费苗苗', 'clinic_name': '通善口腔医院'},
{'username': 'chenxinyu', 'password': 'chenxinyu123', 'role': 'clinic_user', 'clinic_id': 'clinic_helai', 'real_name': '陈心语', 'clinic_name': '河埒门诊'},
# 惠山门诊 (2名用户)
{'username': 'yanghong', 'password': 'yanghong123', 'role': 'clinic_user', 'clinic_id': 'clinic_huishan', 'real_name': '杨红', 'clinic_name': '惠山门诊'},
{'username': 'panjinli', 'password': 'panjinli123', 'role': 'clinic_user', 'clinic_id': 'clinic_huishan', 'real_name': '潘金丽', 'clinic_name': '惠山门诊'},
# 大丰门诊 (1名用户)
{'username': 'chenlin', 'password': 'chenlin123', 'role': 'clinic_user', 'clinic_id': 'clinic_dafeng', 'real_name': '陈琳', 'clinic_name': '大丰门诊'},
# 总部管理员 (最高权限)
{'username': 'admin', 'password': 'admin123', 'role': 'admin', 'clinic_id': None, 'real_name': '系统管理员', 'clinic_name': '总部管理'}
]
def get_user_by_username(username):
"""根据用户名获取用户信息"""
for user in DEFAULT_USERS:
if user['username'] == username:
return user
return None
def get_clinic_info(clinic_id):
"""根据clinic_id获取门诊信息"""
return CLINIC_CONFIG.get(clinic_id, {'clinic_name': '未知门诊'})
-- 批量创建诊所用户SQL脚本
USE callback_system;
-- 学前街门诊用户
INSERT INTO users (username, password_hash, user_type, clinic_access, created_at, is_active)
VALUES
('jinqin', SHA2('jinqin123', 256), 'user', '["clinic_xuexian"]', NOW(), 1),
('renshanshan', SHA2('renshanshan123', 256), 'user', '["clinic_xuexian"]', NOW(), 1),
('shaojun', SHA2('shaojun123', 256), 'user', '["clinic_xuexian"]', NOW(), 1)
ON DUPLICATE KEY UPDATE
password_hash = VALUES(password_hash),
clinic_access = VALUES(clinic_access),
updated_at = NOW();
-- 新吴门诊用户
INSERT INTO users (username, password_hash, user_type, clinic_access, created_at, is_active)
VALUES
('litingting', SHA2('litingting123', 256), 'user', '["clinic_xinwu"]', NOW(), 1)
ON DUPLICATE KEY UPDATE
password_hash = VALUES(password_hash),
clinic_access = VALUES(clinic_access),
updated_at = NOW();
-- 红豆门诊用户
INSERT INTO users (username, password_hash, user_type, clinic_access, created_at, is_active)
VALUES
('maqiuyi', SHA2('maqiuyi123', 256), 'user', '["clinic_hongdou"]', NOW(), 1),
('tangqimin', SHA2('tangqimin123', 256), 'user', '["clinic_hongdou"]', NOW(), 1)
ON DUPLICATE KEY UPDATE
password_hash = VALUES(password_hash),
clinic_access = VALUES(clinic_access),
updated_at = NOW();
-- 东亭门诊用户
INSERT INTO users (username, password_hash, user_type, clinic_access, created_at, is_active)
VALUES
('yueling', SHA2('yueling123', 256), 'user', '["clinic_dongting"]', NOW(), 1)
ON DUPLICATE KEY UPDATE
password_hash = VALUES(password_hash),
clinic_access = VALUES(clinic_access),
updated_at = NOW();
-- 马山门诊用户
INSERT INTO users (username, password_hash, user_type, clinic_access, created_at, is_active)
VALUES
('jijunlin', SHA2('jijunlin123', 256), 'user', '["clinic_mashan"]', NOW(), 1),
('zhouliping', SHA2('zhouliping123', 256), 'user', '["clinic_mashan"]', NOW(), 1)
ON DUPLICATE KEY UPDATE
password_hash = VALUES(password_hash),
clinic_access = VALUES(clinic_access),
updated_at = NOW();
-- 总院用户
INSERT INTO users (username, password_hash, user_type, clinic_access, created_at, is_active)
VALUES
('feimiaomiao', SHA2('feimiaomiao123', 256), 'user', '["clinic_hospital"]', NOW(), 1),
('chenxinyu', SHA2('chenxinyu123', 256), 'user', '["clinic_hospital"]', NOW(), 1)
ON DUPLICATE KEY UPDATE
password_hash = VALUES(password_hash),
clinic_access = VALUES(clinic_access),
updated_at = NOW();
-- 惠山门诊用户
INSERT INTO users (username, password_hash, user_type, clinic_access, created_at, is_active)
VALUES
('yanghong', SHA2('yanghong123', 256), 'user', '["clinic_huishan"]', NOW(), 1),
('panjinli', SHA2('panjinli123', 256), 'user', '["clinic_huishan"]', NOW(), 1)
ON DUPLICATE KEY UPDATE
password_hash = VALUES(password_hash),
clinic_access = VALUES(clinic_access),
updated_at = NOW();
-- 大丰门诊用户
INSERT INTO users (username, password_hash, user_type, clinic_access, created_at, is_active)
VALUES
('chenlin', SHA2('chenlin123', 256), 'user', '["clinic_dafeng"]', NOW(), 1)
ON DUPLICATE KEY UPDATE
password_hash = VALUES(password_hash),
clinic_access = VALUES(clinic_access),
updated_at = NOW();
-- 查看创建结果
SELECT
username,
user_type,
clinic_access,
created_at,
is_active
FROM users
WHERE user_type = 'user'
ORDER BY created_at DESC;
\ No newline at end of file
USE callback_system;
-- 清空现有用户表
TRUNCATE TABLE users;
-- 创建管理员用户 (admin/admin123)
INSERT INTO users (username, password_hash, user_type, clinic_access, created_at, is_active)
VALUES ('admin', 'admin123', 'admin', '["all"]', NOW(), 1);
-- 学前街门诊用户
INSERT INTO users (username, password_hash, user_type, clinic_access, created_at, is_active) VALUES
('jinqin', 'jinqin123', 'user', '["clinic_xuexian"]', NOW(), 1),
('renshanshan', 'renshanshan123', 'user', '["clinic_xuexian"]', NOW(), 1),
('shaojun', 'shaojun123', 'user', '["clinic_xuexian"]', NOW(), 1);
-- 新吴门诊用户
INSERT INTO users (username, password_hash, user_type, clinic_access, created_at, is_active) VALUES
('litingting', 'litingting123', 'user', '["clinic_xinwu"]', NOW(), 1);
-- 红豆门诊用户
INSERT INTO users (username, password_hash, user_type, clinic_access, created_at, is_active) VALUES
('maqiuyi', 'maqiuyi123', 'user', '["clinic_hongdou"]', NOW(), 1),
('tangqimin', 'tangqimin123', 'user', '["clinic_hongdou"]', NOW(), 1);
-- 东亭门诊用户
INSERT INTO users (username, password_hash, user_type, clinic_access, created_at, is_active) VALUES
('yueling', 'yueling123', 'user', '["clinic_dongting"]', NOW(), 1);
-- 马山门诊用户
INSERT INTO users (username, password_hash, user_type, clinic_access, created_at, is_active) VALUES
('jijunlin', 'jijunlin123', 'user', '["clinic_mashan"]', NOW(), 1),
('zhouliping', 'zhouliping123', 'user', '["clinic_mashan"]', NOW(), 1);
-- 总院用户
INSERT INTO users (username, password_hash, user_type, clinic_access, created_at, is_active) VALUES
('feimiaomiao', 'feimiaomiao123', 'user', '["clinic_hospital"]', NOW(), 1),
('chenxinyu', 'chenxinyu123', 'user', '["clinic_hospital"]', NOW(), 1);
-- 惠山门诊用户
INSERT INTO users (username, password_hash, user_type, clinic_access, created_at, is_active) VALUES
('yanghong', 'yanghong123', 'user', '["clinic_huishan"]', NOW(), 1),
('panjinli', 'panjinli123', 'user', '["clinic_huishan"]', NOW(), 1);
-- 大丰门诊用户
INSERT INTO users (username, password_hash, user_type, clinic_access, created_at, is_active) VALUES
('chenlin', 'chenlin123', 'user', '["clinic_dafeng"]', NOW(), 1);
\ No newline at end of file
...@@ -531,7 +531,7 @@ def main(): ...@@ -531,7 +531,7 @@ def main():
# 加载患者数据 # 加载患者数据
try: try:
# 尝试加载现有的患者数据文件 # 尝试加载现有的患者数据文件
data_files = ['漏诊客户画像.json', '合并结果.json'] data_files = ['漏诊客户画像.json', 'data/patients/merged/合并结果.json']
patients_data = None patients_data = None
for file_name in data_files: for file_name in data_files:
......
...@@ -30,7 +30,7 @@ class DifyConfig: ...@@ -30,7 +30,7 @@ class DifyConfig:
self.USER_ID = "callback_system" # 默认用户ID self.USER_ID = "callback_system" # 默认用户ID
# 输出配置 # 输出配置
self.OUTPUT_DIR = "dify_callback_results" # 结果输出目录 self.OUTPUT_DIR = "data/callbacks" # 结果输出目录
self.LOG_LEVEL = "INFO" # 日志级别 self.LOG_LEVEL = "INFO" # 日志级别
def get_headers(self) -> Dict[str, str]: def get_headers(self) -> Dict[str, str]:
......
...@@ -12,7 +12,7 @@ from typing import Dict, Any ...@@ -12,7 +12,7 @@ from typing import Dict, Any
class DatabaseConfig: class DatabaseConfig:
"""数据库配置管理类""" """数据库配置管理类"""
def __init__(self, config_file: str = "database_config.ini"): def __init__(self, config_file: str = "database/config/database_config.ini"):
""" """
初始化配置管理器 初始化配置管理器
......
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
SQLAlchemy数据库模型
定义所有数据库表的结构,用于Flask-Migrate自动迁移
"""
from flask_sqlalchemy import SQLAlchemy
from datetime import datetime
import json
db = SQLAlchemy()
class User(db.Model):
"""用户管理表"""
__tablename__ = 'users'
user_id = db.Column(db.Integer, primary_key=True, comment='用户ID')
username = db.Column(db.String(50), unique=True, nullable=False, comment='用户名')
password_hash = db.Column(db.String(255), nullable=False, comment='密码哈希')
user_type = db.Column(db.String(20), default='user', comment='用户类型')
clinic_access = db.Column(db.JSON, comment='诊所访问权限')
created_at = db.Column(db.DateTime, default=datetime.utcnow, comment='创建时间')
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, comment='更新时间')
is_active = db.Column(db.Boolean, default=True, comment='是否激活')
last_login = db.Column(db.DateTime, comment='最后登录时间')
# 索引
__table_args__ = (
db.Index('idx_user_type', 'user_type'),
db.Index('idx_is_active', 'is_active'),
{'comment': '用户管理表'}
)
def __repr__(self):
return f'<User {self.username}>'
class Clinic(db.Model):
"""诊所信息表"""
__tablename__ = 'clinics'
clinic_id = db.Column(db.Integer, primary_key=True, comment='诊所ID')
clinic_name = db.Column(db.String(100), nullable=False, comment='诊所名称')
clinic_code = db.Column(db.String(50), unique=True, nullable=False, comment='诊所代码')
clinic_address = db.Column(db.String(255), comment='诊所地址')
clinic_phone = db.Column(db.String(20), comment='诊所电话')
created_at = db.Column(db.DateTime, default=datetime.utcnow, comment='创建时间')
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, comment='更新时间')
is_active = db.Column(db.Boolean, default=True, comment='是否激活')
# 关系
patients = db.relationship('Patient', backref='clinic', lazy=True)
# 索引
__table_args__ = (
db.Index('idx_is_active', 'is_active'),
{'comment': '诊所信息表'}
)
def __repr__(self):
return f'<Clinic {self.clinic_name}>'
class Patient(db.Model):
"""患者信息表"""
__tablename__ = 'patients'
patient_id = db.Column(db.Integer, primary_key=True, comment='患者ID')
case_number = db.Column(db.String(50), unique=True, nullable=False, comment='病历号')
patient_name = db.Column(db.String(100), comment='患者姓名')
patient_phone = db.Column(db.String(20), comment='患者电话')
gender = db.Column(db.String(10), comment='性别')
age = db.Column(db.Integer, comment='年龄')
clinic_id = db.Column(db.Integer, db.ForeignKey('clinics.clinic_id'), comment='诊所ID')
clinic_name = db.Column(db.String(100), comment='诊所名称')
diagnosis = db.Column(db.JSON, comment='诊断信息')
treatment_date = db.Column(db.Date, comment='治疗日期')
created_at = db.Column(db.DateTime, default=datetime.utcnow, comment='创建时间')
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, comment='更新时间')
# 关系
callback_records = db.relationship('CallbackRecord', backref='patient', lazy=True, cascade='all, delete-orphan')
# 索引
__table_args__ = (
db.Index('idx_patient_name', 'patient_name'),
db.Index('idx_clinic_name', 'clinic_name'),
db.Index('idx_treatment_date', 'treatment_date'),
{'comment': '患者信息表'}
)
def to_dict(self):
"""转换为字典格式,兼容原有的JSON数据格式"""
return {
'姓名': self.patient_name,
'年龄': self.age,
'性别': self.gender,
'病历号': self.case_number,
'电话': self.patient_phone,
'门诊名称': self.clinic_name,
'诊断信息': self.diagnosis,
'治疗日期': self.treatment_date.isoformat() if self.treatment_date else None,
'创建时间': self.created_at.isoformat() if self.created_at else None,
'更新时间': self.updated_at.isoformat() if self.updated_at else None
}
def __repr__(self):
return f'<Patient {self.patient_name} ({self.case_number})>'
class CallbackRecord(db.Model):
"""回访记录表"""
__tablename__ = 'callback_records'
record_id = db.Column(db.Integer, primary_key=True, comment='记录ID')
case_number = db.Column(db.String(50), db.ForeignKey('patients.case_number'), nullable=False, comment='病历号')
callback_methods = db.Column(db.JSON, nullable=False, comment='回访方式(JSON格式)')
callback_record = db.Column(db.Text, nullable=False, comment='回访记录内容')
callback_result = db.Column(db.String(50), nullable=False, comment='回访结果(成功/不成功/放弃回访)')
next_appointment_time = db.Column(db.Text, comment='下次预约时间')
failure_reason = db.Column(db.Text, comment='不成功的原因')
abandon_reason = db.Column(db.Text, comment='放弃回访的原因')
ai_feedback_type = db.Column(db.String(100), comment='AI错误反馈类型')
failure_reason_note = db.Column(db.Text, comment='不成功备注')
abandon_reason_note = db.Column(db.Text, comment='放弃回访备注')
ai_feedback_note = db.Column(db.Text, comment='AI反馈备注')
callback_status = db.Column(db.String(50), nullable=False, default='已回访', comment='回访状态')
operator = db.Column(db.String(100), nullable=False, comment='操作员')
create_time = db.Column(db.DateTime, default=datetime.utcnow, comment='创建时间')
update_time = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, comment='更新时间')
# 索引
__table_args__ = (
db.Index('idx_case_number', 'case_number'),
db.Index('idx_create_time', 'create_time'),
db.Index('idx_operator', 'operator'),
db.Index('idx_callback_result', 'callback_result'),
{'comment': '回访记录表'}
)
@property
def callback_success(self):
"""为了向后兼容,提供callback_success属性"""
return self.callback_result == "成功"
def to_dict(self):
"""转换为字典格式"""
return {
'record_id': self.record_id,
'case_number': self.case_number,
'callback_methods': self.callback_methods if isinstance(self.callback_methods, list) else json.loads(self.callback_methods or '[]'),
'callback_record': self.callback_record,
'callback_result': self.callback_result,
'callback_success': self.callback_success,
'callback_status': self.callback_status,
'next_appointment_time': self.next_appointment_time,
'failure_reason': self.failure_reason,
'abandon_reason': self.abandon_reason,
'ai_feedback_type': self.ai_feedback_type,
'failure_reason_note': self.failure_reason_note,
'abandon_reason_note': self.abandon_reason_note,
'ai_feedback_note': self.ai_feedback_note,
'operator': self.operator,
'operator_name': self.operator, # 前端期望的字段名
'create_time': self.create_time.isoformat() if self.create_time else None,
'update_time': self.update_time.isoformat() if self.update_time else None
}
def __repr__(self):
return f'<CallbackRecord {self.case_number} - {self.callback_result}>'
# 数据库初始化函数
def init_db(app):
"""初始化数据库"""
db.init_app(app)
with app.app_context():
# 创建所有表
db.create_all()
# 默认数据插入由 app.py 中的 insert_default_data() 处理
# 默认数据插入功能已移至 app.py 中的 insert_default_data() 函数
# 避免重复插入门诊数据
#!/bin/bash
# 容器启动脚本 - 零配置自动迁移
# 功能:
# 1. 等待数据库就绪
# 2. 自动执行数据库迁移
# 3. 启动Flask应用
set -e # 遇到错误立即退出
# 颜色输出
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"
}
log_success() {
echo -e "${GREEN}[SUCCESS]${NC} $(date '+%Y-%m-%d %H:%M:%S') - $1"
}
log_warning() {
echo -e "${YELLOW}[WARNING]${NC} $(date '+%Y-%m-%d %H:%M:%S') - $1"
}
log_error() {
echo -e "${RED}[ERROR]${NC} $(date '+%Y-%m-%d %H:%M:%S') - $1"
}
# 错误处理函数
handle_error() {
log_error "容器启动过程中发生错误!"
log_error "错误发生在第 $1 行"
exit 1
}
# 设置错误处理
trap 'handle_error $LINENO' ERR
log_info "🚀 患者画像回访话术系统 - 容器启动"
log_info "📍 工作目录: $(pwd)"
log_info "📍 Python版本: $(python --version)"
# 第一步:等待数据库就绪
log_info "📋 第一步:等待数据库就绪..."
python database/scripts/wait-for-db.py
log_success "数据库连接就绪"
# 第二步:初始化Flask-Migrate(如果需要)
log_info "📋 第二步:检查Flask-Migrate状态..."
# 检查migrations目录是否存在
if [ ! -d "migrations" ]; then
log_info "初始化Flask-Migrate..."
export FLASK_APP=app.py
flask db init
log_success "Flask-Migrate初始化完成"
else
log_info "Flask-Migrate已初始化"
fi
# 第三步:检查是否需要生成初始迁移
log_info "📋 第三步:检查迁移文件..."
# 检查是否有迁移文件
MIGRATION_FILES=$(find migrations/versions -name "*.py" 2>/dev/null | wc -l)
if [ "$MIGRATION_FILES" -eq 0 ]; then
log_info "生成初始迁移文件..."
export FLASK_APP=app.py
flask db migrate -m "Initial migration"
log_success "初始迁移文件生成完成"
else
log_info "发现 $MIGRATION_FILES 个迁移文件"
fi
# 第四步:执行数据库迁移
log_info "📋 第四步:执行数据库迁移..."
export FLASK_APP=app.py
# 检查数据库当前状态
log_info "检查数据库迁移状态..."
flask db current || log_warning "无法获取当前迁移状态(可能是首次运行)"
# 执行迁移
log_info "执行数据库迁移..."
flask db upgrade
log_success "数据库迁移完成"
# 第五步:数据导入(如果需要)
log_info "📋 第五步:检查数据导入需求..."
# 检查是否有患者数据需要导入
if [ -f "database/scripts/safe_import_patients.py" ] && [ -d "data/patients/clinics" ]; then
log_info "发现患者数据,执行数据导入..."
python database/scripts/safe_import_patients.py || log_warning "数据导入失败,但继续启动应用"
log_success "数据导入完成"
else
log_info "无需数据导入"
fi
# 第六步:启动Flask应用
log_info "📋 第六步:启动Flask应用..."
log_info "🌐 应用将在端口 5000 启动"
log_info "🔗 健康检查端点: http://localhost:5000/api/health"
# 启动应用
log_success "🎉 容器启动完成,开始运行应用..."
exec python app.py
...@@ -6,6 +6,7 @@ ...@@ -6,6 +6,7 @@
import os import os
import json import json
import io
from datetime import datetime from datetime import datetime
from typing import Dict, List, Any from typing import Dict, List, Any
import pymysql import pymysql
...@@ -174,6 +175,38 @@ class DataExporter: ...@@ -174,6 +175,38 @@ class DataExporter:
# 保存文件 # 保存文件
wb.save(output_path) wb.save(output_path)
return output_path return output_path
def export_to_memory(self) -> bytes:
"""导出数据到内存缓冲区,返回二进制数据"""
# 获取数据
clinic_data = self.get_clinic_data()
# 创建工作簿
wb = Workbook()
# 创建总览表
self._create_summary_sheet(wb, clinic_data)
# 创建各诊所详细表
print(f"开始创建诊所详细表...")
for clinic_name, data in clinic_data.items():
if data['records']:
print(f"创建诊所表: {clinic_name} - {data['total_count']} 条记录)")
self._create_clinic_sheet(wb, clinic_name, data)
else:
print(f"跳过空诊所: {clinic_name} - {data['total_count']} 条记录)")
print(f"Excel工作表列表: {wb.sheetnames}")
# 删除默认的Sheet
if 'Sheet' in wb.sheetnames:
wb.remove(wb['Sheet'])
# 保存到内存缓冲区
buffer = io.BytesIO()
wb.save(buffer)
buffer.seek(0)
return buffer.getvalue()
def _create_summary_sheet(self, wb: Workbook, clinic_data: Dict[str, Any]): def _create_summary_sheet(self, wb: Workbook, clinic_data: Dict[str, Any]):
"""创建总览表""" """创建总览表"""
......
...@@ -250,15 +250,15 @@ def main(): ...@@ -250,15 +250,15 @@ def main():
# 定义门诊和对应的JSON文件 # 定义门诊和对应的JSON文件
clinics = [ clinics = [
('学前街门诊', '学前街门诊.json'), # 根目录文件 ('学前街门诊', 'data/patients/clinics/学前街门诊.json'),
('大丰门诊', '诊所患者json/大丰门诊.json'), ('大丰门诊', 'data/patients/clinics/大丰门诊.json'),
('东亭门诊', '诊所患者json/东亭门诊.json'), ('东亭门诊', 'data/patients/clinics/东亭门诊.json'),
('河埒门诊', '诊所患者json/河埒门诊.json'), ('河埒门诊', 'data/patients/clinics/河埒门诊.json'),
('红豆门诊', '诊所患者json/红豆门诊.json'), ('红豆门诊', 'data/patients/clinics/红豆门诊.json'),
('惠山门诊', '诊所患者json/惠山门诊.json'), ('惠山门诊', 'data/patients/clinics/惠山门诊.json'),
('马山门诊', '诊所患者json/马山门诊.json'), ('马山门诊', 'data/patients/clinics/马山门诊.json'),
('通善口腔医院', '诊所患者json/通善口腔医院.json'), ('通善口腔医院', 'data/patients/clinics/通善口腔医院.json'),
('新吴门诊', '诊所患者json/新吴门诊.json') ('新吴门诊', 'data/patients/clinics/新吴门诊.json')
] ]
total_added = 0 total_added = 0
......
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
数据库等待脚本
等待MySQL数据库完全启动并可连接后再继续
"""
import os
import sys
import time
import pymysql
from datetime import datetime
def get_db_config():
"""获取数据库配置"""
return {
'host': os.getenv('DB_HOST', 'mysql'),
'port': int(os.getenv('DB_PORT', 3306)),
'user': os.getenv('DB_USER', 'callback_user'),
'password': os.getenv('DB_PASSWORD', 'dev_password_123'),
'database': os.getenv('DB_NAME', 'callback_system'),
'charset': os.getenv('DB_CHARSET', 'utf8mb4')
}
def test_connection(config, max_retries=30, retry_interval=2):
"""
测试数据库连接
Args:
config: 数据库配置
max_retries: 最大重试次数
retry_interval: 重试间隔(秒)
Returns:
bool: 连接是否成功
"""
print(f"🔍 等待数据库连接就绪...")
print(f"📍 数据库地址: {config['host']}:{config['port']}")
print(f"📍 数据库名称: {config['database']}")
print(f"📍 最大重试次数: {max_retries}")
for attempt in range(1, max_retries + 1):
try:
print(f"⏳ 尝试连接数据库 ({attempt}/{max_retries})...")
# 尝试连接数据库
connection = pymysql.connect(**config)
# 测试基本查询
with connection.cursor() as cursor:
cursor.execute("SELECT 1")
result = cursor.fetchone()
connection.close()
if result:
print(f"✅ 数据库连接成功!(耗时: {(attempt-1) * retry_interval}秒)")
return True
except pymysql.Error as e:
error_code = getattr(e, 'args', [None])[0]
# 常见的连接错误
if error_code in [2003, 2006, 2013]: # 连接被拒绝、连接丢失、连接超时
print(f"⏳ 数据库尚未就绪,等待 {retry_interval} 秒后重试...")
elif error_code == 1049: # 数据库不存在
print(f"❌ 数据库 '{config['database']}' 不存在")
return False
elif error_code == 1045: # 访问被拒绝
print(f"❌ 数据库认证失败,请检查用户名和密码")
return False
else:
print(f"❌ 数据库连接错误: {e}")
if attempt < max_retries:
time.sleep(retry_interval)
else:
print(f"❌ 数据库连接失败,已达到最大重试次数 ({max_retries})")
return False
except Exception as e:
print(f"❌ 未知错误: {e}")
if attempt < max_retries:
time.sleep(retry_interval)
else:
return False
return False
def check_database_exists(config):
"""检查数据库是否存在"""
try:
# 连接到MySQL服务器(不指定数据库)
server_config = config.copy()
del server_config['database']
connection = pymysql.connect(**server_config)
with connection.cursor() as cursor:
cursor.execute("SHOW DATABASES LIKE %s", (config['database'],))
result = cursor.fetchone()
connection.close()
if result:
print(f"✅ 数据库 '{config['database']}' 存在")
return True
else:
print(f"❌ 数据库 '{config['database']}' 不存在")
return False
except Exception as e:
print(f"❌ 检查数据库存在性失败: {e}")
return False
def create_database_if_not_exists(config):
"""如果数据库不存在则创建"""
try:
# 连接到MySQL服务器(不指定数据库)
server_config = config.copy()
del server_config['database']
connection = pymysql.connect(**server_config)
with connection.cursor() as cursor:
# 创建数据库
cursor.execute(f"""
CREATE DATABASE IF NOT EXISTS `{config['database']}`
CHARACTER SET utf8mb4
COLLATE utf8mb4_unicode_ci
""")
print(f"✅ 数据库 '{config['database']}' 创建成功")
connection.close()
return True
except Exception as e:
print(f"❌ 创建数据库失败: {e}")
return False
def main():
"""主函数"""
print("🚀 数据库等待脚本启动")
print(f"🕐 启动时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
# 获取数据库配置
config = get_db_config()
# 首先等待MySQL服务器启动
print("\n📋 第一步:等待MySQL服务器启动...")
server_config = config.copy()
del server_config['database']
if not test_connection(server_config):
print("❌ MySQL服务器连接失败")
sys.exit(1)
# 检查并创建数据库
print("\n📋 第二步:检查数据库存在性...")
if not check_database_exists(config):
print("📋 数据库不存在,尝试创建...")
if not create_database_if_not_exists(config):
print("❌ 数据库创建失败")
sys.exit(1)
# 测试完整的数据库连接
print("\n📋 第三步:测试完整数据库连接...")
if not test_connection(config):
print("❌ 数据库连接失败")
sys.exit(1)
print("\n🎉 数据库就绪!可以开始应用启动流程")
print(f"🕐 完成时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
return 0
if __name__ == "__main__":
sys.exit(main())
#!/bin/bash
# 患者画像回访话术系统 - Docker快速部署脚本
set -e # 遇到错误立即退出
echo "🚀 患者画像回访话术系统 - Docker快速部署"
echo "================================================"
# 检查Docker环境
echo ""
echo "📋 检查Docker环境..."
if ! command -v docker &> /dev/null; then
echo "❌ 错误:未检测到Docker,请先安装Docker"
echo "安装指南:https://docs.docker.com/get-docker/"
exit 1
fi
if ! command -v docker-compose &> /dev/null; then
echo "❌ 错误:未检测到Docker Compose"
echo "安装指南:https://docs.docker.com/compose/install/"
exit 1
fi
echo "✅ Docker环境检查通过"
docker --version
docker-compose --version
# 停止现有服务
echo ""
echo "🔧 停止现有服务..."
docker-compose down || true
# 构建镜像
echo ""
echo "🏗️ 构建镜像..."
docker-compose build
# 启动服务
echo ""
echo "🚀 启动服务..."
docker-compose up -d
# 等待服务启动
echo ""
echo "⏳ 等待服务启动完成..."
sleep 10
# 检查服务状态
echo ""
echo "📊 检查服务状态..."
docker-compose ps
# 运行测试
echo ""
echo "🧪 运行部署测试..."
if command -v python3 &> /dev/null; then
python3 test_docker_deployment.py
elif command -v python &> /dev/null; then
python test_docker_deployment.py
else
echo "⚠️ 未找到Python,跳过自动测试"
echo "请手动访问 http://localhost:5000 验证部署"
fi
echo ""
echo "🎉 部署完成!"
echo ""
echo "🌐 访问地址:"
echo " 主应用:http://localhost:5000"
echo " 登录页面:http://localhost:5000/login"
echo " 患者画像:http://localhost:5000/patient_profiles/"
echo ""
echo "📖 更多信息请查看 README_DOCKER.md"
echo ""
# 询问是否打开浏览器
read -p "是否打开浏览器访问系统?(y/n): " -n 1 -r
echo
if [[ $REPLY =~ ^[Yy]$ ]]; then
if command -v xdg-open &> /dev/null; then
xdg-open http://localhost:5000
elif command -v open &> /dev/null; then
open http://localhost:5000
else
echo "请手动打开浏览器访问:http://localhost:5000"
fi
fi
\ No newline at end of file
#!/bin/bash
# 生产环境部署脚本(包含数据库备份)
# 使用方法:./deploy_with_backup.sh
set -e # 遇到错误立即退出
# 颜色定义
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} $1"
}
log_success() {
echo -e "${GREEN}[SUCCESS]${NC} $1"
}
log_warning() {
echo -e "${YELLOW}[WARNING]${NC} $1"
}
log_error() {
echo -e "${RED}[ERROR]${NC} $1"
}
# 配置信息
PROJECT_DIR="customer-recall"
BACKUP_DIR="/backup/database"
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
BACKUP_FILE="production_backup_${TIMESTAMP}.sql"
# 数据库配置(从环境变量获取)
DB_HOST="${DB_HOST:-localhost}"
DB_PORT="${DB_PORT:-3306}"
DB_USER="${DB_USER:-callback_user}"
DB_NAME="${DB_NAME:-callback_system}"
log_info "🚀 开始生产环境部署流程..."
log_info "📅 部署时间: $(date)"
log_info "🏥 项目目录: $PROJECT_DIR"
log_info "🗄️ 数据库: $DB_NAME@$DB_HOST:$DB_PORT"
# 第一步:创建备份目录
log_info "📁 创建备份目录..."
mkdir -p "$BACKUP_DIR"
log_success "备份目录创建成功: $BACKUP_DIR"
# 第二步:数据库备份
log_info "💾 开始数据库备份..."
if command -v mysqldump &> /dev/null; then
# 使用mysqldump备份
log_info "使用mysqldump备份数据库..."
# 检查数据库连接
if mysql -h"$DB_HOST" -P"$DB_PORT" -u"$DB_USER" -p"$DB_PASSWORD" -e "SELECT 1;" "$DB_NAME" &> /dev/null; then
log_success "数据库连接成功,开始备份..."
# 执行备份
mysqldump -h"$DB_HOST" -P"$DB_PORT" -u"$DB_USER" -p"$DB_PASSWORD" \
--single-transaction \
--routines \
--triggers \
--events \
--add-drop-database \
--create-options \
"$DB_NAME" > "$BACKUP_DIR/$BACKUP_FILE"
if [ $? -eq 0 ]; then
BACKUP_SIZE=$(du -h "$BACKUP_DIR/$BACKUP_FILE" | cut -f1)
log_success "数据库备份成功!"
log_info "📊 备份文件: $BACKUP_FILE"
log_info "📏 备份大小: $BACKUP_SIZE"
log_info "📍 备份路径: $BACKUP_DIR/$BACKUP_FILE"
else
log_error "数据库备份失败!"
exit 1
fi
else
log_error "无法连接到数据库,请检查配置!"
exit 1
fi
else
log_warning "mysqldump命令不存在,尝试使用Docker容器备份..."
# 使用Docker容器备份
if docker ps | grep -q mysql; then
log_info "使用Docker容器备份数据库..."
# 查找MySQL容器
MYSQL_CONTAINER=$(docker ps -q --filter "ancestor=mysql" --filter "ancestor=mariadb" | head -1)
if [ -n "$MYSQL_CONTAINER" ]; then
log_info "找到MySQL容器: $MYSQL_CONTAINER"
docker exec "$MYSQL_CONTAINER" \
mysqldump -u"$DB_USER" -p"$DB_PASSWORD" \
--single-transaction \
--routines \
--triggers \
--events \
--add-drop-database \
--create-options \
"$DB_NAME" > "$BACKUP_DIR/$BACKUP_FILE"
if [ $? -eq 0 ]; then
BACKUP_SIZE=$(du -h "$BACKUP_DIR/$BACKUP_FILE" | cut -f1)
log_success "Docker容器备份成功!"
log_info "📊 备份文件: $BACKUP_FILE"
log_info "📏 备份大小: $BACKUP_SIZE"
else
log_error "Docker容器备份失败!"
exit 1
fi
else
log_error "未找到MySQL容器,无法备份数据库!"
log_warning "跳过数据库备份,继续部署流程..."
# 创建一个空的备份文件占位符
touch "$BACKUP_DIR/$BACKUP_FILE"
log_info "创建空的备份文件占位符: $BACKUP_FILE"
fi
fi
fi
# 第三步:验证备份文件
log_info "🔍 验证备份文件..."
if [ -f "$BACKUP_DIR/$BACKUP_FILE" ]; then
if [ -s "$BACKUP_DIR/$BACKUP_FILE" ]; then
log_success "备份文件验证成功!"
else
log_warning "备份文件为空(可能是占位符),继续部署..."
fi
else
log_error "备份文件验证失败!"
exit 1
fi
# 第四步:更新代码
log_info "📥 更新项目代码..."
cd "$PROJECT_DIR"
git pull origin master
log_success "代码更新成功!"
# 第四步.5:确保诊所数据文件存在
log_info "📋 检查诊所数据文件..."
if [ ! -d "诊所患者json" ]; then
log_warning "诊所患者json目录不存在,创建目录..."
mkdir -p "诊所患者json"
fi
# 检查关键诊所文件
CLINIC_FILES=("东亭门诊.json" "大丰门诊.json" "惠山门诊.json" "新吴门诊.json" "河埒门诊.json" "红豆门诊.json" "通善口腔医院.json" "马山门诊.json" "学前街门诊.json")
for file in "${CLINIC_FILES[@]}"; do
if [ ! -f "诊所患者json/$file" ]; then
log_warning "诊所文件缺失: $file"
else
log_info "诊所文件存在: $file"
fi
done
# 第五步:部署Docker容器
log_info "🐳 部署Docker容器..."
docker compose down
docker compose up -d --build
# 等待容器启动
log_info "⏳ 等待容器启动..."
sleep 10
# 检查容器状态
if docker compose ps | grep -q "Up"; then
log_success "Docker容器启动成功!"
else
log_error "Docker容器启动失败!"
exit 1
fi
# 第六步:执行安全数据导入
log_info "📋 执行安全数据导入..."
if docker exec $(docker compose ps -q app) python safe_import_patients.py; then
log_success "安全数据导入成功!"
else
log_warning "安全数据导入失败,但部署继续..."
fi
# 第七步:清理旧备份(保留最近7天)
log_info "🧹 清理旧备份文件..."
find "$BACKUP_DIR" -name "production_backup_*.sql" -mtime +7 -delete
log_success "旧备份文件清理完成!"
# 部署完成
log_success "🎉 生产环境部署完成!"
log_info "📊 部署总结:"
log_info " ✅ 数据库备份: $BACKUP_FILE"
log_info " ✅ 代码更新: 完成"
log_info " ✅ 容器部署: 完成"
log_info " ✅ 数据导入: 完成"
log_info " 📍 备份位置: $BACKUP_DIR"
log_info " 🕐 完成时间: $(date)"
# 显示备份文件列表
log_info "📋 当前备份文件列表:"
ls -lh "$BACKUP_DIR"/production_backup_*.sql 2>/dev/null || log_warning "暂无备份文件"
\ No newline at end of file
#!/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()
...@@ -18,7 +18,7 @@ services: ...@@ -18,7 +18,7 @@ services:
MYSQL_COLLATION_SERVER: utf8mb4_unicode_ci MYSQL_COLLATION_SERVER: utf8mb4_unicode_ci
volumes: volumes:
- mysql_data:/var/lib/mysql - mysql_data:/var/lib/mysql
- ./init.sql:/docker-entrypoint-initdb.d/init.sql:ro - ./database/migrations/init.sql:/docker-entrypoint-initdb.d/init.sql:ro
ports: ports:
- "3306:3306" # Host port 3306 maps to container port 3306 - "3306:3306" # Host port 3306 maps to container port 3306
networks: networks:
......
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Docker环境数据库配置管理
支持环境变量配置,用于Docker部署
"""
import os
import configparser
from typing import Dict, Any
class DockerDatabaseConfig:
"""Docker环境数据库配置管理类"""
def __init__(self, config_file: str = "database_config.ini"):
"""
初始化配置管理器
Args:
config_file: 配置文件路径(可选,Docker环境优先使用环境变量)
"""
self.config_file = config_file
self.config = configparser.ConfigParser()
# 优先使用环境变量,如果不存在则尝试读取配置文件
if self._has_env_config():
print("使用环境变量配置数据库连接")
elif os.path.exists(config_file):
print(f"使用配置文件: {config_file}")
self.load_config()
else:
print("未找到配置文件,将使用默认配置和环境变量")
self.create_default_config()
def _has_env_config(self) -> bool:
"""检查是否有完整的环境变量配置"""
required_env_vars = ['DB_HOST', 'DB_USER', 'DB_NAME']
return all(os.getenv(var) for var in required_env_vars)
def create_default_config(self):
"""创建默认配置文件"""
self.config['mysql'] = {
'host': os.getenv('DB_HOST', 'localhost'),
'port': os.getenv('DB_PORT', '3306'),
'user': os.getenv('DB_USER', 'root'),
'password': os.getenv('DB_PASSWORD', ''),
'database': os.getenv('DB_NAME', 'callback_system'),
'charset': os.getenv('DB_CHARSET', 'utf8mb4')
}
# 如果不是Docker环境,保存配置文件
if not self._has_env_config():
with open(self.config_file, 'w', encoding='utf-8') as f:
f.write("""# 回访记录系统数据库配置文件
# Docker环境会优先使用环境变量配置
""")
self.config.write(f)
print(f"已创建默认配置文件: {self.config_file}")
def load_config(self):
"""从配置文件加载配置"""
try:
self.config.read(self.config_file, encoding='utf-8')
except Exception as e:
print(f"加载配置文件失败: {e}")
self.create_default_config()
def get_mysql_config(self) -> Dict[str, Any]:
"""
获取MySQL配置
优先使用环境变量,其次使用配置文件
Returns:
MySQL连接配置字典
"""
# 优先使用环境变量
if self._has_env_config():
mysql_config = {
'host': os.getenv('DB_HOST'),
'port': int(os.getenv('DB_PORT', '3306')),
'user': os.getenv('DB_USER'),
'password': os.getenv('DB_PASSWORD', ''),
'database': os.getenv('DB_NAME'),
'charset': os.getenv('DB_CHARSET', 'utf8mb4')
}
else:
# 使用配置文件
if 'mysql' not in self.config:
raise ValueError("配置文件中未找到mysql配置段")
mysql_config = dict(self.config['mysql'])
mysql_config['port'] = int(mysql_config.get('port', 3306))
return mysql_config
def validate_config(self) -> bool:
"""
验证配置是否完整
Returns:
配置有效返回True
"""
try:
config = self.get_mysql_config()
required_fields = ['host', 'port', 'user', 'database']
for field in required_fields:
if not config.get(field):
print(f"配置项 {field} 缺失或为空")
return False
if not config.get('password'):
print("警告:数据库密码为空,如果数据库需要密码,请在配置文件中设置")
return True
except Exception as e:
print(f"配置验证失败: {e}")
return False
def test_connection(self) -> bool:
"""
测试数据库连接
Returns:
连接成功返回True
"""
try:
import pymysql
config = self.get_mysql_config()
print(f"正在测试数据库连接...")
print(f"主机: {config['host']}:{config['port']}")
print(f"用户: {config['user']}")
print(f"数据库: {config['database']}")
connection = pymysql.connect(**config)
with connection.cursor() as cursor:
cursor.execute("SELECT VERSION()")
version = cursor.fetchone()
print(f"数据库连接成功!MySQL版本: {version[0]}")
connection.close()
return True
except ImportError:
print("错误:未安装pymysql库")
print("请运行: pip install pymysql")
return False
except Exception as e:
print(f"数据库连接失败: {e}")
return False
def print_config_info(self):
"""打印配置信息"""
print("\n=== 数据库配置信息 ===")
try:
config = self.get_mysql_config()
print(f"配置来源: {'环境变量' if self._has_env_config() else '配置文件'}")
print(f"主机: {config['host']}")
print(f"端口: {config['port']}")
print(f"用户: {config['user']}")
print(f"数据库: {config['database']}")
print(f"字符集: {config['charset']}")
print(f"密码: {'已设置' if config.get('password') else '未设置'}")
except Exception as e:
print(f"获取配置信息失败: {e}")
def main():
"""主函数"""
print("=== Docker数据库配置管理 ===")
config_manager = DockerDatabaseConfig()
# 显示配置信息
config_manager.print_config_info()
# 验证配置
if not config_manager.validate_config():
print("\n❌ 配置验证失败")
return False
# 测试连接
if config_manager.test_connection():
print("\n✅ 数据库配置正确,连接测试成功")
return True
else:
print("\n❌ 数据库连接测试失败")
return False
if __name__ == "__main__":
main()
\ No newline at end of file
USE callback_system;
-- 删除现有的admin用户
DELETE FROM users WHERE username='admin';
-- 插入新的admin用户
INSERT INTO users (username, password_hash, user_type, clinic_access)
VALUES ('admin', 'admin123', 'admin', '["all"]');
\ No newline at end of file
USE callback_system;
UPDATE users SET password_hash = CASE username
WHEN 'admin' THEN 'bcf3315b991bfa7efee55ab1ac28d53a461baf8bf04a0d3a9c2c990e1b2bf2f3'
WHEN 'jinqin' THEN 'c485147aeb57a2fef4f581d0f99d6a82efdaa0fcf9f5b6c1dce67fab85fd5fae'
WHEN 'renshanshan' THEN '90c81c768468bb206479f81db51a5435ca5224adb9345c388039bbb93b9a6a68'
WHEN 'shaojun' THEN 'bece37a8e6b73e0094970e3c3ad563758f57d22df4c6a0e7834768f48c8d37c6'
WHEN 'litingting' THEN 'a1b2c3d4e5f6789012345678901234567890abcdef1234567890abcdef1234'
WHEN 'maqiuyi' THEN 'b2c3d4e5f6789012345678901234567890abcdef1234567890abcdef1234567890'
WHEN 'tangqimin' THEN 'c3d4e5f6789012345678901234567890abcdef1234567890abcdef1234567890ab'
WHEN 'yueling' THEN 'd4e5f6789012345678901234567890abcdef1234567890abcdef1234567890abcd'
WHEN 'jijunlin' THEN 'e5f6789012345678901234567890abcdef1234567890abcdef1234567890abcde'
WHEN 'zhouliping' THEN 'f6789012345678901234567890abcdef1234567890abcdef1234567890abcdef'
WHEN 'feimiaomiao' THEN '7890123456789012345678901234567890abcdef1234567890abcdef12345678'
WHEN 'chenxinyu' THEN '77d100ff5e130d2ae1641a3f2cd91f229b2f1b40c200a3d9fd56da5b671ba515'
WHEN 'yanghong' THEN '34a56c71dd7e71482bf71a1ea9e0861c107acd1daa50f6d0de23483ce078db6e'
WHEN 'panjinli' THEN '894cf43ff5d416c9606f9f2f11ef2be848c331fd785361b2ef9b4c2e991a24fd'
WHEN 'chenlin' THEN '94d2967a391c7292682903043b5150a8b7a87a726fc8b3f650ed30d7ca5853ae'
END WHERE username IN (
'admin', 'jinqin', 'renshanshan', 'shaojun', 'litingting', 'maqiuyi', 'tangqimin', 'yueling', 'jijunlin', 'zhouliping', 'feimiaomiao', 'chenxinyu', 'yanghong', 'panjinli', 'chenlin'
);
\ No newline at end of file
#!/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 random
from config import MISSED_DIAGNOSIS_KEY_POINTS
def get_age_group(age):
"""根据年龄判断年龄组"""
try:
age_num = int(age)
if age_num <= 12:
return "儿童"
elif age_num <= 17:
return "青少年"
elif age_num <= 35:
return "青年"
elif age_num <= 59:
return "中年"
else:
return "老年"
except:
return "中年"
def generate_risk_description(diagnosis, patient_info=None):
"""
根据漏诊项目生成个性化的风险描述
Args:
diagnosis: 漏诊项目
patient_info: 患者信息字典(可选)
Returns:
str: 个性化的风险描述
"""
if diagnosis not in MISSED_DIAGNOSIS_KEY_POINTS:
return f"如果{diagnosis}问题不及时处理,可能会影响到您的口腔健康"
key_points = MISSED_DIAGNOSIS_KEY_POINTS[diagnosis]
risk_points = key_points["风险要点"]
# 随机选择2-3个关键风险点
selected_risks = random.sample(risk_points, min(3, len(risk_points)))
# 构建风险描述
risk_desc = f"如果{diagnosis}问题不及时处理,可能会出现"
risk_desc += "、".join(selected_risks[:2]) # 前两个风险点
if len(selected_risks) > 2:
risk_desc += f",甚至{selected_risks[2]}" # 第三个作为更严重后果
risk_desc += "的情况"
return risk_desc
def generate_treatment_advantage(diagnosis, patient_info):
"""
根据漏诊项目和患者信息生成个性化的治疗优势描述
Args:
diagnosis: 漏诊项目
patient_info: 患者信息字典,包含年龄、性别等
Returns:
str: 个性化的治疗优势描述
"""
if diagnosis not in MISSED_DIAGNOSIS_KEY_POINTS:
return "及时治疗对您的健康维护会有帮助"
key_points = MISSED_DIAGNOSIS_KEY_POINTS[diagnosis]
advantage_points = key_points["治疗优势要点"]
age_adaptability = key_points.get("年龄适应性", {})
# 获取年龄组
age_group = get_age_group(patient_info.get("年龄", "35"))
age_specific_msg = age_adaptability.get(age_group, "")
# 选择关键优势点
selected_advantages = random.sample(advantage_points, min(4, len(advantage_points)))
# 构建治疗优势描述
advantage_desc = f"您现在{selected_advantages[0]}"
if len(selected_advantages) > 1:
advantage_desc += f",通过治疗可以{selected_advantages[1]}"
if len(selected_advantages) > 2:
advantage_desc += f",{selected_advantages[2]}"
if len(selected_advantages) > 3:
advantage_desc += f",让您{selected_advantages[3]}"
# 添加年龄特异性信息
if age_specific_msg:
advantage_desc += f"。{age_specific_msg}"
# 添加同龄患者案例
gender = patient_info.get("性别", "")
if gender == "女":
advantage_desc += f"。很多和您年龄相仿的女性患者,经过治疗后效果都很好"
elif gender == "男":
advantage_desc += f"。很多和您年龄相仿的男性患者,经过治疗后都很满意"
else:
advantage_desc += f"。很多和您年龄相仿的患者,经过治疗后都有很好的效果"
return advantage_desc
def generate_personalized_callback_points(diagnosis, patient_info):
"""
生成个性化的回访话术关键点
Args:
diagnosis: 漏诊项目
patient_info: 患者信息字典
Returns:
dict: 包含个性化话术要点的字典
"""
return {
"风险描述": generate_risk_description(diagnosis, patient_info),
"治疗优势": generate_treatment_advantage(diagnosis, patient_info),
"关键要点": MISSED_DIAGNOSIS_KEY_POINTS.get(diagnosis, {})
}
# 使用示例
if __name__ == "__main__":
# 测试不同患者的个性化话术生成
test_patients = [
{
"姓名": "张女士",
"年龄": "39",
"性别": "女",
"漏诊项": "牙槽骨吸收"
},
{
"姓名": "李先生",
"年龄": "28",
"性别": "男",
"漏诊项": "缺失牙"
},
{
"姓名": "小明",
"年龄": "8",
"性别": "男",
"漏诊项": "儿牙早矫"
}
]
print("个性化回访话术关键点生成测试:")
print("=" * 80)
for patient in test_patients:
print(f"\n患者:{patient['姓名']}({patient['年龄']}岁 {patient['性别']})")
print(f"漏诊项:{patient['漏诊项']}")
print("-" * 60)
personalized_points = generate_personalized_callback_points(
patient['漏诊项'],
patient
)
print(f"风险描述:{personalized_points['风险描述']}")
print(f"治疗优势:{personalized_points['治疗优势']}")
print()
# 显示可用的关键要点
key_points = personalized_points['关键要点']
if key_points:
print("可用关键要点:")
print(f" 风险要点:{', '.join(key_points.get('风险要点', []))}")
print(f" 治疗优势要点:{', '.join(key_points.get('治疗优势要点', []))}")
if '年龄适应性' in key_points:
print(f" 年龄适应性:{key_points['年龄适应性']}")
print("=" * 80)
\ No newline at end of file
# Web框架 # Web框架
Flask==3.1.1 Flask==3.1.1
Flask-CORS==6.0.1 Flask-CORS==6.0.1
Flask-SQLAlchemy==3.1.1
Flask-Migrate==4.0.7
# 数据库 # 数据库
PyMySQL==1.1.2 PyMySQL==1.1.2
......
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
恢复备份数据
检查备份文件中是否有红豆门诊的处理数据
"""
import os
import pickle
import shutil
from datetime import datetime
def check_backup_data():
"""检查备份文件中的数据"""
print("🔍 检查备份文件中的红豆门诊数据...")
progress_dir = "progress_saves"
# 查找最新的备份文件
backup_files = []
for file_name in os.listdir(progress_dir):
if file_name.startswith('session_20250805_232912_backup_'):
file_path = os.path.join(progress_dir, file_name)
backup_files.append((file_name, file_path, os.path.getmtime(file_path)))
if not backup_files:
print("❌ 未找到备份文件")
return
# 使用最新的备份文件
backup_files.sort(key=lambda x: x[2], reverse=True)
latest_backup = backup_files[0]
backup_file = latest_backup[1]
print(f"📁 使用备份文件: {latest_backup[0]}")
try:
with open(backup_file, 'rb') as f:
backup_data = pickle.load(f)
print("\n📊 备份文件信息:")
print(f" - 当前诊所: {backup_data.get('current_clinic', '未知')}")
print(f" - 当前患者索引: {backup_data.get('current_patient_index', 0)}")
print(f" - 保存时间: {backup_data.get('save_timestamp', '未知')}")
# 检查当前诊所的处理数据
current_callbacks = backup_data.get('current_clinic_callbacks', [])
print(f"\n📊 当前诊所处理数据:")
if current_callbacks:
success_count = len([cb for cb in current_callbacks if cb.get('callback_type') == 'success'])
error_count = len([cb for cb in current_callbacks if cb.get('callback_type') == 'error'])
print(f" - 总处理: {len(current_callbacks)} 个患者")
print(f" - ✅ 成功: {success_count} 个")
print(f" - ❌ 失败: {error_count} 个")
# 显示一些处理的患者信息
if len(current_callbacks) > 0:
print(f"\n📋 处理的患者样本:")
sample_size = min(5, len(current_callbacks))
for i in range(sample_size):
cb = current_callbacks[i]
status = "✅" if cb.get('callback_type') == 'success' else "❌"
print(f" {i+1}. {cb.get('patient_name', '未知')} ({cb.get('patient_id', '未知')}) {status}")
if len(current_callbacks) > 5:
print(f" ... 还有 {len(current_callbacks) - 5} 个患者")
else:
print(" - 无处理数据")
# 检查是否需要恢复
if backup_data.get('current_clinic') == '红豆门诊' and current_callbacks:
print(f"\n🎯 发现红豆门诊数据!")
print(f" - 诊所: 红豆门诊")
print(f" - 处理进度: {len(current_callbacks)} 个患者")
print(f" - 当前索引: {backup_data.get('current_patient_index', 0)}")
restore = input("\n是否恢复这些数据?(Y/n): ").strip().lower()
if restore in ['', 'y', 'yes']:
restore_data(backup_file)
else:
print("取消恢复")
else:
print(f"\n⚠️ 备份文件中当前诊所是: {backup_data.get('current_clinic')}")
print(" 可能红豆门诊数据在其他地方,让我检查all_results...")
all_results = backup_data.get('all_results', {})
if '红豆门诊' in all_results:
red_data = all_results['红豆门诊']
print(f" 🎯 在all_results中找到红豆门诊数据: {len(red_data)} 个患者")
restore = input("\n是否恢复all_results中的红豆门诊数据?(Y/n): ").strip().lower()
if restore in ['', 'y', 'yes']:
restore_all_results_data(backup_file)
else:
print("取消恢复")
else:
print(" ❌ 在all_results中也未找到红豆门诊数据")
except Exception as e:
print(f"❌ 读取备份文件失败: {e}")
import traceback
traceback.print_exc()
def restore_data(backup_file):
"""恢复备份数据"""
print("\n🔄 恢复备份数据...")
try:
# 读取备份数据
with open(backup_file, 'rb') as f:
backup_data = pickle.load(f)
# 恢复当前会话文件
session_file = "progress_saves/session_20250805_232912.pkl"
# 创建恢复前的备份
restore_backup = f"progress_saves/before_restore_{datetime.now().strftime('%Y%m%d_%H%M%S')}.pkl"
shutil.copy2(session_file, restore_backup)
# 恢复数据
with open(session_file, 'wb') as f:
pickle.dump(backup_data, f)
print("✅ 数据恢复成功!")
print(f"✅ 已创建恢复前备份: {os.path.basename(restore_backup)}")
# 验证恢复结果
with open(session_file, 'rb') as f:
restored_data = pickle.load(f)
print(f"\n📊 恢复后状态:")
print(f" - 当前诊所: {restored_data.get('current_clinic', '未知')}")
print(f" - 当前患者索引: {restored_data.get('current_patient_index', 0)}")
current_callbacks = restored_data.get('current_clinic_callbacks', [])
if current_callbacks:
success_count = len([cb for cb in current_callbacks if cb.get('callback_type') == 'success'])
print(f" - 当前诊所处理: {len(current_callbacks)} 个患者 (✅{success_count})")
except Exception as e:
print(f"❌ 恢复失败: {e}")
import traceback
traceback.print_exc()
def restore_all_results_data(backup_file):
"""恢复all_results中的红豆门诊数据"""
print("\n🔄 恢复all_results中的红豆门诊数据...")
try:
# 读取备份数据
with open(backup_file, 'rb') as f:
backup_data = pickle.load(f)
# 读取当前会话数据
session_file = "progress_saves/session_20250805_232912.pkl"
with open(session_file, 'rb') as f:
current_data = pickle.load(f)
# 恢复红豆门诊数据到all_results
if 'all_results' not in current_data:
current_data['all_results'] = {}
red_data = backup_data.get('all_results', {}).get('红豆门诊', [])
current_data['all_results']['红豆门诊'] = red_data
# 将红豆门诊添加到已完成列表
if '红豆门诊' not in current_data.get('completed_clinics', []):
current_data.setdefault('completed_clinics', []).append('红豆门诊')
# 如果当前诊所是红豆门诊,切换到下一个诊所
if current_data.get('current_clinic') == '红豆门诊':
selected_clinics = current_data.get('generation_config', {}).get('selected_clinics', [])
completed_clinics = current_data.get('completed_clinics', [])
remaining_clinics = [c for c in selected_clinics if c not in completed_clinics]
if remaining_clinics:
current_data['current_clinic'] = remaining_clinics[0]
current_data['current_patient_index'] = 0
current_data['current_clinic_callbacks'] = []
print(f" ✅ 切换到下一个诊所: {remaining_clinics[0]}")
# 更新时间戳
current_data['save_timestamp'] = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
current_data['last_save_time'] = datetime.now()
# 保存恢复的数据
with open(session_file, 'wb') as f:
pickle.dump(current_data, f)
print("✅ 红豆门诊数据恢复成功!")
print(f"✅ 恢复 {len(red_data)} 个患者的处理结果")
print(f"✅ 红豆门诊已标记为完成")
except Exception as e:
print(f"❌ 恢复失败: {e}")
import traceback
traceback.print_exc()
if __name__ == "__main__":
check_backup_data()
\ No newline at end of file
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
路由模块
"""
from flask import Blueprint
def register_blueprints(app):
"""注册所有蓝图"""
from .auth import auth_bp
from .patient import patient_bp
from .callback import callback_bp
from .api import api_bp
app.register_blueprint(auth_bp)
app.register_blueprint(patient_bp)
app.register_blueprint(callback_bp)
app.register_blueprint(api_bp, url_prefix='/api')
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
API路由
通用API接口
"""
from flask import Blueprint, jsonify, session, request, send_file, Response
from datetime import datetime
import os
import io
api_bp = Blueprint('api', __name__)
def require_login(f):
"""登录装饰器"""
from functools import wraps
@wraps(f)
def decorated_function(*args, **kwargs):
if 'user' not in session:
return jsonify({'success': False, 'message': '未登录'}), 401
return f(*args, **kwargs)
return decorated_function
@api_bp.route('/health', methods=['GET'])
def health_check():
"""健康检查API"""
return jsonify({
'status': 'healthy',
'timestamp': datetime.now().isoformat(),
'version': '1.0.0'
})
@api_bp.route('/export-data', methods=['GET'])
@require_login
def export_data():
"""数据导出API"""
try:
user = session['user']
# 检查管理员权限
if user.get('role') != 'admin':
return jsonify({
'success': False,
'message': '权限不足,只有管理员可以导出数据'
}), 403
# 导入导出模块
try:
from database.scripts.export_data import DataExporter
except ImportError:
return jsonify({
'success': False,
'message': '导出模块未找到'
}), 500
# 生成Excel文件到内存
exporter = DataExporter()
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
output_filename = f"回访记录导出_{timestamp}.xlsx"
print(f"开始生成Excel文件: {output_filename}")
# 导出数据到内存缓冲区
try:
excel_buffer = exporter.export_to_memory()
print(f"Excel文件生成完成,大小: {len(excel_buffer)} 字节")
except Exception as export_error:
print(f"导出过程中出错: {export_error}")
import traceback
traceback.print_exc()
return jsonify({'success': False, 'message': f'导出过程出错: {str(export_error)}'}), 500
# 处理中文文件名编码问题
# 使用 RFC 5987 标准的编码方式
import urllib.parse
encoded_filename = urllib.parse.quote(output_filename.encode('utf-8'))
# 直接返回二进制文件流
return Response(
excel_buffer,
mimetype='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
headers={
'Content-Disposition': f'attachment; filename*=UTF-8\'\'{encoded_filename}',
'Content-Length': str(len(excel_buffer))
}
)
except Exception as e:
print(f"❌ 数据导出失败: {e}")
return jsonify({
'success': False,
'message': f'导出失败: {str(e)}'
}), 500
@api_bp.route('/system-info', methods=['GET'])
@require_login
def system_info():
"""系统信息API"""
try:
user = session['user']
# 检查管理员权限
if user.get('role') != 'admin':
return jsonify({
'success': False,
'message': '权限不足'
}), 403
# 获取系统信息
from database.models import CallbackRecord, Patient, Clinic
# 统计数据
total_records = CallbackRecord.query.count()
total_patients = Patient.query.count()
total_clinics = Clinic.query.count()
return jsonify({
'success': True,
'data': {
'total_callback_records': total_records,
'total_patients': total_patients,
'total_clinics': total_clinics,
'system_time': datetime.now().isoformat(),
'database_status': 'connected'
}
})
except Exception as e:
print(f"❌ 获取系统信息失败: {e}")
return jsonify({
'success': False,
'message': f'获取系统信息失败: {str(e)}'
}), 500
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
认证相关路由
"""
from flask import Blueprint, render_template, request, redirect, url_for, session, flash, jsonify
from services.auth_service import authenticate_user, check_clinic_access
auth_bp = Blueprint('auth', __name__)
@auth_bp.route('/')
def index():
"""首页 - 重定向到登录页面"""
return redirect(url_for('auth.login'))
@auth_bp.route('/login', methods=['GET', 'POST'])
def login():
"""登录页面"""
if request.method == 'POST':
username = request.form['username']
password = request.form['password']
# 验证用户
auth_result = authenticate_user(username, password)
if auth_result['success']:
user = auth_result['user']
session['user'] = user
session['username'] = user['username']
session['real_name'] = user['real_name']
session['role'] = user['role']
session['clinic_id'] = user['clinic_id']
session['clinic_name'] = user['clinic_name']
print(f"✅ 用户 {user['real_name']} ({user['username']}) 登录成功")
flash(f'欢迎,{user["real_name"]}!', 'success')
# 根据用户角色重定向
if user['role'] == 'admin':
# 管理员重定向到管理员仪表板
return redirect(url_for('patient.admin_dashboard'))
else:
# 普通用户重定向到患者列表页面(原版行为)
return redirect(url_for('patient.patient_list_original', clinic_id=user['clinic_id']))
else:
flash(auth_result['message'], 'error')
return render_template('login.html')
@auth_bp.route('/logout')
def logout():
"""退出登录"""
username = session.get('real_name', '未知用户')
session.clear()
print(f"✅ 用户 {username} 退出登录")
flash('已成功退出登录', 'info')
return redirect(url_for('auth.login'))
@auth_bp.route('/api/check-session', methods=['GET'])
def check_session():
"""检查会话状态API"""
if 'user' in session:
return jsonify({
'logged_in': True,
'user': {
'username': session.get('username'),
'real_name': session.get('real_name'),
'role': session.get('role'),
'clinic_name': session.get('clinic_name')
}
})
else:
return jsonify({'logged_in': False})
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
回访相关路由
"""
from flask import Blueprint, render_template, session, redirect, url_for, flash, request, jsonify
from services.auth_service import check_clinic_access
from services.callback_service import save_callback_record, get_callback_records, get_clinic_callback_status
from services.patient_service import get_patient_by_id, load_callback_script
from config import get_clinic_info
import os
import json
callback_bp = Blueprint('callback', __name__)
def require_login(f):
"""登录装饰器"""
from functools import wraps
@wraps(f)
def decorated_function(*args, **kwargs):
if 'user' not in session:
flash('请先登录', 'error')
return redirect(url_for('auth.login'))
return f(*args, **kwargs)
return decorated_function
@callback_bp.route('/callback/<clinic_id>/<patient_id>')
@require_login
def callback_form(clinic_id, patient_id):
"""回访表单页面"""
user = session['user']
# 检查权限
if not check_clinic_access(user, clinic_id):
flash('您没有权限访问该门诊', 'error')
return redirect(url_for('auth.login'))
# 获取患者信息
patient = get_patient_by_id(clinic_id, patient_id)
if not patient:
flash('未找到该患者信息', 'error')
return redirect(url_for('patient.clinic_patients', clinic_id=clinic_id))
# 加载回访话术
callback_script = load_callback_script(clinic_id, patient_id)
# 加载历史回访记录
callback_records = get_callback_records(patient_id)
return render_template('patient_detail.html',
patient=patient,
clinic_id=clinic_id,
clinic_name=get_clinic_info(clinic_id)['clinic_name'],
callback_script=callback_script,
callback_records=callback_records,
user=user)
@callback_bp.route('/api/save-callback', methods=['POST'])
@require_login
def api_save_callback():
"""保存回访记录API"""
try:
data = request.get_json()
if not data:
return jsonify({'success': False, 'message': '无效的请求数据'}), 400
# 获取当前用户
current_user = session.get('real_name', session.get('username', '未知用户'))
# 保存回访记录
result = save_callback_record(data, current_user)
if result['success']:
return jsonify(result)
else:
return jsonify(result), 500
except Exception as e:
print(f"❌ 保存回访记录API失败: {e}")
return jsonify({
'success': False,
'message': f'保存失败: {str(e)}'
}), 500
@callback_bp.route('/api/callback-records/<case_number>')
@require_login
def api_get_callback_records(case_number):
"""获取回访记录API"""
try:
records = get_callback_records(case_number)
return jsonify({
'success': True,
'data': records,
'count': len(records),
'storage': 'database'
})
except Exception as e:
print(f"❌ 获取回访记录失败: {e}")
return jsonify({
'success': False,
'message': f'获取失败: {str(e)}'
}), 500
@callback_bp.route('/api/callback-status/<clinic_id>')
@require_login
def api_get_callback_status(clinic_id):
"""获取门诊回访状态API"""
user = session['user']
# 检查权限
if not check_clinic_access(user, clinic_id):
return jsonify({'success': False, 'message': '权限不足'}), 403
try:
status_dict = get_clinic_callback_status(clinic_id)
return jsonify({
'success': True,
'data': status_dict
})
except Exception as e:
print(f"❌ 获取回访状态失败: {e}")
return jsonify({
'success': False,
'message': f'获取失败: {str(e)}'
}), 500
@callback_bp.route('/api/generate-callback-script', methods=['POST'])
@require_login
def api_generate_callback_script():
"""生成AI回访话术API"""
try:
data = request.get_json()
if not data:
return jsonify({'success': False, 'message': '无效的请求数据'}), 400
clinic_id = data.get('clinic_id')
patient_id = data.get('patient_id')
if not clinic_id or not patient_id:
return jsonify({'success': False, 'message': '缺少必要参数'}), 400
user = session['user']
# 检查权限
if not check_clinic_access(user, clinic_id):
return jsonify({'success': False, 'message': '权限不足'}), 403
# 获取患者信息
patient = get_patient_by_id(clinic_id, patient_id)
if not patient:
return jsonify({'success': False, 'message': '患者不存在'}), 404
# 这里可以调用Dify API生成回访话术
# 目前先返回一个模拟的话术
mock_script = f"""
<div class="mb-6 p-4 bg-blue-50 border-l-4 border-blue-500 rounded-lg">
<h4 class="font-bold text-blue-700 mb-3 flex items-center">
<i class="fas fa-comment mr-2"></i>问候语
</h4>
<div class="text-gray-700 leading-relaxed">
<div class="mb-1">• 您好,{patient.get('姓名', '患者')},我是{get_clinic_info(clinic_id)['clinic_name']}的回访专员。</div>
<div class="mb-1">• 今天给您打电话是想了解一下您的恢复情况。</div>
</div>
</div>
<div class="mb-6 p-4 bg-green-50 border-l-4 border-green-500 rounded-lg">
<h4 class="font-bold text-green-700 mb-3 flex items-center">
<i class="fas fa-comment mr-2"></i>治疗效果询问
</h4>
<div class="text-gray-700 leading-relaxed">
<div class="mb-1">• 您现在感觉怎么样?有没有什么不适的地方?</div>
<div class="mb-1">• 治疗后的效果如何?是否达到了您的预期?</div>
</div>
</div>
<div class="mb-6 p-4 bg-yellow-50 border-l-4 border-yellow-500 rounded-lg">
<h4 class="font-bold text-yellow-700 mb-3 flex items-center">
<i class="fas fa-comment mr-2"></i>护理建议
</h4>
<div class="text-gray-700 leading-relaxed">
<div class="mb-1">• 请注意保持口腔清洁,按时刷牙漱口。</div>
<div class="mb-1">• 如有任何不适,请及时联系我们。</div>
</div>
</div>
<div class="mb-6 p-4 bg-purple-50 border-l-4 border-purple-500 rounded-lg">
<h4 class="font-bold text-purple-700 mb-3 flex items-center">
<i class="fas fa-comment mr-2"></i>结束语
</h4>
<div class="text-gray-700 leading-relaxed">
<div class="mb-1">• 感谢您选择我们的服务,祝您身体健康!</div>
<div class="mb-1">• 如有任何问题,随时可以联系我们。</div>
</div>
</div>
"""
# 保存生成的话术到文件(模拟)
# 实际应用中应该保存到数据库或调用真实的AI API
return jsonify({
'success': True,
'message': '回访话术生成成功',
'script': mock_script.strip()
})
except Exception as e:
print(f"❌ 生成回访话术失败: {e}")
return jsonify({
'success': False,
'message': f'生成失败: {str(e)}'
}), 500
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
患者相关路由
"""
from flask import Blueprint, render_template, session, redirect, url_for, flash, request, jsonify
from services.auth_service import check_clinic_access
from services.patient_service import load_clinic_patients, get_patient_by_id, load_callback_script, get_clinic_info
from services.callback_service import get_callback_records
from config import get_clinic_info
patient_bp = Blueprint('patient', __name__)
def require_login(f):
"""登录装饰器"""
from functools import wraps
@wraps(f)
def decorated_function(*args, **kwargs):
if 'user' not in session:
flash('请先登录', 'error')
return redirect(url_for('auth.login'))
return f(*args, **kwargs)
return decorated_function
@patient_bp.route('/admin/dashboard')
@require_login
def admin_dashboard():
"""管理员仪表板 - 门诊索引页"""
user = session['user']
# 检查管理员权限
if user.get('role') != 'admin':
flash('权限不足', 'error')
return redirect(url_for('auth.login'))
return render_template('admin_dashboard.html', session_data=user, user=user)
@patient_bp.route('/clinic/<clinic_id>/patients')
@require_login
def clinic_patients(clinic_id):
"""门诊患者列表页面"""
user = session['user']
# 检查权限
if not check_clinic_access(user, clinic_id):
flash('您没有权限访问该门诊', 'error')
if user.get('role') == 'admin':
return redirect(url_for('patient.admin_dashboard'))
else:
return redirect(url_for('auth.login'))
# 获取门诊信息
clinic_info = get_clinic_info(clinic_id)
clinic_name = clinic_info['clinic_name']
# 加载患者数据
patients, doctors = load_clinic_patients(clinic_id)
# 所有用户都使用原版的患者列表模板
return render_template('patient_list.html',
session_data=user,
clinic_id=clinic_id,
clinic_name=clinic_name,
patients=patients,
doctors=doctors)
@patient_bp.route('/patient_profiles/<clinic_id>/index.html')
@require_login
def patient_list_original(clinic_id):
"""患者列表页面(原版路由,普通用户登录后的首页)"""
user = session['user']
# 检查权限
if not check_clinic_access(user, clinic_id):
flash('您没有权限访问该门诊', 'error')
if user.get('role') == 'admin':
return redirect(url_for('patient.admin_dashboard'))
else:
return redirect(url_for('auth.login'))
# 获取门诊信息
clinic_info = get_clinic_info(clinic_id)
clinic_name = clinic_info['clinic_name']
# 加载患者数据
patients, doctors = load_clinic_patients(clinic_id)
# 所有用户都使用原版的患者列表模板
return render_template('patient_list.html',
session_data=user,
clinic_id=clinic_id,
clinic_name=clinic_name,
patients=patients,
doctors=doctors)
@patient_bp.route('/clinic/<clinic_id>/patient/<patient_id>')
@require_login
def patient_detail(clinic_id, patient_id):
"""患者详情页面"""
user = session['user']
# 检查权限
if not check_clinic_access(user, clinic_id):
flash('您没有权限访问该门诊', 'error')
return redirect(url_for('patient.clinic_patients', clinic_id=clinic_id))
# 获取患者信息
patient = get_patient_by_id(clinic_id, patient_id)
if not patient:
flash('未找到该患者信息', 'error')
return redirect(url_for('patient.clinic_patients', clinic_id=clinic_id))
# 获取门诊信息
clinic_info = get_clinic_info(clinic_id)
clinic_name = clinic_info['clinic_name']
# 加载回访话术
callback_script = load_callback_script(clinic_id, patient_id)
# 加载历史回访记录
callback_records = get_callback_records(patient_id)
return render_template('patient_detail.html',
patient=patient,
clinic_id=clinic_id,
clinic_name=clinic_name,
callback_script=callback_script,
callback_records=callback_records,
user=user)
@patient_bp.route('/api/patients/<clinic_id>')
@require_login
def api_get_patients(clinic_id):
"""获取门诊患者列表API"""
user = session['user']
# 检查权限
if not check_clinic_access(user, clinic_id):
return jsonify({'success': False, 'message': '权限不足'}), 403
try:
patients, _ = load_clinic_patients(clinic_id)
return jsonify({
'success': True,
'data': patients,
'count': len(patients)
})
except Exception as e:
return jsonify({
'success': False,
'message': f'获取患者列表失败: {str(e)}'
}), 500
@patient_bp.route('/api/patient/<clinic_id>/<patient_id>')
@require_login
def api_get_patient(clinic_id, patient_id):
"""获取患者详情API"""
user = session['user']
# 检查权限
if not check_clinic_access(user, clinic_id):
return jsonify({'success': False, 'message': '权限不足'}), 403
try:
patient = get_patient_by_id(clinic_id, patient_id)
if not patient:
return jsonify({'success': False, 'message': '患者不存在'}), 404
return jsonify({
'success': True,
'data': patient
})
except Exception as e:
return jsonify({
'success': False,
'message': f'获取患者信息失败: {str(e)}'
}), 500
# 服务层模块
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
认证服务层
处理用户认证相关的业务逻辑
"""
from config import get_user_by_username
def authenticate_user(username, password):
"""验证用户登录"""
try:
user = get_user_by_username(username)
if user and user['password'] == password:
return {
'success': True,
'user': user,
'message': '登录成功'
}
else:
return {
'success': False,
'message': '用户名或密码错误'
}
except Exception as e:
print(f"❌ 用户认证失败: {e}")
return {
'success': False,
'message': f'认证失败: {str(e)}'
}
def check_clinic_access(user, clinic_id):
"""检查用户是否有权限访问指定门诊"""
try:
# 管理员可以访问所有门诊
if user.get('role') == 'admin':
return True
# 普通用户只能访问自己的门诊
return user.get('clinic_id') == clinic_id
except Exception as e:
print(f"❌ 权限检查失败: {e}")
return False
def get_user_clinics(user):
"""获取用户可访问的门诊列表"""
try:
if user.get('role') == 'admin':
# 管理员可以访问所有门诊
from config import CLINIC_CONFIG
return list(CLINIC_CONFIG.keys())
else:
# 普通用户只能访问自己的门诊
clinic_id = user.get('clinic_id')
return [clinic_id] if clinic_id else []
except Exception as e:
print(f"❌ 获取用户门诊列表失败: {e}")
return []
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
回访服务层
处理回访记录相关的业务逻辑
"""
from datetime import datetime
from database.models import db, CallbackRecord
# 内存存储最新的回访结果,用于状态API
callback_results_cache = {}
def save_callback_record(data, current_user):
"""保存回访记录"""
try:
# 构建回访记录内容
callback_record = f"""
回访时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}
回访方式: {', '.join(data['callbackMethods'])}
回访结果: {data['callbackResult']}
操作员: {current_user}
"""
# 如果有下次预约时间,添加到记录中
if data.get('nextAppointmentTime'):
callback_record += f"\n下次预约时间: {data['nextAppointmentTime']}"
# 如果有失败原因,添加到记录中
if data.get('failureReason'):
callback_record += f"\n失败原因: {data['failureReason']}"
# 如果有放弃原因,添加到记录中
if data.get('abandonReason'):
callback_record += f"\n放弃原因: {data['abandonReason']}"
# 如果有AI反馈,添加到记录中
if data.get('aiFeedbackType'):
callback_record += f"\nAI反馈类型: {data['aiFeedbackType']}"
result = data['callbackResult']
# 使用SQLAlchemy ORM保存记录
record = CallbackRecord(
case_number=data['caseNumber'],
callback_methods=data['callbackMethods'],
callback_record=callback_record, # 使用构建好的回访记录内容
callback_result=result, # 传递回访结果
next_appointment_time=data.get('nextAppointmentTime'),
failure_reason=data.get('failureReason'),
abandon_reason=data.get('abandonReason'),
ai_feedback_type=data.get('aiFeedbackType'),
failure_reason_note=data.get('failureReasonNote'),
abandon_reason_note=data.get('abandonReasonNote'),
ai_feedback_note=data.get('aiFeedbackNote'),
callback_status=data.get('callbackStatus', '已回访'),
operator=current_user # 使用从session获取的真实用户名
)
db.session.add(record)
db.session.commit()
record_id = record.record_id
# 更新内存缓存
callback_results_cache[data['caseNumber']] = {
'callback_result': result,
'create_time': datetime.now()
}
return {
'success': True,
'id': record_id,
'message': '保存成功',
'timestamp': datetime.now().isoformat(),
'storage': 'database',
'operator': current_user
}
except Exception as e:
db.session.rollback()
print(f"❌ 保存回访记录失败: {e}")
return {
'success': False,
'message': f'保存失败: {str(e)}'
}
def get_callback_records(case_number):
"""获取患者的回访记录"""
try:
# 使用SQLAlchemy ORM查询
records = CallbackRecord.query.filter_by(case_number=case_number).order_by(CallbackRecord.create_time.desc()).all()
return [record.to_dict() for record in records]
except Exception as e:
print(f"❌ 获取回访记录失败: {e}")
return []
def get_clinic_callback_status(clinic_id):
"""获取指定门诊所有患者的回访状态"""
try:
# 使用SQLAlchemy ORM查询所有回访记录
all_records = CallbackRecord.query.order_by(CallbackRecord.create_time.desc()).all()
# 按病历号分组,获取最新的回访状态
status_map = {}
for record in all_records:
case_number = record.case_number
# 如果该病历号还没有记录,或者当前记录更新,则更新状态
if case_number not in status_map:
status_map[case_number] = {
'case_number': case_number,
'callback_status': '已回访',
'callback_result': record.callback_result, # 直接使用存储的结果
'create_time': record.create_time.isoformat() if record.create_time else None
}
else:
# 比较时间,保留最新的记录
current_time = record.create_time
existing_time = status_map[case_number]['create_time']
if current_time and existing_time:
try:
if isinstance(existing_time, str):
existing_time = datetime.fromisoformat(existing_time.replace('Z', '+00:00'))
if current_time > existing_time:
status_map[case_number] = {
'case_number': case_number,
'callback_status': '已回访',
'callback_result': record.callback_result, # 直接使用存储的结果
'create_time': current_time.isoformat() if current_time else None
}
except Exception as e:
print(f"时间比较错误: {e}")
# 转换为以病历号为键的字典格式,方便前端查找
status_dict = {}
for status in status_map.values():
status_dict[status['case_number']] = status
return status_dict
except Exception as e:
print(f"❌ 获取回访状态失败: {e}")
return {}
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
患者服务层
处理患者数据相关的业务逻辑
"""
import json
import os
from config import get_clinic_info
def load_clinic_patients(clinic_id):
"""加载指定门诊的患者数据,返回(patients, doctors)元组"""
try:
# 获取门诊信息
clinic_info = get_clinic_info(clinic_id)
clinic_name = clinic_info.get('clinic_name', '未知门诊')
patients = []
# 首先尝试从数据库加载
try:
from database.models import Patient
db_patients = Patient.query.filter_by(clinic_name=clinic_name).all()
if db_patients:
patients = [patient.to_dict() for patient in db_patients]
print(f"✅ 从数据库加载 {clinic_name} 患者数据成功: {len(patients)} 名患者")
except Exception as db_error:
print(f"⚠️ 数据库查询失败: {db_error}")
# 如果数据库没有数据,从文件系统加载
if not patients:
# 优先尝试读取单独的门诊文件
json_file_path = f'data/patients/clinics/{clinic_name}.json'
if os.path.exists(json_file_path):
print(f"📁 从门诊文件加载患者数据: {json_file_path}")
with open(json_file_path, 'r', encoding='utf-8') as f:
patients = json.load(f)
print(f"✅ 加载 {clinic_name} 患者数据成功: {len(patients)} 名患者")
else:
# 最后尝试读取合并结果文件
merged_file_path = 'data/patients/merged/合并结果.json'
if os.path.exists(merged_file_path):
print(f"📁 从合并文件加载患者数据: {merged_file_path}")
with open(merged_file_path, 'r', encoding='utf-8') as f:
all_data = json.load(f)
# 筛选出指定门诊的患者(使用最后一次就诊诊所字段)
clinic_patients = []
# 创建门诊名称映射表
clinic_name_mapping = {
'学前街门诊': '江苏瑞泰通善口腔学前街医院',
'大丰门诊': '江苏瑞泰通善口腔大丰医院',
'东亭门诊': '江苏瑞泰通善口腔东亭医院',
'河埒门诊': '江苏瑞泰通善口腔河埒医院',
'红豆门诊': '江苏瑞泰通善口腔红豆医院',
'惠山门诊': '江苏瑞泰通善口腔惠山医院',
'马山门诊': '江苏瑞泰通善口腔马山医院',
'通善口腔医院': '江苏瑞泰通善口腔医院',
'新吴门诊': '江苏瑞泰通善口腔新吴医院'
}
# 获取实际的门诊名称
actual_clinic_name = clinic_name_mapping.get(clinic_name, clinic_name)
for patient in all_data:
last_clinic = patient.get('最后一次就诊诊所', '')
if actual_clinic_name in last_clinic or clinic_name in last_clinic:
clinic_patients.append(patient)
patients = clinic_patients
print(f"✅ 从合并文件加载 {clinic_name} 患者数据成功: {len(clinic_patients)} 名患者")
# 提取医生列表
doctors = set()
for patient in patients:
doctor = patient.get('最后一次就诊医生')
if doctor and doctor.strip():
doctors.add(doctor.strip())
doctors_list = sorted(list(doctors))
print(f"✅ 加载 {clinic_name} 患者数据: {len(patients)} 人, {len(doctors_list)} 位医生")
return patients, doctors_list
except Exception as e:
print(f"❌ 加载患者数据失败: {e}")
return [], []
def format_callback_script(script):
"""格式化回访话术为HTML显示"""
if not script:
return None
try:
import re
# 移除开头的JSON部分
script = re.sub(r'^```json.*?```\n', '', script, flags=re.DOTALL)
# 分割各个部分
sections = []
current_section = None
current_content = []
lines = script.split('\n')
for line in lines:
line = line.strip()
if not line:
continue
# 检查是否是新的部分标题
if line.startswith('═══') and '部分:' in line:
# 保存上一个部分
if current_section and current_content:
sections.append({
'title': current_section,
'content': current_content.copy()
})
# 开始新的部分
current_section = line.replace('═══', '').strip()
current_content = []
elif line.startswith('•'):
# 添加内容项
current_content.append(line[1:].strip())
elif line.startswith('**') and line.endswith('**'):
# 子标题
current_content.append(f"<strong>{line[2:-2]}</strong>")
elif line and current_section:
# 其他内容
current_content.append(line)
# 保存最后一个部分
if current_section and current_content:
sections.append({
'title': current_section,
'content': current_content.copy()
})
# 生成HTML
if not sections:
return script # 如果解析失败,返回原始内容
html_parts = []
colors = [
('bg-blue-100', 'border-blue-500', 'text-blue-800'),
('bg-yellow-100', 'border-yellow-500', 'text-yellow-800'),
('bg-green-100', 'border-green-500', 'text-green-800'),
('bg-purple-100', 'border-purple-500', 'text-purple-800')
]
for i, section in enumerate(sections):
bg_color, border_color, text_color = colors[i % len(colors)]
content_html = []
for item in section['content']:
if item.startswith('<strong>'):
content_html.append(f'<div class="font-semibold mt-2 mb-1">{item}</div>')
else:
content_html.append(f'<div class="mb-1">• {item}</div>')
section_html = f'''
<div class="mb-6 p-4 {bg_color} border-l-4 {border_color} rounded-lg">
<h4 class="font-bold {text_color} mb-3 flex items-center">
<i class="fas fa-comment mr-2"></i>{section['title']}
</h4>
<div class="text-gray-700 leading-relaxed">
{''.join(content_html)}
</div>
</div>'''
html_parts.append(section_html)
return ''.join(html_parts)
except Exception as e:
print(f"❌ 格式化回访话术失败: {e}")
return script # 返回原始内容
def load_callback_script(clinic_id, patient_id):
"""加载患者的回访话术"""
try:
# 获取门诊信息
clinic_info = get_clinic_info(clinic_id)
clinic_name = clinic_info.get('clinic_name', '未知门诊')
# 尝试多种文件名格式
possible_files = [
f'data/callbacks/中间结果_{clinic_name}.json',
f'data/callbacks/中间结果_{clinic_name}_*.json'
]
# 查找匹配的文件
import glob
for pattern in possible_files:
if '*' in pattern:
# 使用glob查找带时间戳的文件
matching_files = glob.glob(pattern)
if matching_files:
# 使用最新的文件
callback_file = max(matching_files, key=os.path.getmtime)
break
else:
if os.path.exists(pattern):
callback_file = pattern
break
else:
print(f"⚠️ 未找到 {clinic_name} 的回访话术文件")
return "暂无个性化回访话术,请使用通用话术进行回访。"
# 读取回访话术文件
with open(callback_file, 'r', encoding='utf-8') as f:
callback_data = json.load(f)
# 查找指定患者的回访话术
if 'callbacks' in callback_data:
for callback in callback_data['callbacks']:
if callback.get('patient_id') == patient_id or callback.get('病历号') == patient_id:
script = callback.get('callback_script') or callback.get('回访话术', '')
if script:
print(f"✅ 找到患者 {patient_id} 的回访话术,长度: {len(script)}")
return format_callback_script(script)
print(f"⚠️ 未找到患者 {patient_id} 的个性化回访话术")
return "暂无个性化回访话术,请使用通用话术进行回访。"
except Exception as e:
print(f"❌ 加载回访话术失败: {e}")
return "加载回访话术失败,请使用通用话术进行回访。"
def get_patient_by_id(clinic_id, patient_id):
"""根据患者ID获取患者详细信息"""
try:
patients, _ = load_clinic_patients(clinic_id)
for patient in patients:
if patient.get('病历号') == patient_id:
return patient
return None
except Exception as e:
print(f"❌ 获取患者信息失败: {e}")
return None
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