From 0ab2c848130e8bc575bd12c3610238155ff56565 Mon Sep 17 00:00:00 2001 From: Pine Date: Sun, 13 Sep 2026 12:23:24 +0800 Subject: [PATCH] =?UTF-8?q?fix(compute):=20=E5=9B=BE=E6=A0=87=E4=B8=AD?= =?UTF-8?q?=E5=BF=83=20URL=20=E7=BB=9F=E4=B8=80=20resolve=20=E8=A1=A5?= =?UTF-8?q?=E5=85=A8=20+=20=E7=94=9F=E4=BA=A7=E5=BA=93=E8=BF=90=E7=BB=B4?= =?UTF-8?q?=E8=84=9A=E6=9C=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - icon_to_dict/options/create/update 用 resolve_url 补全相对路径 - 新增生产库结构对比/索引修复/建表脚本 --- app/api/routers/rbac_compute_assets.py | 11 +- scripts/compare_db_structure.py | 170 +++++++++++++++++++++++++ scripts/fix_index_names.py | 82 ++++++++++++ scripts/fix_prod_db.py | 127 ++++++++++++++++++ 4 files changed, 385 insertions(+), 5 deletions(-) create mode 100644 scripts/compare_db_structure.py create mode 100644 scripts/fix_index_names.py create mode 100644 scripts/fix_prod_db.py diff --git a/app/api/routers/rbac_compute_assets.py b/app/api/routers/rbac_compute_assets.py index bae74a2..84a3b30 100644 --- a/app/api/routers/rbac_compute_assets.py +++ b/app/api/routers/rbac_compute_assets.py @@ -21,6 +21,7 @@ from ...rbac import write_audit from ...domain.rules import role_allowed from ...infrastructure.models import ComputeSeries, ComputeIcon from ...infrastructure.repositories import Database +from ...infrastructure.oss import resolve_url router = APIRouter(prefix="/compute", tags=["compute-assets"]) logger = logging.getLogger("compute-assets") @@ -58,12 +59,12 @@ def series_to_dict(s: ComputeSeries) -> dict: def icon_to_dict(i: ComputeIcon) -> dict: - """图标 ORM → dict。""" + """图标 ORM → dict(url 统一补全为可访问直链)。""" return { "id": i.id, "name": i.name, "category": i.category, - "url": i.url, + "url": resolve_url(i.url or i.file), "file": i.file, "file_size": i.file_size, "mime_type": i.mime_type, @@ -337,7 +338,7 @@ async def icon_options( { "id": i.id, "name": i.name, - "url": i.url or i.file, + "url": resolve_url(i.url or i.file), "category": i.category, } for i in items @@ -370,7 +371,7 @@ async def create_icon( id=new_id("ci_"), name=body.name, category=body.category, - url=body.url, + url=resolve_url(body.url or body.file), file=body.file, file_size=body.file_size, mime_type=body.mime_type, @@ -401,7 +402,7 @@ async def update_icon( raise HTTPException(status_code=404, detail="图标不存在") i.name = body.name i.category = body.category - i.url = body.url + i.url = resolve_url(body.url or body.file) i.file = body.file i.file_size = body.file_size i.mime_type = body.mime_type diff --git a/scripts/compare_db_structure.py b/scripts/compare_db_structure.py new file mode 100644 index 0000000..59bd359 --- /dev/null +++ b/scripts/compare_db_structure.py @@ -0,0 +1,170 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +"""对比生产数据库和开发数据库的表结构一致性。""" +import sys + +# 数据库连接配置 +PROD_HOST = "124.221.143.140" +DEV_HOST = "192.168.1.3" +PORT = 8091 +USER = "opc" +PASSWORD = "jjjsgysyujkwjwgb" +DB_NAME = "opc" + +PROD_URL = f"mysql+pymysql://{USER}:{PASSWORD}@{PROD_HOST}:{PORT}/{DB_NAME}?charset=utf8mb4" +DEV_URL = f"mysql+pymysql://{USER}:{PASSWORD}@{DEV_HOST}:{PORT}/{DB_NAME}?charset=utf8mb4" + +print(f"生产数据库: {PROD_HOST}:{PORT}/{DB_NAME}") +print(f"开发数据库: {DEV_HOST}:{PORT}/{DB_NAME}") +print() + +from sqlalchemy import create_engine, inspect + +def get_db_structure(url): + """获取数据库的完整结构信息。""" + engine = create_engine(url, pool_pre_ping=True, connect_args={'connect_timeout': 10}) + inspector = inspect(engine) + + structure = {} + + # 获取所有表 + tables = inspector.get_table_names() + structure['tables'] = sorted(tables) + + # 获取每个表的详细结构 + table_details = {} + for table in tables: + cols = inspector.get_columns(table) + col_info = {} + for col in cols: + col_info[col['name']] = { + 'type': str(col['type']), + 'nullable': col.get('nullable', True), + 'default': str(col.get('default', '')), + } + # 获取主键 + try: + pk_constraint = inspector.get_pk_constraint(table) + if isinstance(pk_constraint, dict): + primary_key = pk_constraint.get('constrained_columns', []) + else: + primary_key = list(pk_constraint) if pk_constraint else [] + except Exception: + primary_key = [] + + table_details[table] = { + 'columns': col_info, + 'primary_key': primary_key, + 'indexes': [(idx['name'], tuple(idx['column_names']), idx.get('unique', False)) for idx in inspector.get_indexes(table)], + } + + structure['table_details'] = table_details + engine.dispose() + return structure + +def compare_structures(prod, dev): + """对比两个数据库结构,返回差异列表。""" + diffs = [] + + # 1. 对比表列表 + prod_tables = set(prod['tables']) + dev_tables = set(dev['tables']) + + only_in_prod = prod_tables - dev_tables + only_in_dev = dev_tables - prod_tables + + if only_in_prod: + diffs.append(f"⚠️ 仅在生产数据库存在的表 ({len(only_in_prod)}): {sorted(only_in_prod)}") + if only_in_dev: + diffs.append(f"⚠️ 仅在开发数据库存在的表 ({len(only_in_dev)}): {sorted(only_in_dev)}") + + # 2. 对比共有表的结构 + common_tables = prod_tables & dev_tables + for table in sorted(common_tables): + prod_detail = prod['table_details'][table] + dev_detail = dev['table_details'][table] + + # 对比列 + prod_cols = set(prod_detail['columns'].keys()) + dev_cols = set(dev_detail['columns'].keys()) + + cols_only_in_prod = prod_cols - dev_cols + cols_only_in_dev = dev_cols - prod_cols + + if cols_only_in_prod: + diffs.append(f" 📋 表 [{table}]: 仅在生产存在的列: {sorted(cols_only_in_prod)}") + if cols_only_in_dev: + diffs.append(f" 📋 表 [{table}]: 仅在开发存在的列: {sorted(cols_only_in_dev)}") + + # 对比共有列的属性 + common_cols = prod_cols & dev_cols + for col in sorted(common_cols): + prod_col = prod_detail['columns'][col] + dev_col = dev_detail['columns'][col] + + if prod_col['type'] != dev_col['type']: + diffs.append(f" 🔧 表 [{table}].列 [{col}]: 类型不一致 - 生产={prod_col['type']}, 开发={dev_col['type']}") + if prod_col['nullable'] != dev_col['nullable']: + diffs.append(f" 🔧 表 [{table}].列 [{col}]: 可空性不一致 - 生产={prod_col['nullable']}, 开发={dev_col['nullable']}") + + # 对比主键 + if prod_detail['primary_key'] != dev_detail['primary_key']: + diffs.append(f" 🔑 表 [{table}]: 主键不一致 - 生产={prod_detail['primary_key']}, 开发={dev_detail['primary_key']}") + + # 对比索引 + prod_indexes = set(prod_detail['indexes']) + dev_indexes = set(dev_detail['indexes']) + idx_only_in_prod = prod_indexes - dev_indexes + idx_only_in_dev = dev_indexes - prod_indexes + if idx_only_in_prod: + diffs.append(f" 📊 表 [{table}]: 仅在生产存在的索引: {sorted(idx_only_in_prod)}") + if idx_only_in_dev: + diffs.append(f" 📊 表 [{table}]: 仅在开发存在的索引: {sorted(idx_only_in_dev)}") + + return diffs + +try: + print("正在连接生产数据库...") + prod_structure = get_db_structure(PROD_URL) + print(f"✓ 生产数据库: {len(prod_structure['tables'])} 张表") + + print("正在连接开发数据库...") + dev_structure = get_db_structure(DEV_URL) + print(f"✓ 开发数据库: {len(dev_structure['tables'])} 张表") + print() + + diffs = compare_structures(prod_structure, dev_structure) + + if not diffs: + print("✅ 生产数据库和开发数据库结构完全一致!") + else: + print(f"❌ 发现 {len(diffs)} 处差异:") + print() + for diff in diffs: + print(diff) + + # 输出表数量统计 + print() + print("=" * 60) + print(f"表总数: 生产={len(prod_structure['tables'])}, 开发={len(dev_structure['tables'])}") + print(f"共有表: {len(set(prod_structure['tables']) & set(dev_structure['tables']))}") + + # 重点检查新增的 compute_series 和 compute_icons 表 + print() + print("=" * 60) + print("重点检查新增表:") + for new_table in ['compute_series', 'compute_icons']: + in_prod = new_table in prod_structure['tables'] + in_dev = new_table in dev_structure['tables'] + print(f" {new_table}: 生产={'✓' if in_prod else '✗'}, 开发={'✓' if in_dev else '✗'}") + if in_prod and in_dev: + prod_cols = list(prod_structure['table_details'][new_table]['columns'].keys()) + dev_cols = list(dev_structure['table_details'][new_table]['columns'].keys()) + print(f" 生产列 ({len(prod_cols)}): {prod_cols}") + print(f" 开发列 ({len(dev_cols)}): {dev_cols}") + +except Exception as e: + print(f"❌ 错误: {e}") + import traceback + traceback.print_exc() + sys.exit(1) diff --git a/scripts/fix_index_names.py b/scripts/fix_index_names.py new file mode 100644 index 0000000..49c7c67 --- /dev/null +++ b/scripts/fix_index_names.py @@ -0,0 +1,82 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +"""修复生产数据库索引名称,使其与开发数据库一致。""" +import sys + +PROD_HOST = "124.221.143.140" +PORT = 8091 +USER = "opc" +PASSWORD = "jjjsgysyujkwjwgb" +DB_NAME = "opc" + +PROD_URL = f"mysql+pymysql://{USER}:{PASSWORD}@{PROD_HOST}:{PORT}/{DB_NAME}?charset=utf8mb4" + +from sqlalchemy import create_engine, text, inspect + +print(f"连接生产数据库: {PROD_HOST}:{PORT}/{DB_NAME}") +print() + +engine = create_engine(PROD_URL, pool_pre_ping=True, connect_args={'connect_timeout': 10}) + +try: + # 需要重命名的索引:旧名称 -> 新名称 + index_renames = [ + ('compute_series', 'idx_compute_series_name', 'ix_compute_series_name'), + ('compute_icons', 'idx_compute_icons_name', 'ix_compute_icons_name'), + ('compute_icons', 'idx_compute_icons_category', 'ix_compute_icons_category'), + ] + + inspector = inspect(engine) + + for table, old_name, new_name in index_renames: + print(f"处理表 [{table}] 索引: {old_name} -> {new_name}") + + # 检查旧索引是否存在 + existing_indexes = [idx['name'] for idx in inspector.get_indexes(table)] + + if old_name in existing_indexes and new_name not in existing_indexes: + # MySQL 不支持直接重命名索引,需要先删除再创建 + # 获取索引的列信息 + old_index = next((idx for idx in inspector.get_indexes(table) if idx['name'] == old_name), None) + if old_index: + columns = ', '.join(old_index['column_names']) + with engine.begin() as conn: + # 删除旧索引 + conn.execute(text(f"ALTER TABLE {table} DROP INDEX {old_name}")) + # 创建新索引 + conn.execute(text(f"CREATE INDEX {new_name} ON {table} ({columns})")) + print(f" ✓ 索引已重命名") + else: + print(f" ⚠️ 找不到旧索引 {old_name} 的详细信息") + elif new_name in existing_indexes: + print(f" ✓ 新索引 {new_name} 已存在,跳过") + # 如果旧索引也存在,删除旧索引 + if old_name in existing_indexes: + with engine.begin() as conn: + conn.execute(text(f"ALTER TABLE {table} DROP INDEX {old_name}")) + print(f" ✓ 已删除重复的旧索引 {old_name}") + else: + print(f" ⚠️ 旧索引 {old_name} 不存在,跳过") + + # 验证结果 + print() + print("验证索引名称...") + inspector = inspect(engine) + for table in ['compute_series', 'compute_icons']: + indexes = inspector.get_indexes(table) + print(f" [{table}] 索引:") + for idx in indexes: + print(f" - {idx['name']}: {idx['column_names']} (unique={idx.get('unique', False)})") + + print() + print("=" * 60) + print("✅ 索引名称修复完成!") + print("=" * 60) + +except Exception as e: + print(f"❌ 错误: {e}") + import traceback + traceback.print_exc() + sys.exit(1) +finally: + engine.dispose() diff --git a/scripts/fix_prod_db.py b/scripts/fix_prod_db.py new file mode 100644 index 0000000..ccad83b --- /dev/null +++ b/scripts/fix_prod_db.py @@ -0,0 +1,127 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +"""直接在生产数据库创建 compute_series 和 compute_icons 表,并更新 alembic 版本。""" +import sys + +# 生产数据库连接配置 +PROD_HOST = "124.221.143.140" +PORT = 8091 +USER = "opc" +PASSWORD = "jjjsgysyujkwjwgb" +DB_NAME = "opc" + +PROD_URL = f"mysql+pymysql://{USER}:{PASSWORD}@{PROD_HOST}:{PORT}/{DB_NAME}?charset=utf8mb4" + +from sqlalchemy import create_engine, text, inspect + +print(f"连接生产数据库: {PROD_HOST}:{PORT}/{DB_NAME}") +print() + +engine = create_engine(PROD_URL, pool_pre_ping=True, connect_args={'connect_timeout': 10}) + +try: + # 1. 创建 compute_series 表 + print("1. 创建 compute_series 表...") + with engine.begin() as conn: + conn.execute(text(""" + CREATE TABLE IF NOT EXISTS compute_series ( + id VARCHAR(64) PRIMARY KEY, + name VARCHAR(128) NOT NULL, + display_name VARCHAR(256) DEFAULT '', + supplier VARCHAR(128) DEFAULT '', + icon VARCHAR(512) DEFAULT '', + icon_file VARCHAR(512) DEFAULT '', + description TEXT, + sort_order INTEGER DEFAULT 0, + status VARCHAR(32) DEFAULT 'active', + created_at VARCHAR(32) DEFAULT '', + updated_at VARCHAR(32) DEFAULT '', + UNIQUE KEY uq_compute_series_name (name), + KEY idx_compute_series_name (name) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci + """)) + print(" ✓ compute_series 表创建成功") + + # 2. 创建 compute_icons 表 + print("2. 创建 compute_icons 表...") + with engine.begin() as conn: + conn.execute(text(""" + CREATE TABLE IF NOT EXISTS compute_icons ( + id VARCHAR(64) PRIMARY KEY, + name VARCHAR(128) NOT NULL, + category VARCHAR(32) DEFAULT 'model', + url VARCHAR(512) DEFAULT '', + file VARCHAR(512) DEFAULT '', + file_size INTEGER DEFAULT 0, + mime_type VARCHAR(64) DEFAULT '', + width INTEGER DEFAULT 0, + height INTEGER DEFAULT 0, + description TEXT, + usage_count INTEGER DEFAULT 0, + status VARCHAR(32) DEFAULT 'active', + created_at VARCHAR(32) DEFAULT '', + updated_at VARCHAR(32) DEFAULT '', + KEY idx_compute_icons_name (name), + KEY idx_compute_icons_category (category) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci + """)) + print(" ✓ compute_icons 表创建成功") + + # 3. 更新 alembic_version 表 + print("3. 更新 alembic_version 到 0078...") + with engine.begin() as conn: + # 检查 alembic_version 表是否存在 + result = conn.execute(text("SHOW TABLES LIKE 'alembic_version'")) + if result.fetchone(): + # 更新版本号 + conn.execute(text("UPDATE alembic_version SET version_num = '0078'")) + print(" ✓ alembic_version 已更新到 0078") + else: + # 创建表并插入版本号 + conn.execute(text(""" + CREATE TABLE alembic_version ( + version_num VARCHAR(32) NOT NULL, + PRIMARY KEY (version_num) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 + """)) + conn.execute(text("INSERT INTO alembic_version (version_num) VALUES ('0078')")) + print(" ✓ alembic_version 表已创建并设置为 0078") + + # 4. 验证表是否创建成功 + print() + print("4. 验证表结构...") + inspector = inspect(engine) + tables = inspector.get_table_names() + + for table_name in ['compute_series', 'compute_icons']: + if table_name in tables: + cols = inspector.get_columns(table_name) + print(f" ✓ {table_name}: {len(cols)} 列") + for col in cols: + print(f" - {col['name']}: {col['type']}") + else: + print(f" ✗ {table_name}: 不存在!") + + # 5. 验证 alembic 版本 + print() + print("5. 验证 alembic 版本...") + with engine.connect() as conn: + result = conn.execute(text("SELECT version_num FROM alembic_version")) + version = result.fetchone() + if version: + print(f" ✓ 当前 alembic 版本: {version[0]}") + else: + print(" ✗ alembic_version 表为空") + + print() + print("=" * 60) + print("✅ 生产数据库修复完成!") + print("=" * 60) + +except Exception as e: + print(f"❌ 错误: {e}") + import traceback + traceback.print_exc() + sys.exit(1) +finally: + engine.dispose()