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
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
......@@ -38,8 +38,8 @@ EXPOSE 5000
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 \
CMD curl -f http://localhost:5000/ || exit 1
HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=3 \
CMD curl -f http://localhost:5000/api/health || exit 1
# 启动命令
CMD ["python", "app.py"]
\ No newline at end of file
# 使用entrypoint.sh启动(零配置自动迁移)
ENTRYPOINT ["./database/scripts/entrypoint.sh"]
\ 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():
# 加载患者数据
try:
# 尝试加载现有的患者数据文件
data_files = ['漏诊客户画像.json', '合并结果.json']
data_files = ['漏诊客户画像.json', 'data/patients/merged/合并结果.json']
patients_data = None
for file_name in data_files:
......
......@@ -30,7 +30,7 @@ class DifyConfig:
self.USER_ID = "callback_system" # 默认用户ID
# 输出配置
self.OUTPUT_DIR = "dify_callback_results" # 结果输出目录
self.OUTPUT_DIR = "data/callbacks" # 结果输出目录
self.LOG_LEVEL = "INFO" # 日志级别
def get_headers(self) -> Dict[str, str]:
......
......@@ -12,7 +12,7 @@ from typing import Dict, Any
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 @@
import os
import json
import io
from datetime import datetime
from typing import Dict, List, Any
import pymysql
......@@ -174,6 +175,38 @@ class DataExporter:
# 保存文件
wb.save(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]):
"""创建总览表"""
......
......@@ -250,15 +250,15 @@ def main():
# 定义门诊和对应的JSON文件
clinics = [
('学前街门诊', '学前街门诊.json'), # 根目录文件
('大丰门诊', '诊所患者json/大丰门诊.json'),
('东亭门诊', '诊所患者json/东亭门诊.json'),
('河埒门诊', '诊所患者json/河埒门诊.json'),
('红豆门诊', '诊所患者json/红豆门诊.json'),
('惠山门诊', '诊所患者json/惠山门诊.json'),
('马山门诊', '诊所患者json/马山门诊.json'),
('通善口腔医院', '诊所患者json/通善口腔医院.json'),
('新吴门诊', '诊所患者json/新吴门诊.json')
('学前街门诊', 'data/patients/clinics/学前街门诊.json'),
('大丰门诊', 'data/patients/clinics/大丰门诊.json'),
('东亭门诊', 'data/patients/clinics/东亭门诊.json'),
('河埒门诊', 'data/patients/clinics/河埒门诊.json'),
('红豆门诊', 'data/patients/clinics/红豆门诊.json'),
('惠山门诊', 'data/patients/clinics/惠山门诊.json'),
('马山门诊', 'data/patients/clinics/马山门诊.json'),
('通善口腔医院', 'data/patients/clinics/通善口腔医院.json'),
('新吴门诊', 'data/patients/clinics/新吴门诊.json')
]
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:
MYSQL_COLLATION_SERVER: utf8mb4_unicode_ci
volumes:
- 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:
- "3306:3306" # Host port 3306 maps to container port 3306
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框架
Flask==3.1.1
Flask-CORS==6.0.1
Flask-SQLAlchemy==3.1.1
Flask-Migrate==4.0.7
# 数据库
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