mirror of
https://github.com/rnvm9wjdtj-bot/myaps_api.git
synced 2026-06-02 05:54:40 +00:00
feat: 添加数据库迁移功能并新增worker字段
1. 新增数据库结构迁移的后端API与前端UI交互功能 2. 为机台模具关联模型新增worker人员字段 3. 优化建表脚本为可重入的增量更新模式 4. 移除旧的业务规则校验函数
This commit is contained in:
@@ -408,6 +408,7 @@ _PAGE_COLUMNS_CONFIG = {
|
||||
{"field": "basesec", "title": "基础工时"},
|
||||
{"field": "fixsec", "title": "额定时间"},
|
||||
{"field": "priority", "title": "优先级"},
|
||||
{"field": "worker", "title": "人员"},
|
||||
{"field": "memo", "title": "备注"},
|
||||
] + SYSTEM_COMMON_COLUMNS_SUFFIX,
|
||||
}
|
||||
|
||||
@@ -1674,3 +1674,7 @@ async def get_index_stats(request: Request):
|
||||
except Exception as e:
|
||||
logger.error(f"获取首页统计失败: {str(e)}")
|
||||
return standard_response(success=0, message=str(e))
|
||||
|
||||
|
||||
from .migrations.migrate_router import migrate_router
|
||||
rt.include_router(migrate_router)
|
||||
|
||||
@@ -64,61 +64,6 @@ async def validate_material_type_e_rules(cleaner, data, staging_id):
|
||||
return errors
|
||||
|
||||
|
||||
# async def validate_mat_wc_rules(cleaner, data, staging_id):
|
||||
# """工艺路线业务规则校验:外键存在校验"""
|
||||
# errors = []
|
||||
# if data.get("materialno") and data.get("matver"):
|
||||
# exists = await TMatVer.filter(materialno=data["materialno"], matver=data["matver"]).exists()
|
||||
# if not exists:
|
||||
# errors.append(cleaner._create_error(
|
||||
# staging_id, ErrorType.FK_NOT_FOUND, "matver",
|
||||
# f"{data['materialno']}/{data['matver']}", "关联的产线版本不存在"
|
||||
# ))
|
||||
# return errors
|
||||
|
||||
|
||||
# async def validate_mat_wc_bom_rules(cleaner, data, staging_id):
|
||||
# """物料清单业务规则校验:外键存在校验"""
|
||||
# errors = []
|
||||
# # productno/materialno 比较已用 config_rules 替代
|
||||
# if data.get("productno") and data.get("matver"):
|
||||
# exists = await TMatVer.filter(materialno=data["productno"], matver=data["matver"]).exists()
|
||||
# if not exists:
|
||||
# errors.append(cleaner._create_error(
|
||||
# staging_id, ErrorType.FK_NOT_FOUND, "matver",
|
||||
# f"{data['productno']}/{data['matver']}", "关联的产线版本不存在"
|
||||
# ))
|
||||
# if data.get("productno") and data.get("matver") and data.get("itemno"):
|
||||
# exists = await TMatWc.filter(
|
||||
# materialno=data["productno"],
|
||||
# matver=data["matver"],
|
||||
# itemno=data["itemno"]
|
||||
# ).exists()
|
||||
# if not exists:
|
||||
# errors.append(cleaner._create_error(
|
||||
# staging_id, ErrorType.FK_NOT_FOUND, "itemno",
|
||||
# f"{data['productno']}/{data['matver']}/{data['itemno']}", "关联的工序不存在"
|
||||
# ))
|
||||
# # qty 和 scrap 范围已用 config_rules 替代
|
||||
# return errors
|
||||
|
||||
|
||||
# async def validate_mat_wc_mold_rules(cleaner, data, staging_id):
|
||||
# """机台模具关联业务规则校验:外键存在校验"""
|
||||
# errors = []
|
||||
# if data.get("materialno") and data.get("workcenter") and data.get("itemno"):
|
||||
# exists = await TMatWc.filter(
|
||||
# materialno=data["materialno"],
|
||||
# workcenter=data["workcenter"],
|
||||
# itemno=data["itemno"]
|
||||
# ).exists()
|
||||
# if not exists:
|
||||
# errors.append(cleaner._create_error(
|
||||
# staging_id, ErrorType.FK_NOT_FOUND, "itemno",
|
||||
# f"{data['materialno']}/{data['workcenter']}/{data['itemno']}", "关联的工艺路线不存在"
|
||||
# ))
|
||||
# return errors
|
||||
|
||||
async def bom_structure_check_hook(processor, table_name: str, context: dict) -> dict:
|
||||
"""
|
||||
BOM结构完整性校验钩子
|
||||
|
||||
@@ -466,6 +466,7 @@ class ProtoMatWcMold(TortoiseBaseModel):
|
||||
basesec = fields.IntField(source_field='BaseSec', blank=True, null=True, description='节拍') # Field name made lowercase.
|
||||
fixsec = fields.IntField(source_field='FixSec', blank=True, null=True, description='额定时间(秒)') # Field name made lowercase.
|
||||
priority = fields.IntField(source_field='Priority', blank=True, null=True, description='优先级') # Field name made lowercase.
|
||||
worker = fields.FloatField(source_field='Worker', blank=True, null=True, description='人员') # Field name made lowercase.
|
||||
memo = fields.CharField(source_field='Memo', max_length=255, blank=True, null=True) # Field name made lowercase.
|
||||
|
||||
class Meta:
|
||||
|
||||
@@ -549,6 +549,7 @@ class AcceptMatWcMold(BaseModel):
|
||||
basesec: float = Field(..., ge=0, description='节拍T/T(秒/100)', example=600)
|
||||
fixsec: int = Field(0, ge=0, description='额定时间(秒)', example=300)
|
||||
priority: int = Field(..., description='优先级', example=1)
|
||||
worker: float = Field(0, ge=0, description='人员', example=1.0)
|
||||
memo: Optional[str] = Field(None, max_length=255, description='备注', example="标准机台模具配置")
|
||||
_raw_input_data: Dict[str, Any] = PrivateAttr(default=None)
|
||||
|
||||
|
||||
@@ -1,9 +1,22 @@
|
||||
-- =====================================================
|
||||
-- APS数据清洗缓冲表建表脚本 (PostgreSQL版本)
|
||||
-- 版本: V001
|
||||
-- 生成时间: 自动生成
|
||||
-- 说明: 用于存储外部系统导入的原始数据,支持数据校验和清洗
|
||||
-- 说明: 可重入脚本,支持重复执行和增量更新
|
||||
-- =====================================================
|
||||
|
||||
-- =====================================================
|
||||
-- 0. 初始化版本管理表
|
||||
-- =====================================================
|
||||
CREATE TABLE IF NOT EXISTS t_schema_version (
|
||||
id SERIAL PRIMARY KEY,
|
||||
version VARCHAR(16) UNIQUE NOT NULL,
|
||||
applied_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
|
||||
description TEXT,
|
||||
sql_scripts TEXT,
|
||||
status VARCHAR(16) DEFAULT 'applied'
|
||||
);
|
||||
|
||||
-- =====================================================
|
||||
-- 1. 物料缓冲表
|
||||
-- =====================================================
|
||||
@@ -60,17 +73,45 @@ CREATE TABLE IF NOT EXISTS t_material_staging (
|
||||
"Sys_Stamp" TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- 增量添加字段(可重入)
|
||||
DO $$
|
||||
BEGIN
|
||||
-- V001 新增字段示例(演示增量模式)
|
||||
IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 't_material_staging' AND column_name = 'free4') THEN
|
||||
ALTER TABLE t_material_staging ADD COLUMN "Free4" VARCHAR(255) NULL;
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
-- 索引(幂等创建)
|
||||
CREATE INDEX IF NOT EXISTS idx_mat_stg_status ON t_material_staging(_status);
|
||||
CREATE INDEX IF NOT EXISTS idx_mat_stg_source ON t_material_staging(_source_system);
|
||||
CREATE INDEX IF NOT EXISTS idx_mat_stg_materialno ON t_material_staging("MaterialNo");
|
||||
|
||||
COMMENT ON TABLE t_material_staging IS '物料数据缓冲表';
|
||||
COMMENT ON COLUMN t_material_staging._staging_id IS '缓冲表主键';
|
||||
COMMENT ON COLUMN t_material_staging._source_system IS '来源系统';
|
||||
COMMENT ON COLUMN t_material_staging._status IS '处理状态: pending/validated/approved/rejected/synced';
|
||||
COMMENT ON COLUMN t_material_staging."MaterialNo" IS '物料号';
|
||||
COMMENT ON COLUMN t_material_staging."Description" IS '物料描述';
|
||||
COMMENT ON COLUMN t_material_staging."Type" IS '物料类型: E-自制, P-采购, F-委外, M-模具, B-虚拟';
|
||||
-- 注释(幂等创建)
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (SELECT 1 FROM pg_description WHERE objoid = 't_material_staging'::regclass AND objsubid = 0) THEN
|
||||
COMMENT ON TABLE t_material_staging IS '物料数据缓冲表';
|
||||
END IF;
|
||||
IF NOT EXISTS (SELECT 1 FROM pg_description WHERE objoid = 't_material_staging'::regclass AND objsubid = (SELECT attnum FROM pg_attribute WHERE attrelid = 't_material_staging'::regclass AND attname = '_staging_id')) THEN
|
||||
COMMENT ON COLUMN t_material_staging._staging_id IS '缓冲表主键';
|
||||
END IF;
|
||||
IF NOT EXISTS (SELECT 1 FROM pg_description WHERE objoid = 't_material_staging'::regclass AND objsubid = (SELECT attnum FROM pg_attribute WHERE attrelid = 't_material_staging'::regclass AND attname = '_source_system')) THEN
|
||||
COMMENT ON COLUMN t_material_staging._source_system IS '来源系统';
|
||||
END IF;
|
||||
IF NOT EXISTS (SELECT 1 FROM pg_description WHERE objoid = 't_material_staging'::regclass AND objsubid = (SELECT attnum FROM pg_attribute WHERE attrelid = 't_material_staging'::regclass AND attname = '_status')) THEN
|
||||
COMMENT ON COLUMN t_material_staging._status IS '处理状态: pending/validated/approved/rejected/synced';
|
||||
END IF;
|
||||
IF NOT EXISTS (SELECT 1 FROM pg_description WHERE objoid = 't_material_staging'::regclass AND objsubid = (SELECT attnum FROM pg_attribute WHERE attrelid = 't_material_staging'::regclass AND attname = '"MaterialNo"')) THEN
|
||||
COMMENT ON COLUMN t_material_staging."MaterialNo" IS '物料号';
|
||||
END IF;
|
||||
IF NOT EXISTS (SELECT 1 FROM pg_description WHERE objoid = 't_material_staging'::regclass AND objsubid = (SELECT attnum FROM pg_attribute WHERE attrelid = 't_material_staging'::regclass AND attname = '"Description"')) THEN
|
||||
COMMENT ON COLUMN t_material_staging."Description" IS '物料描述';
|
||||
END IF;
|
||||
IF NOT EXISTS (SELECT 1 FROM pg_description WHERE objoid = 't_material_staging'::regclass AND objsubid = (SELECT attnum FROM pg_attribute WHERE attrelid = 't_material_staging'::regclass AND attname = '"Type"')) THEN
|
||||
COMMENT ON COLUMN t_material_staging."Type" IS '物料类型: E-自制, P-采购, F-委外, M-模具, B-虚拟';
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
-- =====================================================
|
||||
-- 2. 工作中心缓冲表
|
||||
@@ -109,7 +150,12 @@ CREATE TABLE IF NOT EXISTS t_workcenter_staging (
|
||||
CREATE INDEX IF NOT EXISTS idx_wc_stg_status ON t_workcenter_staging(_status);
|
||||
CREATE INDEX IF NOT EXISTS idx_wc_stg_workcenter ON t_workcenter_staging("WorkCenter");
|
||||
|
||||
COMMENT ON TABLE t_workcenter_staging IS '工作中心数据缓冲表';
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (SELECT 1 FROM pg_description WHERE objoid = 't_workcenter_staging'::regclass AND objsubid = 0) THEN
|
||||
COMMENT ON TABLE t_workcenter_staging IS '工作中心数据缓冲表';
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
-- =====================================================
|
||||
-- 3. 产线版本缓冲表
|
||||
@@ -141,7 +187,12 @@ CREATE TABLE IF NOT EXISTS t_mat_ver_staging (
|
||||
CREATE INDEX IF NOT EXISTS idx_mvr_stg_status ON t_mat_ver_staging(_status);
|
||||
CREATE INDEX IF NOT EXISTS idx_mvr_stg_mat_ver ON t_mat_ver_staging("MaterialNo", "MatVer");
|
||||
|
||||
COMMENT ON TABLE t_mat_ver_staging IS '产线版本数据缓冲表';
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (SELECT 1 FROM pg_description WHERE objoid = 't_mat_ver_staging'::regclass AND objsubid = 0) THEN
|
||||
COMMENT ON TABLE t_mat_ver_staging IS '产线版本数据缓冲表';
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
-- =====================================================
|
||||
-- 4. 工艺路线缓冲表
|
||||
@@ -179,7 +230,12 @@ CREATE INDEX IF NOT EXISTS idx_mwc_stg_status ON t_mat_wc_staging(_status);
|
||||
CREATE INDEX IF NOT EXISTS idx_mwc_stg_mat_ver_item ON t_mat_wc_staging("MaterialNo", "MatVer", "ItemNo");
|
||||
CREATE INDEX IF NOT EXISTS idx_mwc_stg_workcenter ON t_mat_wc_staging("WorkCenter");
|
||||
|
||||
COMMENT ON TABLE t_mat_wc_staging IS '工艺路线数据缓冲表';
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (SELECT 1 FROM pg_description WHERE objoid = 't_mat_wc_staging'::regclass AND objsubid = 0) THEN
|
||||
COMMENT ON TABLE t_mat_wc_staging IS '工艺路线数据缓冲表';
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
-- =====================================================
|
||||
-- 5. 物料清单缓冲表
|
||||
@@ -199,11 +255,11 @@ CREATE TABLE IF NOT EXISTS t_mat_wc_bom_staging (
|
||||
|
||||
-- ProtoMatWcBom 字段
|
||||
"ProductNo" VARCHAR(64) NOT NULL,
|
||||
"ProductUnit" VARCHAR(16) NULL,
|
||||
"ProductUnit" VARCHAR(16) NULL,
|
||||
"MatVer" VARCHAR(4) NOT NULL,
|
||||
"ItemNo" VARCHAR(6) NOT NULL,
|
||||
"MaterialNo" VARCHAR(64) NOT NULL,
|
||||
"MaterialUnit" VARCHAR(16) NULL,
|
||||
"MaterialUnit" VARCHAR(16) NULL,
|
||||
"Qty" DOUBLE PRECISION NOT NULL,
|
||||
"OffsetHour" INT NOT NULL,
|
||||
"TreeNo" INT NULL,
|
||||
@@ -218,7 +274,12 @@ CREATE INDEX IF NOT EXISTS idx_bom_stg_status ON t_mat_wc_bom_staging(_status);
|
||||
CREATE INDEX IF NOT EXISTS idx_bom_stg_product ON t_mat_wc_bom_staging("ProductNo", "MatVer", "ItemNo", "MaterialNo");
|
||||
CREATE INDEX IF NOT EXISTS idx_bom_stg_materialno ON t_mat_wc_bom_staging("MaterialNo");
|
||||
|
||||
COMMENT ON TABLE t_mat_wc_bom_staging IS '物料清单数据缓冲表';
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (SELECT 1 FROM pg_description WHERE objoid = 't_mat_wc_bom_staging'::regclass AND objsubid = 0) THEN
|
||||
COMMENT ON TABLE t_mat_wc_bom_staging IS '物料清单数据缓冲表';
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
-- =====================================================
|
||||
-- 6. 模具缓冲表
|
||||
@@ -249,9 +310,18 @@ CREATE TABLE IF NOT EXISTS t_mold_staging (
|
||||
CREATE INDEX IF NOT EXISTS idx_mold_stg_status ON t_mold_staging(_status);
|
||||
CREATE INDEX IF NOT EXISTS idx_mold_stg_moldno ON t_mold_staging("MoldNo");
|
||||
|
||||
COMMENT ON TABLE t_mold_staging IS '模具数据缓冲表';
|
||||
COMMENT ON COLUMN t_mold_staging."Type" IS '模具类型: 注塑/冲压/压铸/夹具';
|
||||
COMMENT ON COLUMN t_mold_staging."Status" IS '模具状态: 空闲/生产中/维修中/报废';
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (SELECT 1 FROM pg_description WHERE objoid = 't_mold_staging'::regclass AND objsubid = 0) THEN
|
||||
COMMENT ON TABLE t_mold_staging IS '模具数据缓冲表';
|
||||
END IF;
|
||||
IF NOT EXISTS (SELECT 1 FROM pg_description WHERE objoid = 't_mold_staging'::regclass AND objsubid = (SELECT attnum FROM pg_attribute WHERE attrelid = 't_mold_staging'::regclass AND attname = '"Type"')) THEN
|
||||
COMMENT ON COLUMN t_mold_staging."Type" IS '模具类型: 注塑/冲压/压铸/夹具';
|
||||
END IF;
|
||||
IF NOT EXISTS (SELECT 1 FROM pg_description WHERE objoid = 't_mold_staging'::regclass AND objsubid = (SELECT attnum FROM pg_attribute WHERE attrelid = 't_mold_staging'::regclass AND attname = '"Status"')) THEN
|
||||
COMMENT ON COLUMN t_mold_staging."Status" IS '模具状态: 空闲/生产中/维修中/报废';
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
-- =====================================================
|
||||
-- 7. 机台模具关联缓冲表
|
||||
@@ -284,7 +354,12 @@ CREATE INDEX IF NOT EXISTS idx_mwm_stg_status ON t_mat_wc_mold_staging(_status);
|
||||
CREATE INDEX IF NOT EXISTS idx_mwm_stg_mat_wc ON t_mat_wc_mold_staging("MaterialNo", "WorkCenter", "ItemNo", "MoldNo");
|
||||
CREATE INDEX IF NOT EXISTS idx_mwm_stg_moldno ON t_mat_wc_mold_staging("MoldNo");
|
||||
|
||||
COMMENT ON TABLE t_mat_wc_mold_staging IS '机台模具关联数据缓冲表';
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (SELECT 1 FROM pg_description WHERE objoid = 't_mat_wc_mold_staging'::regclass AND objsubid = 0) THEN
|
||||
COMMENT ON TABLE t_mat_wc_mold_staging IS '机台模具关联数据缓冲表';
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
-- =====================================================
|
||||
-- 8. 校验错误记录表
|
||||
@@ -305,7 +380,12 @@ CREATE INDEX IF NOT EXISTS idx_err_staging ON t_validation_error(staging_table,
|
||||
CREATE INDEX IF NOT EXISTS idx_err_type ON t_validation_error(error_type);
|
||||
CREATE INDEX IF NOT EXISTS idx_err_time ON t_validation_error(createtime);
|
||||
|
||||
COMMENT ON TABLE t_validation_error IS '校验错误记录表';
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (SELECT 1 FROM pg_description WHERE objoid = 't_validation_error'::regclass AND objsubid = 0) THEN
|
||||
COMMENT ON TABLE t_validation_error IS '校验错误记录表';
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
-- =====================================================
|
||||
-- 9. 数据转换规则配置表
|
||||
@@ -330,10 +410,15 @@ CREATE INDEX IF NOT EXISTS idx_rule_source ON t_transform_rule(source_system);
|
||||
CREATE INDEX IF NOT EXISTS idx_rule_target ON t_transform_rule(target_table);
|
||||
CREATE INDEX IF NOT EXISTS idx_rule_active ON t_transform_rule(is_active);
|
||||
|
||||
COMMENT ON TABLE t_transform_rule IS '数据转换规则配置表';
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (SELECT 1 FROM pg_description WHERE objoid = 't_transform_rule'::regclass AND objsubid = 0) THEN
|
||||
COMMENT ON TABLE t_transform_rule IS '数据转换规则配置表';
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
-- =====================================================
|
||||
-- 创建更新时间触发器函数
|
||||
-- 10. 创建更新时间触发器函数(幂等)
|
||||
-- =====================================================
|
||||
CREATE OR REPLACE FUNCTION update_timestamp()
|
||||
RETURNS TRIGGER AS $$
|
||||
@@ -343,7 +428,7 @@ BEGIN
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
-- 为每个缓冲表创建触发器
|
||||
-- 为每个缓冲表创建触发器(幂等)
|
||||
DO $$
|
||||
DECLARE
|
||||
tbl TEXT;
|
||||
@@ -365,11 +450,26 @@ BEGIN
|
||||
tbl, tbl, tbl, tbl
|
||||
);
|
||||
END LOOP;
|
||||
END;
|
||||
$$;
|
||||
END $$;
|
||||
|
||||
-- =====================================================
|
||||
-- 11. 记录版本(幂等)
|
||||
-- =====================================================
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (SELECT 1 FROM t_schema_version WHERE version = 'V001') THEN
|
||||
INSERT INTO t_schema_version (version, description, sql_scripts)
|
||||
VALUES (
|
||||
'V001',
|
||||
'初始化缓冲表结构:物料、工作中心、产线版本、工艺路线、物料清单、模具、机台模具关联、校验错误、转换规则',
|
||||
'staging_tables.sql V001'
|
||||
);
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
-- =====================================================
|
||||
-- 完成提示
|
||||
-- =====================================================
|
||||
-- 执行完成后,可通过以下命令验证表创建成功:
|
||||
-- 执行完成后,可通过以下命令验证:
|
||||
-- SELECT tablename FROM pg_tables WHERE tablename LIKE '%staging' OR tablename IN ('t_validation_error', 't_transform_rule');
|
||||
-- SELECT * FROM t_schema_version ORDER BY applied_at DESC;
|
||||
|
||||
+64
-3
@@ -145,9 +145,13 @@
|
||||
{cards}
|
||||
</div>
|
||||
|
||||
<div class="text-center mt-5">
|
||||
<!-- <small class="text-white opacity-50">数据清洗管理系统 v1.0</small> -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="migrateTriggerZone" style="position: fixed; right: 0; bottom: 0; width: 120px; height: 120px; z-index: 1000; cursor: default;">
|
||||
<button class="btn btn-sm" id="migrateBtn" title="数据库结构迁移" disabled
|
||||
style="opacity: 0; pointer-events: none; background: rgba(108, 117, 234, 0.9); color: white; border: none; border-radius: 8px 0 0 0;">
|
||||
<i class="bi bi-database-gear"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
@@ -319,12 +323,64 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal fade" id="migrateDialog" tabindex="-1">
|
||||
<div class="modal-dialog modal-lg modal-dialog-centered">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title"><i class="bi bi-database-gear"></i> 数据库结构迁移</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div id="migrateDiffBody"></div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" id="closeMigrateDialogBtn">取消</button>
|
||||
<button type="button" class="btn btn-warning" id="confirmMigrateBtn">确认迁移</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal fade" id="migrateProgress" tabindex="-1" data-bs-backdrop="static" data-bs-keyboard="false">
|
||||
<div class="modal-dialog modal-dialog-centered">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title"><i class="bi bi-arrow-repeat"></i> 迁移执行中</h5>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="progress mb-3" style="height: 20px;">
|
||||
<div class="progress-bar progress-bar-striped progress-bar-animated bg-warning" id="migrateProgressBar" style="width: 0%">0%</div>
|
||||
</div>
|
||||
<p class="text-center text-muted" id="migrateProgressText">正在检测差异...</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal fade" id="migrateResult" tabindex="-1">
|
||||
<div class="modal-dialog modal-lg modal-dialog-centered">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title"><i class="bi bi-check-circle"></i> 迁移结果</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div id="migrateResultBody"></div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-primary" id="closeMigrateResultBtn">关闭</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- i18n国际化支持 -->
|
||||
<script src="/static/lib/i18n/i18n.js"></script>
|
||||
<script src="/static/lib/i18n/zh-CN.js"></script>
|
||||
<script src="/static/lib/i18n/en-US.js"></script>
|
||||
<script src="/static/lib/i18n/de-DE.js"></script>
|
||||
<script src="/static/mds/js/global-ops-controller.js"></script>
|
||||
<script src="/static/mds/js/migrate-controller.js"></script>
|
||||
<script>
|
||||
// 初始化i18n并设置语言
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
@@ -334,6 +390,11 @@
|
||||
i18n.init(savedLang);
|
||||
}
|
||||
|
||||
// 初始化迁移控制器
|
||||
if (typeof MigrateController !== 'undefined') {
|
||||
MigrateController.init();
|
||||
}
|
||||
|
||||
// 语言切换处理
|
||||
const langSelector = document.getElementById('lang-selector');
|
||||
if (langSelector) {
|
||||
|
||||
@@ -0,0 +1,308 @@
|
||||
/**
|
||||
* MDS 数据库迁移控制器
|
||||
* 独立命名空间,不与 GlobalOpsController 冲突
|
||||
*
|
||||
* 安全特性:
|
||||
* 1. 按钮默认隐藏且禁用
|
||||
* 2. 鼠标悬停右下角5秒后显示
|
||||
* 3. 点击前校验数据库权限
|
||||
*/
|
||||
var MigrateController = (function() {
|
||||
var API = {
|
||||
checkPermission: '/api/mds/migrate/check-permission',
|
||||
diff: '/api/mds/migrate/diff',
|
||||
execute: '/api/mds/migrate/execute',
|
||||
versions: '/api/mds/migrate/versions'
|
||||
};
|
||||
|
||||
var HOVER_DELAY = 5000;
|
||||
|
||||
var state = {
|
||||
diffData: null,
|
||||
isExecuting: false,
|
||||
hoverTimer: null,
|
||||
isUnlocked: false
|
||||
};
|
||||
|
||||
var dom = {};
|
||||
|
||||
function cacheDOM() {
|
||||
dom.migrateTriggerZone = document.getElementById('migrateTriggerZone');
|
||||
dom.migrateBtn = document.getElementById('migrateBtn');
|
||||
dom.migrateDialog = document.getElementById('migrateDialog');
|
||||
dom.migrateDiffBody = document.getElementById('migrateDiffBody');
|
||||
dom.migrateProgress = document.getElementById('migrateProgress');
|
||||
dom.migrateProgressBar = document.getElementById('migrateProgressBar');
|
||||
dom.migrateProgressText = document.getElementById('migrateProgressText');
|
||||
dom.migrateResult = document.getElementById('migrateResult');
|
||||
dom.migrateResultBody = document.getElementById('migrateResultBody');
|
||||
dom.confirmMigrateBtn = document.getElementById('confirmMigrateBtn');
|
||||
dom.closeMigrateDialogBtn = document.getElementById('closeMigrateDialogBtn');
|
||||
dom.closeMigrateResultBtn = document.getElementById('closeMigrateResultBtn');
|
||||
}
|
||||
|
||||
function bindEvents() {
|
||||
if (dom.migrateTriggerZone) {
|
||||
dom.migrateTriggerZone.addEventListener('mouseenter', handleMouseEnter);
|
||||
dom.migrateTriggerZone.addEventListener('mouseleave', handleMouseLeave);
|
||||
}
|
||||
|
||||
if (dom.migrateBtn) {
|
||||
dom.migrateBtn.addEventListener('click', handleMigrateClick);
|
||||
}
|
||||
if (dom.confirmMigrateBtn) {
|
||||
dom.confirmMigrateBtn.addEventListener('click', executeMigration);
|
||||
}
|
||||
if (dom.closeMigrateDialogBtn) {
|
||||
dom.closeMigrateDialogBtn.addEventListener('click', function() {
|
||||
var modal = bootstrap.Modal.getInstance(dom.migrateDialog);
|
||||
if (modal) modal.hide();
|
||||
});
|
||||
}
|
||||
if (dom.closeMigrateResultBtn) {
|
||||
dom.closeMigrateResultBtn.addEventListener('click', function() {
|
||||
var modal = bootstrap.Modal.getInstance(dom.migrateResult);
|
||||
if (modal) modal.hide();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function handleMouseEnter() {
|
||||
if (state.hoverTimer) {
|
||||
clearTimeout(state.hoverTimer);
|
||||
}
|
||||
|
||||
state.hoverTimer = setTimeout(function() {
|
||||
showButton();
|
||||
}, HOVER_DELAY);
|
||||
}
|
||||
|
||||
function handleMouseLeave() {
|
||||
if (state.hoverTimer) {
|
||||
clearTimeout(state.hoverTimer);
|
||||
state.hoverTimer = null;
|
||||
}
|
||||
|
||||
hideButton();
|
||||
state.isUnlocked = false;
|
||||
}
|
||||
|
||||
function showButton() {
|
||||
if (dom.migrateBtn) {
|
||||
dom.migrateBtn.disabled = false;
|
||||
dom.migrateBtn.style.opacity = '1';
|
||||
dom.migrateBtn.style.pointerEvents = 'auto';
|
||||
dom.migrateBtn.style.transition = 'opacity 0.3s ease';
|
||||
}
|
||||
}
|
||||
|
||||
function hideButton() {
|
||||
if (dom.migrateBtn) {
|
||||
dom.migrateBtn.disabled = true;
|
||||
dom.migrateBtn.style.opacity = '0';
|
||||
dom.migrateBtn.style.pointerEvents = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
async function checkPermission() {
|
||||
try {
|
||||
var response = await fetch(API.checkPermission);
|
||||
var result = await response.json();
|
||||
|
||||
if (result.success && result.data && result.data.has_permission) {
|
||||
return {
|
||||
hasPermission: true,
|
||||
user: result.data.user,
|
||||
database: result.data.database
|
||||
};
|
||||
} else {
|
||||
var message = result.message || '权限不足';
|
||||
alert('数据库权限校验失败:\n' + message);
|
||||
return { hasPermission: false };
|
||||
}
|
||||
} catch (e) {
|
||||
alert('权限校验请求失败: ' + e.message);
|
||||
return { hasPermission: false };
|
||||
}
|
||||
}
|
||||
|
||||
async function handleMigrateClick() {
|
||||
var permResult = await checkPermission();
|
||||
|
||||
if (!permResult.hasPermission) {
|
||||
return;
|
||||
}
|
||||
|
||||
state.isUnlocked = true;
|
||||
|
||||
dom.migrateBtn.disabled = true;
|
||||
dom.migrateBtn.innerHTML = '<i class="bi bi-arrow-repeat spin-animation"></i>';
|
||||
|
||||
try {
|
||||
var response = await fetch(API.diff);
|
||||
var result = await response.json();
|
||||
|
||||
if (result.success && result.data) {
|
||||
state.diffData = result.data;
|
||||
showConfirmDialog(result.data);
|
||||
} else {
|
||||
alert('差异检测失败: ' + (result.message || '未知错误'));
|
||||
}
|
||||
} catch (e) {
|
||||
alert('请求失败: ' + e.message);
|
||||
} finally {
|
||||
dom.migrateBtn.disabled = false;
|
||||
dom.migrateBtn.innerHTML = '<i class="bi bi-database-gear"></i>';
|
||||
}
|
||||
}
|
||||
|
||||
function showConfirmDialog(data) {
|
||||
var differences = data.differences || [];
|
||||
var totalTables = data.total_tables || 0;
|
||||
var totalFields = data.total_fields || 0;
|
||||
|
||||
if (totalFields === 0) {
|
||||
alert('数据库结构已是最新,无需迁移');
|
||||
return;
|
||||
}
|
||||
|
||||
var html = '<div class="alert alert-warning mb-3">' +
|
||||
'<i class="bi bi-exclamation-triangle-fill me-2"></i>' +
|
||||
'<strong>重要提醒:</strong>执行迁移前请先备份数据库!' +
|
||||
'</div>';
|
||||
|
||||
html += '<div class="mb-3">' +
|
||||
'<span class="badge bg-info me-2">待迁移表: ' + totalTables + '</span>' +
|
||||
'<span class="badge bg-primary">待迁移字段: ' + totalFields + '</span>' +
|
||||
'</div>';
|
||||
|
||||
html += '<div class="table-responsive" style="max-height: 300px; overflow-y: auto;">' +
|
||||
'<table class="table table-sm table-hover">' +
|
||||
'<thead class="table-light sticky-top">' +
|
||||
'<tr><th>表名</th><th>字段名</th><th>数据库字段</th><th>类型</th></tr>' +
|
||||
'</thead><tbody>';
|
||||
|
||||
differences.forEach(function(diff) {
|
||||
html += '<tr>' +
|
||||
'<td><code>' + diff.table + '</code></td>' +
|
||||
'<td>' + diff.field + '</td>' +
|
||||
'<td><code>' + diff.db_field + '</code></td>' +
|
||||
'<td><small>' + diff.sql_type + '</small></td>' +
|
||||
'</tr>';
|
||||
});
|
||||
|
||||
html += '</tbody></table></div>';
|
||||
|
||||
if (dom.migrateDiffBody) {
|
||||
dom.migrateDiffBody.innerHTML = html;
|
||||
}
|
||||
|
||||
var modal = new bootstrap.Modal(dom.migrateDialog);
|
||||
modal.show();
|
||||
}
|
||||
|
||||
async function executeMigration() {
|
||||
if (!state.diffData || !state.diffData.differences || state.diffData.differences.length === 0) {
|
||||
alert('没有待迁移的数据');
|
||||
return;
|
||||
}
|
||||
|
||||
var confirmDialog = bootstrap.Modal.getInstance(dom.migrateDialog);
|
||||
if (confirmDialog) confirmDialog.hide();
|
||||
|
||||
var progressModal = new bootstrap.Modal(dom.migrateProgress);
|
||||
progressModal.show();
|
||||
|
||||
updateProgress(10, '正在执行迁移...');
|
||||
|
||||
try {
|
||||
var response = await fetch(API.execute, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
tables: [],
|
||||
force: false
|
||||
})
|
||||
});
|
||||
|
||||
var result = await response.json();
|
||||
|
||||
updateProgress(100, '迁移完成');
|
||||
|
||||
setTimeout(function() {
|
||||
progressModal.hide();
|
||||
showResultSummary(result);
|
||||
}, 500);
|
||||
|
||||
} catch (e) {
|
||||
updateProgress(100, '迁移失败');
|
||||
setTimeout(function() {
|
||||
progressModal.hide();
|
||||
alert('迁移失败: ' + e.message);
|
||||
}, 500);
|
||||
}
|
||||
}
|
||||
|
||||
function updateProgress(percent, text) {
|
||||
if (dom.migrateProgressBar) {
|
||||
dom.migrateProgressBar.style.width = percent + '%';
|
||||
dom.migrateProgressBar.setAttribute('aria-valuenow', percent);
|
||||
}
|
||||
if (dom.migrateProgressText) {
|
||||
dom.migrateProgressText.textContent = text;
|
||||
}
|
||||
}
|
||||
|
||||
function showResultSummary(result) {
|
||||
var data = result.data || {};
|
||||
var version = data.version || 'N/A';
|
||||
var applied = data.applied_count || 0;
|
||||
var failed = data.failed_count || 0;
|
||||
var changes = data.changes || [];
|
||||
|
||||
var html = '<div class="mb-3">' +
|
||||
'<span class="badge bg-secondary me-2">版本: ' + version + '</span>' +
|
||||
'<span class="badge bg-success me-2">成功: ' + applied + '</span>' +
|
||||
'<span class="badge bg-danger">失败: ' + failed + '</span>' +
|
||||
'</div>';
|
||||
|
||||
if (changes.length > 0) {
|
||||
html += '<div class="table-responsive" style="max-height: 300px; overflow-y: auto;">' +
|
||||
'<table class="table table-sm">' +
|
||||
'<thead class="table-light sticky-top">' +
|
||||
'<tr><th>表名</th><th>字段</th><th>状态</th></tr>' +
|
||||
'</thead><tbody>';
|
||||
|
||||
changes.forEach(function(change) {
|
||||
var statusIcon = change.success
|
||||
? '<i class="bi bi-check-circle text-success"></i>'
|
||||
: '<i class="bi bi-x-circle text-danger"></i>';
|
||||
html += '<tr>' +
|
||||
'<td><code>' + change.table + '</code></td>' +
|
||||
'<td>' + change.db_field + '</td>' +
|
||||
'<td>' + statusIcon + '</td>' +
|
||||
'</tr>';
|
||||
});
|
||||
|
||||
html += '</tbody></table></div>';
|
||||
}
|
||||
|
||||
if (dom.migrateResultBody) {
|
||||
dom.migrateResultBody.innerHTML = html;
|
||||
}
|
||||
|
||||
var resultModal = new bootstrap.Modal(dom.migrateResult);
|
||||
resultModal.show();
|
||||
}
|
||||
|
||||
function init() {
|
||||
cacheDOM();
|
||||
bindEvents();
|
||||
}
|
||||
|
||||
return {
|
||||
init: init
|
||||
};
|
||||
})();
|
||||
Reference in New Issue
Block a user