feat: 初始化考培练系统项目

- 从服务器拉取完整代码
- 按框架规范整理项目结构
- 配置 Drone CI 测试环境部署
- 包含后端(FastAPI)、前端(Vue3)、管理端

技术栈: Vue3 + TypeScript + FastAPI + MySQL
This commit is contained in:
111
2026-01-24 19:33:28 +08:00
commit 998211c483
1197 changed files with 228429 additions and 0 deletions

View 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()