feat: 初始化考培练系统项目
- 从服务器拉取完整代码 - 按框架规范整理项目结构 - 配置 Drone CI 测试环境部署 - 包含后端(FastAPI)、前端(Vue3)、管理端 技术栈: Vue3 + TypeScript + FastAPI + MySQL
This commit is contained in:
317
backend/scripts/migrate_env_to_db.py
Normal file
317
backend/scripts/migrate_env_to_db.py
Normal file
@@ -0,0 +1,317 @@
|
||||
#!/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()
|
||||
|
||||
Reference in New Issue
Block a user