feat: 添加数据库迁移功能并新增worker字段

1. 新增数据库结构迁移的后端API与前端UI交互功能
2. 为机台模具关联模型新增worker人员字段
3. 优化建表脚本为可重入的增量更新模式
4. 移除旧的业务规则校验函数
This commit is contained in:
2026-05-26 14:48:12 +08:00
parent f0a036d3c6
commit 6d512f0a59
8 changed files with 504 additions and 83 deletions
+1
View File
@@ -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,
}
+4
View File
@@ -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)
-55
View File
@@ -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结构完整性校验钩子
+1
View File
@@ -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:
+1
View File
@@ -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)
+125 -25
View File
@@ -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
View File
@@ -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) {
+308
View File
@@ -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
};
})();