From 4f90000c3692091be26cdfef9411ca12dbf5dc99 Mon Sep 17 00:00:00 2001 From: Nixevol Date: Thu, 25 Jun 2026 02:32:08 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=95=B0=E6=8D=AE=E7=AE=A1=E7=90=86?= =?UTF-8?q?=E6=94=AF=E6=8C=81=E5=88=87=E6=8D=A2=20CellData=20=E6=95=B0?= =?UTF-8?q?=E6=8D=AE=E5=BA=93?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Configure.json | 33 ++++++- app/api/routers/database.py | 69 ++++++++++----- app/database.py | 9 +- app/warehouse.py | 4 + docs/project_context.md | 7 ++ frontend/src/components/DatabasePanel.vue | 102 +++++++++++++++++++--- 6 files changed, 182 insertions(+), 42 deletions(-) diff --git a/Configure.json b/Configure.json index c72bac6..3143e7a 100644 --- a/Configure.json +++ b/Configure.json @@ -1,13 +1,13 @@ { - "Update": "2026/06/24 23:16:46", + "Update": "2026/06/25 02:26:49", "SourceType": "sftp", "WarehouseType": "mysql", "MySQL_DBInfo": { "host": "127.0.0.1", "port": 3306, "user": "root", - "passwd": "123456", - "dbname": "CapacityReport" + "dbname": "CapacityReport", + "passwd": "123456" }, "Metrix": { "base_url": "http://host.docker.internal:8000", @@ -204,6 +204,33 @@ }, "passwd": "242520" }, + "CellData": { + "remote_data": { + "enabled": false, + "protocol": "sftp", + "host": "", + "port": 22, + "user": "", + "remote_dir": "/CellData", + "passive": true, + "timeout": 30, + "auto_delete_source": false, + "auto_scheduler": { + "enabled": false, + "check_interval_hours": 1, + "expected_directories": [], + "week_offset": 0 + }, + "passwd": "" + }, + "mysql": { + "host": "localhost", + "port": 3306, + "user": "root", + "dbname": "celldata", + "passwd": "123456" + } + }, "HistoryRetention": { "enabled": true, "keep_count": 1 diff --git a/app/api/routers/database.py b/app/api/routers/database.py index 1e911da..e9d2e0a 100644 --- a/app/api/routers/database.py +++ b/app/api/routers/database.py @@ -12,7 +12,7 @@ from app.config import CACHE_DIR from app.database import DatabaseManager from app.services.platform import make_client from app.utils.files import remove_file_safely -from app.warehouse import make_warehouse +from app.warehouse import make_cell_data_warehouse, make_warehouse router = APIRouter(tags=["database"]) @@ -42,6 +42,9 @@ def _make_sheet_name(table_name: str, used_names: set[str]) -> str: return sheet_name +DatabaseSource = str + + def _dataframe_from_table(db: DatabaseManager, table_name: str) -> pd.DataFrame: result = db.query_table(table_name, page=1, page_size=1000000) table_info = db.get_table_info(table_name) @@ -49,21 +52,26 @@ def _dataframe_from_table(db: DatabaseManager, table_name: str) -> pd.DataFrame: return pd.DataFrame(result["data"], columns=columns) -def _db(): +def _db(database_source: DatabaseSource = "main"): """Direct MySQL DatabaseManager, or a Metrix-backed warehouse with the same interface.""" - return make_warehouse(state.current_config()) + config = state.current_config() + if database_source == "cell_data": + return make_cell_data_warehouse(config) + if database_source != "main": + raise HTTPException(status_code=400, detail="不支持的数据库来源") + return make_warehouse(config) @router.post("/api/database/test") -def test_database(): - db = _db() +def test_database(database_source: DatabaseSource = Body("main", embed=True)): + db = _db(database_source) success, message = db.test_connection() return {"success": success, "message": message} @router.get("/api/database/info") -def get_database_info(): - db = _db() +def get_database_info(database_source: DatabaseSource = "main"): + db = _db(database_source) try: return {"success": True, **db.get_server_info()} except Exception as exc: @@ -72,8 +80,8 @@ def get_database_info(): @router.get("/api/database/tables") @router.post("/api/database/tables") -def get_tables(): - db = _db() +def get_tables(database_source: DatabaseSource = Body("main", embed=True)): + db = _db(database_source) try: return {"tables": db.get_tables()} except Exception as exc: @@ -81,8 +89,11 @@ def get_tables(): @router.post("/api/database/table/info") -def get_table_info(table_name: str = Body(..., embed=True)): - db = _db() +def get_table_info( + table_name: str = Body(..., embed=True), + database_source: DatabaseSource = Body("main"), +): + db = _db(database_source) try: return db.get_table_info(table_name) except Exception as exc: @@ -92,12 +103,13 @@ def get_table_info(table_name: str = Body(..., embed=True)): @router.post("/api/database/table/data") def query_table_data( table_name: str = Body(..., embed=True), + database_source: DatabaseSource = Body("main"), page: int = Body(1), page_size: int = Body(50), order_by: Optional[str] = Body(None), order_dir: str = Body("ASC"), ): - db = _db() + db = _db(database_source) try: return db.query_table(table_name, page, page_size, order_by=order_by, order_dir=order_dir) except Exception as exc: @@ -107,13 +119,14 @@ def query_table_data( @router.post("/api/database/table/query") def query_table_with_filter( table_name: str = Body(..., embed=True), + database_source: DatabaseSource = Body("main"), page: int = Body(1), page_size: int = Body(50), filters: Optional[dict[str, str]] = Body(None), order_by: Optional[str] = Body(None), order_dir: str = Body("ASC"), ): - db = _db() + db = _db(database_source) try: return db.query_table( table_name, @@ -128,8 +141,11 @@ def query_table_with_filter( @router.post("/api/database/table/truncate") -def truncate_table(table_name: str = Body(..., embed=True)): - db = _db() +def truncate_table( + table_name: str = Body(..., embed=True), + database_source: DatabaseSource = Body("main"), +): + db = _db(database_source) try: db.truncate_table(table_name) return {"success": True, "message": f"表 {table_name} 已清空"} @@ -138,8 +154,11 @@ def truncate_table(table_name: str = Body(..., embed=True)): @router.post("/api/database/table/drop") -def drop_table(table_name: str = Body(..., embed=True)): - db = _db() +def drop_table( + table_name: str = Body(..., embed=True), + database_source: DatabaseSource = Body("main"), +): + db = _db(database_source) try: db.drop_table(table_name) return {"success": True, "message": f"表 {table_name} 已删除"} @@ -148,8 +167,8 @@ def drop_table(table_name: str = Body(..., embed=True)): @router.post("/api/database/table/drop-all") -def drop_all_tables(): - db = _db() +def drop_all_tables(database_source: DatabaseSource = Body("main", embed=True)): + db = _db(database_source) try: result = db.drop_all_tables() return { @@ -163,8 +182,11 @@ def drop_all_tables(): @router.post("/api/database/execute") -def execute_sql(sql: str = Body(..., embed=True)): - db = _db() +def execute_sql( + sql: str = Body(..., embed=True), + database_source: DatabaseSource = Body("main"), +): + db = _db(database_source) try: success, result = db.execute_sql(sql) if success: @@ -182,6 +204,7 @@ def execute_sql(sql: str = Body(..., embed=True)): def download_table( table_name: Optional[str] = Body(None, embed=True), table_names: Optional[list[str]] = Body(None, embed=True), + database_source: DatabaseSource = Body("main"), file_format: str = Body("csv", alias="format"), ): if file_format not in {"csv", "xlsx"}: @@ -194,10 +217,10 @@ def download_table( raise HTTPException(status_code=400, detail="CSV 每次只能导出一张表") config = state.current_config() - if config.warehouse_type == "metrix": + if database_source == "main" and config.warehouse_type == "metrix": return _download_via_metrix(config, requested_tables, file_format) - db = _db() + db = _db(database_source) try: available_tables = set(db.get_tables()) missing_tables = [name for name in requested_tables if name not in available_tables] diff --git a/app/database.py b/app/database.py index eafacd3..3dac2fa 100644 --- a/app/database.py +++ b/app/database.py @@ -5,14 +5,15 @@ import pymysql from contextlib import contextmanager from typing import Any, Dict, List, Optional, Tuple -from app.config import AppConfig +from app.config import AppConfig, MySQLConfig class DatabaseManager: """数据库管理器""" - def __init__(self, config: AppConfig): + def __init__(self, config: AppConfig, mysql_config: MySQLConfig | None = None): self.config = config + self.mysql_config = (mysql_config or config.mysql).normalized() @contextmanager def get_connection(self): @@ -26,7 +27,7 @@ class DatabaseManager: 如果需要高性能的短连接操作,请使用 engine 属性(连接池) """ - mysql = self.config.mysql + mysql = self.mysql_config conn = pymysql.connect( host=mysql.host, port=mysql.port, @@ -46,7 +47,7 @@ class DatabaseManager: @contextmanager def get_fast_connection(self): """获取高性能 PyMySQL 连接(用于批量插入)""" - mysql = self.config.mysql + mysql = self.mysql_config conn = pymysql.connect( host=mysql.host, port=mysql.port, diff --git a/app/warehouse.py b/app/warehouse.py index 6708db9..2db94a1 100644 --- a/app/warehouse.py +++ b/app/warehouse.py @@ -22,6 +22,10 @@ def make_warehouse(config: AppConfig): return DatabaseManager(config) +def make_cell_data_warehouse(config: AppConfig): + return DatabaseManager(config, config.cell_data.normalized().mysql) + + def _quote_ident(name: str) -> str: return "`" + str(name).replace("`", "``") + "`" diff --git a/docs/project_context.md b/docs/project_context.md index 4e9b73f..ef56744 100644 --- a/docs/project_context.md +++ b/docs/project_context.md @@ -717,3 +717,10 @@ - 设置页说明文案去掉开发实现细节,只保留用户填写配置所需的短提示:主数据源/仓库、Metrix 连接、CellData 数据库/远程源、远程数据源、自动调度、目录映射、Sheet 过滤和字段映射等位置均已压缩。 - 历史删除确认中的“缓存文件”改为“相关文件”,避免把内部存储实现暴露给用户。 - 验证:`frontend` `npm run build` 通过;构建产物已清理。 + +## 2026-06-25:数据管理支持切换主数据库与 CellData 数据库 + +- 数据管理页左侧表列表新增数据库选择,可在主数据库与 CellData 数据库之间切换;选项名称跟随系统设置里的主仓库库名和 `CellData.MySQL_DBInfo.dbname`。 +- 后端数据库接口新增 `database_source` 参数,`main` 保持原有直连 MySQL / Metrix 仓库逻辑,`cell_data` 使用 `CellData.MySQL_DBInfo` 创建独立 MySQL 仓库。表列表、表结构、分页数据、清空、删除、删除全部、执行 SQL 和导出均按该参数选择数据库。 +- CellData 数据库当前只支持直连 MySQL 配置;Metrix 仓库模式只影响主数据库。 +- 验证:`python -m compileall -q app` 通过;`frontend` `npm run build` 通过(仅既有大 chunk 提示);构建产物与 Python 缓存已清理。 diff --git a/frontend/src/components/DatabasePanel.vue b/frontend/src/components/DatabasePanel.vue index f639878..550f5e3 100644 --- a/frontend/src/components/DatabasePanel.vue +++ b/frontend/src/components/DatabasePanel.vue @@ -3,7 +3,16 @@