feat: Metrix 平台按需启用与数据源合并为外部储存
This commit is contained in:
@@ -62,6 +62,17 @@ async def update_remote_config(config: dict[str, Any] = Body(...)):
|
|||||||
return {"success": True, "message": "远程数据配置已更新", "update": state.config.update}
|
return {"success": True, "message": "远程数据配置已更新", "update": state.config.update}
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/api/config/metrix-enabled")
|
||||||
|
async def update_metrix_enabled(enabled: bool = Body(..., embed=True)):
|
||||||
|
state.reload_config()
|
||||||
|
state.config.metrix_enabled = enabled
|
||||||
|
if not enabled:
|
||||||
|
state.config.source_type = "external"
|
||||||
|
state.config.warehouse_type = "mysql"
|
||||||
|
state.config.save()
|
||||||
|
return {"success": True, "message": "已更新", "update": state.config.update}
|
||||||
|
|
||||||
|
|
||||||
@router.post("/api/config/backend")
|
@router.post("/api/config/backend")
|
||||||
async def update_backend(
|
async def update_backend(
|
||||||
source_type: str = Body(...),
|
source_type: str = Body(...),
|
||||||
|
|||||||
+11
-5
@@ -72,8 +72,8 @@ class MySQLConfig:
|
|||||||
).normalized()
|
).normalized()
|
||||||
|
|
||||||
|
|
||||||
# Source backends: direct FTP/SFTP, or Metrix storage platform.
|
# Source backends: external (FTP/SFTP), or Metrix storage platform.
|
||||||
SOURCE_TYPES = ("ftp", "sftp", "metrix")
|
SOURCE_TYPES = ("external", "metrix")
|
||||||
# Warehouse backends: direct MySQL, or Metrix database platform.
|
# Warehouse backends: direct MySQL, or Metrix database platform.
|
||||||
WAREHOUSE_TYPES = ("mysql", "metrix")
|
WAREHOUSE_TYPES = ("mysql", "metrix")
|
||||||
|
|
||||||
@@ -537,8 +537,9 @@ def _normalize_source_type(value: Any, protocol: str = "sftp") -> str:
|
|||||||
text = str(value or "").strip().lower()
|
text = str(value or "").strip().lower()
|
||||||
if text in SOURCE_TYPES:
|
if text in SOURCE_TYPES:
|
||||||
return text
|
return text
|
||||||
# Back-compat: no explicit source type means direct remote, pick its protocol.
|
if text in ("ftp", "sftp"):
|
||||||
return "ftp" if str(protocol).strip().lower() == "ftp" else "sftp"
|
return "external"
|
||||||
|
return "external"
|
||||||
|
|
||||||
|
|
||||||
def _normalize_warehouse_type(value: Any) -> str:
|
def _normalize_warehouse_type(value: Any) -> str:
|
||||||
@@ -549,7 +550,8 @@ def _normalize_warehouse_type(value: Any) -> str:
|
|||||||
@dataclass
|
@dataclass
|
||||||
class AppConfig:
|
class AppConfig:
|
||||||
update: str = ""
|
update: str = ""
|
||||||
source_type: str = "sftp"
|
metrix_enabled: bool = False
|
||||||
|
source_type: str = "external"
|
||||||
warehouse_type: str = "mysql"
|
warehouse_type: str = "mysql"
|
||||||
mysql: MySQLConfig = field(default_factory=MySQLConfig)
|
mysql: MySQLConfig = field(default_factory=MySQLConfig)
|
||||||
metrix: MetrixConfig = field(default_factory=MetrixConfig)
|
metrix: MetrixConfig = field(default_factory=MetrixConfig)
|
||||||
@@ -578,6 +580,7 @@ class AppConfig:
|
|||||||
|
|
||||||
return cls(
|
return cls(
|
||||||
update=data.get("Update", ""),
|
update=data.get("Update", ""),
|
||||||
|
metrix_enabled=bool(data.get("MetrixEnabled", False)),
|
||||||
source_type=_normalize_source_type(data.get("SourceType"), remote_config.protocol),
|
source_type=_normalize_source_type(data.get("SourceType"), remote_config.protocol),
|
||||||
warehouse_type=_normalize_warehouse_type(data.get("WarehouseType")),
|
warehouse_type=_normalize_warehouse_type(data.get("WarehouseType")),
|
||||||
mysql=mysql_config,
|
mysql=mysql_config,
|
||||||
@@ -602,6 +605,7 @@ class AppConfig:
|
|||||||
"""转换为配置文件结构(包含敏感字段,用于保存和下载)"""
|
"""转换为配置文件结构(包含敏感字段,用于保存和下载)"""
|
||||||
return {
|
return {
|
||||||
"Update": self.update,
|
"Update": self.update,
|
||||||
|
"MetrixEnabled": self.metrix_enabled,
|
||||||
"SourceType": self.source_type,
|
"SourceType": self.source_type,
|
||||||
"WarehouseType": self.warehouse_type,
|
"WarehouseType": self.warehouse_type,
|
||||||
"MySQL_DBInfo": self.mysql.normalized().to_dict(include_password=True),
|
"MySQL_DBInfo": self.mysql.normalized().to_dict(include_password=True),
|
||||||
@@ -618,6 +622,7 @@ class AppConfig:
|
|||||||
"""转换为字典(用于返回给前端,隐藏密码)"""
|
"""转换为字典(用于返回给前端,隐藏密码)"""
|
||||||
return {
|
return {
|
||||||
"update": self.update,
|
"update": self.update,
|
||||||
|
"metrix_enabled": self.metrix_enabled,
|
||||||
"source_type": self.source_type,
|
"source_type": self.source_type,
|
||||||
"warehouse_type": self.warehouse_type,
|
"warehouse_type": self.warehouse_type,
|
||||||
"mysql": self.mysql.normalized().to_dict(),
|
"mysql": self.mysql.normalized().to_dict(),
|
||||||
@@ -634,6 +639,7 @@ class AppConfig:
|
|||||||
"""转换为完整字典(包含密码,用于编辑时回显)"""
|
"""转换为完整字典(包含密码,用于编辑时回显)"""
|
||||||
return {
|
return {
|
||||||
"update": self.update,
|
"update": self.update,
|
||||||
|
"metrix_enabled": self.metrix_enabled,
|
||||||
"source_type": self.source_type,
|
"source_type": self.source_type,
|
||||||
"warehouse_type": self.warehouse_type,
|
"warehouse_type": self.warehouse_type,
|
||||||
"mysql": self.mysql.normalized().to_dict(include_password=True),
|
"mysql": self.mysql.normalized().to_dict(include_password=True),
|
||||||
|
|||||||
+10
-1
@@ -877,5 +877,14 @@
|
|||||||
- 新增 `cell_data.py::copy_celldata_tables_to_capacity()`:列出 CellData 库所有表,逐表 `SHOW CREATE TABLE` → 目标库建表 → 分批 `SELECT * / INSERT INTO` 复制数据;两库相同时跳过。
|
- 新增 `cell_data.py::copy_celldata_tables_to_capacity()`:列出 CellData 库所有表,逐表 `SHOW CREATE TABLE` → 目标库建表 → 分批 `SELECT * / INSERT INTO` 复制数据;两库相同时跳过。
|
||||||
- CellData 独立处理(远程 / 本地上传)完成后自动执行 CellData 脚本(不复制表)。
|
- CellData 独立处理(远程 / 本地上传)完成后自动执行 CellData 脚本(不复制表)。
|
||||||
- 容量处理集成(`tasks.py` / `remote.py` 的 `_try_refresh_cell_data`):CellData 数据更新 → 执行 CellData 脚本 → 复制表到容量库 → 后续 ReportScript.sql 可直接引用 CellData 表。
|
- 容量处理集成(`tasks.py` / `remote.py` 的 `_try_refresh_cell_data`):CellData 数据更新 → 执行 CellData 脚本 → 复制表到容量库 → 后续 ReportScript.sql 可直接引用 CellData 表。
|
||||||
- 前端脚本编辑页(`ScriptPanel.vue`)编辑器上方新增 Naive UI segment tabs 切换「报表脚本」和「CellData 脚本」;切换时检查未保存状态、加载对应脚本内容;保存和运行按钮跟随当前脚本类型。
|
- 前端脚本编辑页(`ScriptPanel.vue`)页头下拉按钮切换「容量报表脚本」和「CellData 脚本」;运行按钮文案统一为「运行」,跟随当前脚本类型执行对应库的 SQL。
|
||||||
|
- 验证:`python -m compileall -q app` 通过;`frontend` `npm run build`(vue-tsc)通过;构建产物已清理。
|
||||||
|
|
||||||
|
## 2026-06-25:Metrix 平台模式按需启用 + 数据源合并为外部储存
|
||||||
|
|
||||||
|
- Metrix 平台开关:新增 `AppConfig.metrix_enabled`(默认 `false`),存储为 `Configure.json` 的 `MetrixEnabled`;关闭时 `source_type` 和 `warehouse_type` 自动回落为 `external` / `mysql`。
|
||||||
|
- 开关入口:连点品牌图标 8 次打开的「授权延期」弹窗底部新增「启用 Metrix 平台」开关(`LicenseActivationModal.vue`),通过 `POST /api/config/metrix-enabled` 保存。
|
||||||
|
- 条件显示:设置页「数据源 / 仓库」标签页仅在 `metrix_enabled=true` 时显示;未启用时该页不可见,系统默认走外部储存 + MySQL 直连。
|
||||||
|
- 数据源合并:`source_type` 从 `ftp | sftp | metrix` 改为 `external | metrix`;FTP/SFTP 协议选择保留在远程数据源配置的 `RemoteData.protocol` 中,`external` 统一代表外部储存。旧值 `ftp`/`sftp` 自动规范化为 `external`。
|
||||||
|
- 全局状态:`metrixEnabled` 通过 `composables/metrixEnabled.ts` 共享响应式状态,`AppShell` 登录后和激活弹窗切换时同步更新。
|
||||||
- 验证:`python -m compileall -q app` 通过;`frontend` `npm run build`(vue-tsc)通过;构建产物已清理。
|
- 验证:`python -m compileall -q app` 通过;`frontend` `npm run build`(vue-tsc)通过;构建产物已清理。
|
||||||
|
|||||||
@@ -112,7 +112,7 @@
|
|||||||
</n-layout>
|
</n-layout>
|
||||||
</n-layout>
|
</n-layout>
|
||||||
|
|
||||||
<LicenseActivationModal v-if="licenseModalVisible" v-model:show="licenseModalVisible" />
|
<LicenseActivationModal v-if="licenseModalVisible" v-model:show="licenseModalVisible" @metrix-changed="onMetrixChanged" />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
@@ -131,9 +131,10 @@ import {
|
|||||||
SunnyOutline
|
SunnyOutline
|
||||||
} from '@vicons/ionicons5';
|
} from '@vicons/ionicons5';
|
||||||
|
|
||||||
import { apiPost, clearToken, getToken, setToken, setUnauthorizedHandler } from './api/client';
|
import { apiGet, apiPost, clearToken, getToken, setToken, setUnauthorizedHandler } from './api/client';
|
||||||
import type { LoginResponse } from './types';
|
import type { AppConfig, LoginResponse } from './types';
|
||||||
import { pageHeader, resetPageHeader, resolveHeaderValue, type PageHeaderAction } from './composables/pageHeader';
|
import { pageHeader, resetPageHeader, resolveHeaderValue, type PageHeaderAction } from './composables/pageHeader';
|
||||||
|
import { metrixEnabled } from './composables/metrixEnabled';
|
||||||
import { themeName, toggleAppTheme } from './composables/theme';
|
import { themeName, toggleAppTheme } from './composables/theme';
|
||||||
|
|
||||||
const LoginView = defineAsyncComponent(() => import('./components/LoginView.vue'));
|
const LoginView = defineAsyncComponent(() => import('./components/LoginView.vue'));
|
||||||
@@ -172,6 +173,9 @@ const themeToggleTitle = computed(() => (themeName.value === 'dark' ? '切换到
|
|||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
window.addEventListener('dragover', preventWindowFileDrop, { capture: true });
|
window.addEventListener('dragover', preventWindowFileDrop, { capture: true });
|
||||||
window.addEventListener('drop', preventWindowFileDrop, { capture: true });
|
window.addEventListener('drop', preventWindowFileDrop, { capture: true });
|
||||||
|
if (token.value) {
|
||||||
|
void loadMetrixEnabled();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
@@ -198,6 +202,7 @@ async function handleLogin(payload: { username: string; password: string }) {
|
|||||||
setToken(result.token);
|
setToken(result.token);
|
||||||
token.value = result.token;
|
token.value = result.token;
|
||||||
message.success('登录成功');
|
message.success('登录成功');
|
||||||
|
void loadMetrixEnabled();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
message.error(error instanceof Error ? error.message : '登录失败');
|
message.error(error instanceof Error ? error.message : '登录失败');
|
||||||
} finally {
|
} finally {
|
||||||
@@ -226,6 +231,19 @@ function handleMenuChange(key: string | number) {
|
|||||||
void router.push({ name: key });
|
void router.push({ name: key });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function onMetrixChanged(enabled: boolean) {
|
||||||
|
metrixEnabled.value = enabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadMetrixEnabled() {
|
||||||
|
try {
|
||||||
|
const config = await apiGet<AppConfig>('/api/config/full');
|
||||||
|
metrixEnabled.value = config.metrix_enabled ?? false;
|
||||||
|
} catch {
|
||||||
|
metrixEnabled.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function handleBrandMarkClick() {
|
function handleBrandMarkClick() {
|
||||||
brandClickCount.value += 1;
|
brandClickCount.value += 1;
|
||||||
if (brandClickResetTimer !== undefined) {
|
if (brandClickResetTimer !== undefined) {
|
||||||
|
|||||||
@@ -16,6 +16,10 @@
|
|||||||
placeholder="请输入激活码"
|
placeholder="请输入激活码"
|
||||||
:disabled="activationLoading"
|
:disabled="activationLoading"
|
||||||
/>
|
/>
|
||||||
|
<div class="license-metrix-toggle">
|
||||||
|
<n-switch v-model:value="metrixEnabled" :loading="savingMetrix" size="small" @update:value="toggleMetrix" />
|
||||||
|
<span class="license-metrix-label">启用 Metrix 平台</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<div class="license-dialog-footer">
|
<div class="license-dialog-footer">
|
||||||
@@ -31,17 +35,21 @@ import { onMounted, ref } from 'vue';
|
|||||||
import { useMessage } from 'naive-ui';
|
import { useMessage } from 'naive-ui';
|
||||||
|
|
||||||
import { apiGet, apiPost } from '../api/client';
|
import { apiGet, apiPost } from '../api/client';
|
||||||
import type { LicenseStatus } from '../types';
|
import type { AppConfig, LicenseStatus } from '../types';
|
||||||
|
|
||||||
const visible = defineModel<boolean>('show', { required: true });
|
const visible = defineModel<boolean>('show', { required: true });
|
||||||
|
const emit = defineEmits<{ metrixChanged: [enabled: boolean] }>();
|
||||||
const message = useMessage();
|
const message = useMessage();
|
||||||
const activationCode = ref('');
|
const activationCode = ref('');
|
||||||
const activationLoading = ref(false);
|
const activationLoading = ref(false);
|
||||||
const activationKeyLabel = ref('2026/12/30');
|
const activationKeyLabel = ref('2026/12/30');
|
||||||
const licenseMessage = ref('输入激活码可将授权到期日期延长 30 天。');
|
const licenseMessage = ref('输入激活码可将授权到期日期延长 30 天。');
|
||||||
|
const metrixEnabled = ref(false);
|
||||||
|
const savingMetrix = ref(false);
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
void loadLicenseStatus();
|
void loadLicenseStatus();
|
||||||
|
void loadMetrixEnabled();
|
||||||
});
|
});
|
||||||
|
|
||||||
async function loadLicenseStatus() {
|
async function loadLicenseStatus() {
|
||||||
@@ -55,6 +63,29 @@ async function loadLicenseStatus() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function loadMetrixEnabled() {
|
||||||
|
try {
|
||||||
|
const config = await apiGet<AppConfig>('/api/config/full');
|
||||||
|
metrixEnabled.value = config.metrix_enabled ?? false;
|
||||||
|
} catch {
|
||||||
|
metrixEnabled.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function toggleMetrix(enabled: boolean) {
|
||||||
|
savingMetrix.value = true;
|
||||||
|
try {
|
||||||
|
await apiPost('/api/config/metrix-enabled', { enabled });
|
||||||
|
metrixEnabled.value = enabled;
|
||||||
|
emit('metrixChanged', enabled);
|
||||||
|
} catch (error) {
|
||||||
|
metrixEnabled.value = !enabled;
|
||||||
|
message.error(error instanceof Error ? error.message : '保存失败');
|
||||||
|
} finally {
|
||||||
|
savingMetrix.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function submitActivation() {
|
async function submitActivation() {
|
||||||
const code = activationCode.value.trim();
|
const code = activationCode.value.trim();
|
||||||
if (!code) {
|
if (!code) {
|
||||||
@@ -76,3 +107,18 @@ async function submitActivation() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.license-metrix-toggle {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
margin-top: 16px;
|
||||||
|
padding-top: 12px;
|
||||||
|
border-top: 1px solid var(--n-border-color, #e0e0e6);
|
||||||
|
}
|
||||||
|
.license-metrix-label {
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--n-text-color-3, #999);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -4,16 +4,15 @@
|
|||||||
|
|
||||||
<n-card size="small" class="work-card settings-tabs-card">
|
<n-card size="small" class="work-card settings-tabs-card">
|
||||||
<n-tabs type="line" animated class="settings-tabs">
|
<n-tabs type="line" animated class="settings-tabs">
|
||||||
<n-tab-pane name="backend" tab="数据源 / 仓库">
|
<n-tab-pane v-if="metrixEnabledRef" name="backend" tab="数据源 / 仓库">
|
||||||
<div class="settings-database-panel">
|
<div class="settings-database-panel">
|
||||||
<div class="settings-database-stack">
|
<div class="settings-database-stack">
|
||||||
<n-card title="后端类型" size="small" class="work-card">
|
<n-card title="后端类型" size="small" class="work-card">
|
||||||
<n-form label-placement="top">
|
<n-form label-placement="top">
|
||||||
<n-form-item label="数据源">
|
<n-form-item label="数据源">
|
||||||
<n-radio-group v-model:value="sourceType">
|
<n-radio-group v-model:value="sourceType">
|
||||||
<n-radio-button value="sftp">SFTP</n-radio-button>
|
<n-radio-button value="external">外部储存</n-radio-button>
|
||||||
<n-radio-button value="ftp">FTP</n-radio-button>
|
<n-radio-button value="metrix">Metrix 储存平台</n-radio-button>
|
||||||
<n-radio-button value="metrix">Metrix 存储平台</n-radio-button>
|
|
||||||
</n-radio-group>
|
</n-radio-group>
|
||||||
</n-form-item>
|
</n-form-item>
|
||||||
<n-form-item label="数据仓库">
|
<n-form-item label="数据仓库">
|
||||||
@@ -22,7 +21,7 @@
|
|||||||
<n-radio-button value="metrix">Metrix 数据库平台</n-radio-button>
|
<n-radio-button value="metrix">Metrix 数据库平台</n-radio-button>
|
||||||
</n-radio-group>
|
</n-radio-group>
|
||||||
</n-form-item>
|
</n-form-item>
|
||||||
<p class="form-hint">主数据源和数据仓库可独立选择;CellData 单独配置。</p>
|
<p class="form-hint">数据源和数据仓库可独立选择;CellData 单独配置。</p>
|
||||||
</n-form>
|
</n-form>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<n-space justify="end">
|
<n-space justify="end">
|
||||||
@@ -819,6 +818,7 @@ import type {
|
|||||||
RemoteSchedulerStatus
|
RemoteSchedulerStatus
|
||||||
} from '../types';
|
} from '../types';
|
||||||
import { showDownloadCompleteDialog } from '../composables/downloadFeedback';
|
import { showDownloadCompleteDialog } from '../composables/downloadFeedback';
|
||||||
|
import { metrixEnabled as metrixEnabledRef } from '../composables/metrixEnabled';
|
||||||
import { resetPageHeader, setPageHeader } from '../composables/pageHeader';
|
import { resetPageHeader, setPageHeader } from '../composables/pageHeader';
|
||||||
|
|
||||||
interface ExtractFieldConfig {
|
interface ExtractFieldConfig {
|
||||||
@@ -882,7 +882,7 @@ const savingCellDataSettings = ref(false);
|
|||||||
const validatingCellDataSettings = ref(false);
|
const validatingCellDataSettings = ref(false);
|
||||||
const savingBackend = ref(false);
|
const savingBackend = ref(false);
|
||||||
const savingMetrix = ref(false);
|
const savingMetrix = ref(false);
|
||||||
const sourceType = ref<'ftp' | 'sftp' | 'metrix'>('sftp');
|
const sourceType = ref<'external' | 'metrix'>('external');
|
||||||
const warehouseType = ref<'mysql' | 'metrix'>('mysql');
|
const warehouseType = ref<'mysql' | 'metrix'>('mysql');
|
||||||
const savingDirectoryMappings = ref(false);
|
const savingDirectoryMappings = ref(false);
|
||||||
const savingHistoryRetention = ref(false);
|
const savingHistoryRetention = ref(false);
|
||||||
@@ -1120,7 +1120,7 @@ async function loadConfig() {
|
|||||||
try {
|
try {
|
||||||
const config = await apiGet<AppConfig>('/api/config/full');
|
const config = await apiGet<AppConfig>('/api/config/full');
|
||||||
configUpdate.value = config.update;
|
configUpdate.value = config.update;
|
||||||
sourceType.value = config.source_type || 'sftp';
|
sourceType.value = (config.source_type === 'metrix' ? 'metrix' : 'external') as 'external' | 'metrix';
|
||||||
warehouseType.value = config.warehouse_type || 'mysql';
|
warehouseType.value = config.warehouse_type || 'mysql';
|
||||||
if (config.metrix) {
|
if (config.metrix) {
|
||||||
metrixForm.base_url = config.metrix.base_url || '';
|
metrixForm.base_url = config.metrix.base_url || '';
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
import { ref } from 'vue';
|
||||||
|
|
||||||
|
export const metrixEnabled = ref(false);
|
||||||
@@ -96,7 +96,8 @@ export interface CacheSize {
|
|||||||
|
|
||||||
export interface AppConfig {
|
export interface AppConfig {
|
||||||
update: string;
|
update: string;
|
||||||
source_type: 'ftp' | 'sftp' | 'metrix';
|
metrix_enabled: boolean;
|
||||||
|
source_type: 'external' | 'metrix';
|
||||||
warehouse_type: 'mysql' | 'metrix';
|
warehouse_type: 'mysql' | 'metrix';
|
||||||
mysql: {
|
mysql: {
|
||||||
host: string;
|
host: string;
|
||||||
|
|||||||
Reference in New Issue
Block a user