Files
012-kaopeilian/backend/scripts/migrate_env_to_db.py
111 998211c483 feat: 初始化考培练系统项目
- 从服务器拉取完整代码
- 按框架规范整理项目结构
- 配置 Drone CI 测试环境部署
- 包含后端(FastAPI)、前端(Vue3)、管理端

技术栈: Vue3 + TypeScript + FastAPI + MySQL
2026-01-24 19:33:28 +08:00

318 lines
9.9 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""
租户配置迁移脚本
功能:将各租户的 .env 文件配置迁移到 kaopeilian_admin 数据库
使用方法:
python scripts/migrate_env_to_db.py
说明:
1. 读取各租户的 .env 文件
2. 创建租户记录
3. 将配置写入 tenant_configs 表
4. 保留原 .env 文件作为备份
"""
import os
import sys
import re
import pymysql
from datetime import datetime
from typing import Dict, List, Tuple, Optional
# 添加项目根目录到路径
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
# ============================================
# 配置
# ============================================
# 管理库连接配置
ADMIN_DB_CONFIG = {
"host": os.getenv("ADMIN_DB_HOST", "120.79.247.16"),
"port": int(os.getenv("ADMIN_DB_PORT", "3309")),
"user": os.getenv("ADMIN_DB_USER", "root"),
"password": os.getenv("ADMIN_DB_PASSWORD", "ProdMySQL2025!@#"),
"db": os.getenv("ADMIN_DB_NAME", "kaopeilian_admin"),
"charset": "utf8mb4",
}
# 租户配置
TENANTS = [
{
"code": "demo",
"name": "演示版",
"display_name": "考培练系统-演示版",
"domain": "aiedu.ireborn.com.cn",
"env_file": "/root/aiedu/kaopeilian-backend/.env.production",
"industry": "medical_beauty",
},
{
"code": "hua",
"name": "华尔倍丽",
"display_name": "华尔倍丽-考培练系统",
"domain": "hua.ireborn.com.cn",
"env_file": "/root/aiedu/kaopeilian-backend/.env.hua",
"industry": "medical_beauty",
},
{
"code": "yy",
"name": "杨扬宠物",
"display_name": "杨扬宠物-考培练系统",
"domain": "yy.ireborn.com.cn",
"env_file": "/root/aiedu/kaopeilian-backend/.env.yy",
"industry": "pet",
},
{
"code": "hl",
"name": "武汉禾丽",
"display_name": "武汉禾丽-考培练系统",
"domain": "hl.ireborn.com.cn",
"env_file": "/root/aiedu/kaopeilian-backend/.env.hl",
"industry": "medical_beauty",
},
{
"code": "xy",
"name": "芯颜定制",
"display_name": "芯颜定制-考培练系统",
"domain": "xy.ireborn.com.cn",
"env_file": "/root/aiedu/kaopeilian-backend/.env.xy",
"industry": "medical_beauty",
},
{
"code": "fw",
"name": "飞沃",
"display_name": "飞沃-考培练系统",
"domain": "fw.ireborn.com.cn",
"env_file": "/root/aiedu/kaopeilian-backend/.env.fw",
"industry": "medical_beauty",
},
{
"code": "ex",
"name": "恩喜成都总院",
"display_name": "恩喜成都总院-考培练系统",
"domain": "ex.ireborn.com.cn",
"env_file": "/root/aiedu/kaopeilian-backend/.env.ex",
"industry": "medical_beauty",
},
{
"code": "kpl",
"name": "瑞小美",
"display_name": "瑞小美-考培练系统",
"domain": "kpl.ireborn.com.cn",
"env_file": "/root/aiedu/kaopeilian-backend/.env.kpl",
"industry": "medical_beauty",
},
]
# 配置键到分组的映射
CONFIG_MAPPING = {
# 数据库配置
"MYSQL_HOST": ("database", "string", False),
"MYSQL_PORT": ("database", "int", False),
"MYSQL_USER": ("database", "string", False),
"MYSQL_PASSWORD": ("database", "string", True),
"MYSQL_DATABASE": ("database", "string", False),
# Redis配置
"REDIS_HOST": ("redis", "string", False),
"REDIS_PORT": ("redis", "int", False),
"REDIS_DB": ("redis", "int", False),
"REDIS_URL": ("redis", "string", False),
# 安全配置
"SECRET_KEY": ("security", "string", True),
"CORS_ORIGINS": ("security", "json", False),
# Dify配置
"DIFY_API_KEY": ("dify", "string", True),
"DIFY_EXAM_GENERATOR_API_KEY": ("dify", "string", True),
"DIFY_PRACTICE_API_KEY": ("dify", "string", True),
"DIFY_COURSE_CHAT_API_KEY": ("dify", "string", True),
"DIFY_YANJI_ANALYSIS_API_KEY": ("dify", "string", True),
# Coze配置
"COZE_PRACTICE_BOT_ID": ("coze", "string", False),
"COZE_BROADCAST_WORKFLOW_ID": ("coze", "string", False),
"COZE_BROADCAST_SPACE_ID": ("coze", "string", False),
"COZE_BROADCAST_BOT_ID": ("coze", "string", False),
"COZE_OAUTH_CLIENT_ID": ("coze", "string", False),
"COZE_OAUTH_PUBLIC_KEY_ID": ("coze", "string", False),
"COZE_OAUTH_PRIVATE_KEY_PATH": ("coze", "string", False),
# AI配置
"AI_PRIMARY_API_KEY": ("ai", "string", True),
"AI_PRIMARY_BASE_URL": ("ai", "string", False),
"AI_FALLBACK_API_KEY": ("ai", "string", True),
"AI_FALLBACK_BASE_URL": ("ai", "string", False),
"AI_DEFAULT_MODEL": ("ai", "string", False),
"AI_TIMEOUT": ("ai", "int", False),
# 言迹配置
"YANJI_CLIENT_ID": ("yanji", "string", False),
"YANJI_CLIENT_SECRET": ("yanji", "string", True),
"YANJI_TENANT_ID": ("yanji", "string", False),
"YANJI_ESTATE_ID": ("yanji", "string", False),
# 其他配置
"APP_NAME": ("basic", "string", False),
"PUBLIC_DOMAIN": ("basic", "string", False),
}
def parse_env_file(filepath: str) -> Dict[str, str]:
"""解析 .env 文件"""
config = {}
if not os.path.exists(filepath):
print(f" 警告: 文件不存在 {filepath}")
return config
with open(filepath, 'r', encoding='utf-8') as f:
for line in f:
line = line.strip()
# 跳过注释和空行
if not line or line.startswith('#'):
continue
# 解析 KEY=VALUE
if '=' in line:
key, value = line.split('=', 1)
key = key.strip()
value = value.strip()
# 去除引号
if (value.startswith('"') and value.endswith('"')) or \
(value.startswith("'") and value.endswith("'")):
value = value[1:-1]
config[key] = value
return config
def create_tenant(cursor, tenant: Dict) -> int:
"""创建租户记录返回租户ID"""
# 检查是否已存在
cursor.execute(
"SELECT id FROM tenants WHERE code = %s",
(tenant["code"],)
)
row = cursor.fetchone()
if row:
print(f" 租户已存在ID: {row['id']}")
return row["id"]
# 创建新租户
cursor.execute(
"""
INSERT INTO tenants (code, name, display_name, domain, industry, status, created_by)
VALUES (%s, %s, %s, %s, %s, 'active', 1)
""",
(tenant["code"], tenant["name"], tenant["display_name"], tenant["domain"], tenant["industry"])
)
tenant_id = cursor.lastrowid
print(f" 创建租户成功ID: {tenant_id}")
return tenant_id
def migrate_config(cursor, tenant_id: int, config: Dict[str, str]) -> Tuple[int, int]:
"""迁移配置到数据库"""
inserted = 0
updated = 0
for key, value in config.items():
if key not in CONFIG_MAPPING:
continue
config_group, value_type, is_secret = CONFIG_MAPPING[key]
# 检查是否已存在
cursor.execute(
"""
SELECT id FROM tenant_configs
WHERE tenant_id = %s AND config_group = %s AND config_key = %s
""",
(tenant_id, config_group, key)
)
row = cursor.fetchone()
if row:
# 更新
cursor.execute(
"""
UPDATE tenant_configs
SET config_value = %s, value_type = %s, is_encrypted = %s, updated_at = NOW()
WHERE id = %s
""",
(value, value_type, is_secret, row["id"])
)
updated += 1
else:
# 插入
cursor.execute(
"""
INSERT INTO tenant_configs
(tenant_id, config_group, config_key, config_value, value_type, is_encrypted)
VALUES (%s, %s, %s, %s, %s, %s)
""",
(tenant_id, config_group, key, value, value_type, is_secret)
)
inserted += 1
return inserted, updated
def main():
"""主函数"""
print("=" * 60)
print("租户配置迁移脚本")
print("=" * 60)
print(f"\n目标数据库: {ADMIN_DB_CONFIG['host']}:{ADMIN_DB_CONFIG['port']}/{ADMIN_DB_CONFIG['db']}")
print(f"待迁移租户: {len(TENANTS)}\n")
# 连接数据库
conn = pymysql.connect(**ADMIN_DB_CONFIG, cursorclass=pymysql.cursors.DictCursor)
try:
with conn.cursor() as cursor:
total_inserted = 0
total_updated = 0
for tenant in TENANTS:
print(f"\n处理租户: {tenant['name']} ({tenant['code']})")
print(f" 环境文件: {tenant['env_file']}")
# 解析 .env 文件
config = parse_env_file(tenant['env_file'])
print(f" 读取配置: {len(config)}")
# 创建租户
tenant_id = create_tenant(cursor, tenant)
# 迁移配置
if config:
inserted, updated = migrate_config(cursor, tenant_id, config)
print(f" 迁移结果: 新增 {inserted} 项, 更新 {updated}")
total_inserted += inserted
total_updated += updated
else:
print(" 跳过迁移(无配置)")
# 提交事务
conn.commit()
print("\n" + "=" * 60)
print("迁移完成!")
print(f"总计: 新增 {total_inserted} 项, 更新 {total_updated}")
print("=" * 60)
except Exception as e:
conn.rollback()
print(f"\n错误: {e}")
raise
finally:
conn.close()
if __name__ == "__main__":
main()