diff --git a/src/common/network/ai.ts b/src/common/network/ai.ts new file mode 100644 index 000000000..8dea0c238 --- /dev/null +++ b/src/common/network/ai.ts @@ -0,0 +1,56 @@ +/* + * Copyright 2025 OceanBase + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import request from '@/util/request'; + +/** + * AI功能状态响应接口 + */ +export interface IAIStatusResponse { + /** AI功能是否启用 */ + enabled: boolean; + /** AI功能是否可用 */ + available: boolean; + /** 使用的AI模型 */ + model: string; + /** API基础URL */ + baseUrl: string; + /** API密钥是否已配置 */ + apiKeyConfigured: boolean; +} + +/** + * 查询AI功能状态 + * @returns AI功能状态信息 + */ +export async function getAIStatus(): Promise { + const res = await request.get('/api/v2/ai/status'); + return res?.data; +} + +/** + * 检查AI功能是否可用 + * @returns 是否可用 + */ +export async function isAIAvailable(): Promise { + try { + const status = await getAIStatus(); + return status.enabled && status.available; + } catch (error) { + console.warn('Failed to check AI status:', error); + return false; + } +} diff --git a/src/common/network/sensitiveColumn.ts b/src/common/network/sensitiveColumn.ts index 920afc300..0e59a18d2 100644 --- a/src/common/network/sensitiveColumn.ts +++ b/src/common/network/sensitiveColumn.ts @@ -28,6 +28,7 @@ export async function startScanning( databaseIds: number[]; allSensitiveRules: boolean; sensitiveRuleIds: number[]; + scanningMode?: string; }, ): Promise { const ret = await request.post( @@ -41,6 +42,70 @@ export async function startScanning( return ret?.data?.taskId; } +// 新增单表扫描API +export async function scanSingleTableAsync( + projectId: number, + params: { + databaseId: number; + tableName: string; + scanningMode?: string; + }, +): Promise { + // 导入login store来检查organizationId + const login = require('@/store/login').default; + console.log('发起单表扫描请求:', { projectId, params }); + console.log('当前organizationId:', login?.organizationId); + console.log('当前用户信息:', login?.user); + + try { + const ret = await request.post( + `/api/v2/collaboration/projects/${projectId}/sensitiveColumns/scanSingleTableAsync`, + { + data: { + ...params, + }, + params: { + currentOrganizationId: login?.organizationId, + }, + }, + ); + console.log('单表扫描请求响应:', ret); + // 后端直接返回taskId在data字段中,而不是data.taskId + const taskId = ret?.data?.taskId || ret?.data; + if (!taskId) { + console.error('单表扫描请求返回的taskId为空:', ret); + } + return taskId; + } catch (error) { + console.error('单表扫描请求失败:', error); + throw error; + } +} + +// 获取单表扫描结果 +export async function getSingleTableScanResult(projectId: number, taskId: string): Promise { + // 导入login store来检查organizationId + const login = require('@/store/login').default; + console.log('获取单表扫描结果:', { projectId, taskId }); + console.log('当前organizationId:', login?.organizationId); + + try { + const ret = await request.get( + `/api/v2/collaboration/projects/${projectId}/sensitiveColumns/singleTableScan/${taskId}/result`, + { + params: { + currentOrganizationId: login?.organizationId, + }, + }, + ); + console.log('单表扫描结果响应:', ret); + return ret?.data; + } catch (error) { + console.error('获取单表扫描结果失败:', error); + throw error; + } +} + export async function setEnabled( projectId: number, id: number, @@ -95,6 +160,7 @@ export enum ScannResultType { RUNNING = 'RUNNING', SUCCESS = 'SUCCESS', FAILED = 'FAILED', + CANCELLED = 'CANCELLED', } export interface IScannResult { taskId: string; @@ -110,9 +176,20 @@ export interface IScannResult { export async function getScanningResults(projectId: number, taskId: string): Promise { const ret = await request.get( - `/api/v2/collaboration/projects/${projectId}/sensitiveColumns/getScanningResults?taskId=${encodeURIComponent( - taskId, - )}`, + `/api/v2/collaboration/projects/${projectId}/sensitiveColumns/getScanningResults`, + { + params: { taskId }, + }, + ); + return ret?.data; +} + +export async function stopScanning(projectId: number, taskId: string): Promise { + const ret = await request.post( + `/api/v2/collaboration/projects/${projectId}/sensitiveColumns/stopScanning`, + { + params: { taskId }, + }, ); return ret?.data; } diff --git a/src/component/SensitiveColumnIndicator/index.less b/src/component/SensitiveColumnIndicator/index.less new file mode 100644 index 000000000..0d77dc28e --- /dev/null +++ b/src/component/SensitiveColumnIndicator/index.less @@ -0,0 +1,96 @@ +.indicator { + display: inline-flex; + align-items: center; + cursor: pointer; + padding: 2px 6px; + border-radius: 3px; + transition: all 0.15s ease; + font-size: 12px; + + &:hover { + background-color: rgba(0, 0, 0, 0.04); + transform: none; + } +} + +.tooltipContent { + max-width: 300px; + background: #fff; + color: #000; + border-radius: 6px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); + border: 1px solid #e8e8e8; + padding: 12px; + + .tooltipTitle { + font-weight: 500; + margin-bottom: 8px; + color: #000; + } + + .tooltipItem { + margin-bottom: 4px; + color: #333; + + &:last-child { + margin-bottom: 0; + } + } + + .tooltipDetails { + margin-top: 12px; + padding-top: 8px; + border-top: 1px solid #e8e8e8; + + .tooltipSubtitle { + font-weight: 500; + margin-bottom: 6px; + color: #333; + font-size: 12px; + } + + .tooltipDetailItem { + margin-bottom: 4px; + font-size: 12px; + color: #666; + + &:last-child { + margin-bottom: 0; + } + } + } +} + +.summary { + margin-top: 8px; + padding-top: 8px; + border-top: 1px solid #f0f0f0; + font-size: 12px; + color: #666; + text-align: center; +} + +.scanInfo { + margin-top: 4px; + font-size: 11px; + color: #999; + font-style: italic; +} + +// Tooltip 全局样式覆盖 +:global(.ant-tooltip-inner) { + .tooltipContent { + background: #fff !important; + border-radius: 6px !important; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15) !important; + border: 1px solid #e8e8e8 !important; + padding: 12px !important; + } +} + +// 响应式调整 +@media (max-width: 768px) { + .tooltipContent { + max-width: 250px; + } +} diff --git a/src/component/SensitiveColumnIndicator/index.tsx b/src/component/SensitiveColumnIndicator/index.tsx new file mode 100644 index 000000000..2cc0ae842 --- /dev/null +++ b/src/component/SensitiveColumnIndicator/index.tsx @@ -0,0 +1,407 @@ +/** + * 敏感列标识组件 + * 用于在表结构查看和SQL执行结果中显示敏感列信息 + */ +import React, { useState, useEffect } from 'react'; +import { Badge, Tooltip, Space, Typography, Button, Tag } from 'antd'; +import { EyeInvisibleOutlined, SecurityScanOutlined, SyncOutlined } from '@ant-design/icons'; +import { formatMessage } from '@/util/intl'; +import sensitiveColumnScanner, { IScanResult } from '@/service/sensitiveColumnScanner'; +import { ISensitiveColumnInfo } from '@/d.ts/sensitiveColumn'; +import { isAIAvailable } from '@/common/network/ai'; +import styles from './index.less'; + +interface ISensitiveColumnIndicatorProps { + /** 敏感列信息列表 - 如果提供则直接使用,否则从扫描服务获取 */ + sensitiveColumns?: ISensitiveColumnInfo[]; + /** 是否显示详细信息 */ + showDetails?: boolean; + /** 组件大小 */ + size?: 'small' | 'default' | 'large'; + /** 自定义样式类名 */ + className?: string; + /** 自定义样式 */ + style?: React.CSSProperties; + /** 表名 - 用于触发扫描 */ + tableName?: string; + /** 数据库名 - 用于触发扫描 */ + databaseName?: string; + /** 会话ID - 用于触发扫描 */ + sessionId?: string; + /** 列信息 - 用于扫描 */ + columns?: Array<{ columnName?: string; name?: string; columnType?: string; typeName?: string }>; + /** 触发来源 */ + triggerSource?: 'TABLE_VIEW' | 'SQL_RESULT'; + /** 是否自动触发扫描 */ + autoScan?: boolean; +} + +// 敏感等级配置 +const SENSITIVITY_CONFIG = { + HIGH: { + color: '#ff4d4f', + text: '高敏感', + icon: , + }, + MEDIUM: { + color: '#faad14', + text: '中敏感', + icon: , + }, + LOW: { + color: '#52c41a', + text: '低敏感', + icon: , + }, +}; + +const SensitiveColumnIndicator: React.FC = ({ + sensitiveColumns: propSensitiveColumns, + showDetails = false, + size = 'default', + className, + style, + tableName, + databaseName, + sessionId, + columns, + triggerSource = 'TABLE_VIEW', + autoScan = false, +}) => { + const [scanResult, setScanResult] = useState(null); + const [isScanning, setIsScanning] = useState(false); + const [scanError, setScanError] = useState(null); + const [retryCount, setRetryCount] = useState(0); + const [isRetrying, setIsRetrying] = useState(false); + const [aiAvailable, setAiAvailable] = useState(true); + + // 检查AI功能状态 + useEffect(() => { + const checkAIStatus = async () => { + try { + const available = await isAIAvailable(); + setAiAvailable(available); + } catch (error) { + console.warn('检查AI状态失败:', error); + setAiAvailable(false); + } + }; + checkAIStatus(); + }, []); + + // 使用传入的敏感列数据或扫描结果 + const sensitiveColumns = propSensitiveColumns || scanResult?.sensitiveColumns || []; + + // 触发扫描 + const triggerScan = async (forceRescan = false) => { + if (!tableName || !databaseName || !sessionId) { + console.warn('缺少扫描参数:', { tableName, databaseName, sessionId }); + setScanError('缺少必要的扫描参数'); + return; + } + + // 如果是强制重新扫描,清除缓存 + if (forceRescan) { + sensitiveColumnScanner.clearCache(tableName, databaseName); + } + + // 如果是重试,设置重试状态 + if (scanError) { + setIsRetrying(true); + setRetryCount((prev) => prev + 1); + } else { + setRetryCount(0); + } + + setIsScanning(true); + setScanError(null); // 清除之前的错误 + try { + // 格式化列数据为 IServerTableColumn 类型 + const formattedColumns = + columns?.map((column, index) => ({ + name: column.name || column.columnName || '', + typeName: column.typeName || column.columnType || 'VARCHAR', + fullTypeName: column.typeName || column.columnType || 'VARCHAR', + schemaName: databaseName, + tableName: tableName, + ordinalPosition: index + 1, + nullable: true, + autoIncrement: false, + defaultValue: '', + comment: '', + virtual: false, + scale: 0, + precision: 0, + typeModifiers: [], + maxLength: 0, + charsetName: '', + collationName: '', + genExpression: '', + unsigned: false, + zerofill: false, + enumValues: [], + stored: false, + onUpdateCurrentTimestamp: false, + extraInfo: '', + charUsed: 'BYTE' as const, + hidden: false, + warning: '', + keyType: undefined, + secondPrecision: undefined, + dayPrecision: undefined, + yearPrecision: undefined, + })) || []; + + const result = await sensitiveColumnScanner.scanSensitiveColumns({ + tableName, + databaseName, + sessionId, + columns: formattedColumns, + triggerSource, + }); + + if (result) { + setScanResult(result); + setScanError(null); + } else { + setScanError('扫描失败,请稍后重试'); + } + } catch (error) { + console.error('扫描失败:', error); + setScanError(error?.message || '扫描过程中发生错误'); + } finally { + setIsScanning(false); + setIsRetrying(false); + } + }; + + // 自动扫描 + useEffect(() => { + if (autoScan && tableName && databaseName && sessionId && !propSensitiveColumns) { + // 检查是否有缓存结果 + const cached = sensitiveColumnScanner.getCachedResult(tableName, databaseName); + if (cached) { + setScanResult(cached); + } else { + triggerScan(); + } + } + }, [autoScan, tableName, databaseName, sessionId, propSensitiveColumns]); + + // 监听缓存清除事件 + useEffect(() => { + if (!tableName || !databaseName || !autoScan) { + return; + } + + const handleCacheChange = (clearedTableName: string, clearedDatabaseName: string) => { + // 如果清除的是当前表的缓存,触发重新扫描 + if (clearedTableName === tableName && clearedDatabaseName === databaseName) { + console.log(`检测到表 ${tableName} 缓存被清除,触发重新扫描`); + // 清除当前扫描结果,触发重新扫描 + setScanResult(null); + setScanError(null); + if (sessionId && !propSensitiveColumns) { + triggerScan(); + } + } + }; + + // 添加监听器 + sensitiveColumnScanner.addCacheChangeListener(handleCacheChange); + + // 清理函数 + return () => { + sensitiveColumnScanner.removeCacheChangeListener(handleCacheChange); + }; + }, [tableName, databaseName, sessionId, autoScan, propSensitiveColumns]); + + // 如果外部已提供敏感列数据,直接显示,不受AI状态影响 + if (propSensitiveColumns && propSensitiveColumns.length > 0) { + // 外部提供的数据,直接渲染,跳过AI状态检查 + } else if (!aiAvailable) { + // 只有在需要扫描且AI未开启时才隐藏组件 + return null; + } + + // 如果正在扫描,显示扫描状态 + if (isScanning) { + return ( + + ); + } + + // 如果有扫描错误,显示错误状态 + if (scanError) { + const retryText = retryCount > 0 ? `重试(${retryCount})` : '重试'; + const tooltipText = + retryCount > 2 + ? `${scanError} - 已重试${retryCount}次,请检查网络或稍后再试` + : `${scanError} - 点击${retryText}`; + + return ( + + ⚠️} + onClick={!isScanning && !isRetrying ? () => triggerScan(true) : undefined} + className={className} + style={{ ...style, cursor: !isScanning && !isRetrying ? 'pointer' : 'default' }} + > + {isRetrying ? '重试中...' : `扫描失败 - ${retryText}`} + + + ); + } + + // 如果没有敏感列且可以触发扫描,显示扫描按钮 + if (!sensitiveColumns || sensitiveColumns.length === 0) { + if (tableName && databaseName && sessionId) { + // 如果是自动扫描且已经扫描过但没有结果,显示"无敏感列"状态 + if (autoScan && scanResult) { + return ( + } className={className} style={style}> + {formatMessage({ + id: 'odc.component.SensitiveColumnIndicator.NoSensitiveColumns', + defaultMessage: '无敏感列', + })} + + ); + } + // 手动扫描模式显示扫描按钮 + if (!autoScan) { + return ( + + ); + } + } + return null; + } + + // 按敏感等级分组统计 + const levelCounts = sensitiveColumns.reduce((acc, col) => { + acc[col.sensitivityLevel] = (acc[col.sensitivityLevel] || 0) + 1; + return acc; + }, {} as Record); + + // 生成提示内容 + const tooltipContent = ( +
+
+ + {formatMessage({ + id: 'component.SensitiveColumnIndicator.title', + defaultMessage: '检测到敏感列', + })} + {tableName && databaseName && sessionId && ( + + )} + +
+ {Object.entries(levelCounts).map(([level, count]) => { + const config = SENSITIVITY_CONFIG[level]; + return ( +
+ + {config.icon} + + {config.text}:{count} 列 + + +
+ ); + })} + {showDetails && ( +
+
+ {formatMessage({ + id: 'component.SensitiveColumnIndicator.details', + defaultMessage: '详细信息:', + })} +
+ {sensitiveColumns.map((col, index) => { + const config = SENSITIVITY_CONFIG[col.sensitivityLevel]; + return ( +
+ + {config.icon} + {col.columnName} + ({config.text}) + +
+ ); + })} +
+ )} +
+ ); + + const totalCount = sensitiveColumns.length; + const hasHighSensitive = levelCounts.HIGH > 0; + const badgeColor = hasHighSensitive ? '#ff4d4f' : '#faad14'; + + return ( + + } + className={className} + style={style} + > + {totalCount}{' '} + {formatMessage({ + id: 'component.SensitiveColumnIndicator.label', + defaultMessage: '敏感列', + })} + + + ); +}; + +export default SensitiveColumnIndicator; +export type { ISensitiveColumnInfo, ISensitiveColumnIndicatorProps }; diff --git a/src/d.ts/sensitiveColumn.ts b/src/d.ts/sensitiveColumn.ts index 3c90e7612..ac9c63444 100644 --- a/src/d.ts/sensitiveColumn.ts +++ b/src/d.ts/sensitiveColumn.ts @@ -42,3 +42,11 @@ export interface ISensitiveColumn { updateTime: number; organizationId: number; } + +// 敏感列信息接口,用于扫描结果 +export interface ISensitiveColumnInfo { + columnName: string; + sensitivityLevel: 'HIGH' | 'MEDIUM' | 'LOW'; + reason: string; + confidence: number; +} diff --git a/src/d.ts/sensitiveRule.ts b/src/d.ts/sensitiveRule.ts index 0a2af5ea4..fe36a443a 100644 --- a/src/d.ts/sensitiveRule.ts +++ b/src/d.ts/sensitiveRule.ts @@ -20,6 +20,7 @@ export enum SensitiveRuleType { PATH = 'PATH', REGEX = 'REGEX', GROOVY = 'GROOVY', + AI = 'AI', } export interface ISensitiveRule { id?: number; @@ -42,4 +43,7 @@ export interface ISensitiveRule { createTime: number; updateTime: number; organizationId: number; + // AI识别相关字段 + aiSensitiveTypes?: string[]; + aiCustomPrompt?: string; } diff --git a/src/locales/must/strings/en-US.json b/src/locales/must/strings/en-US.json index 6c94ce8d6..bce5ac3df 100644 --- a/src/locales/must/strings/en-US.json +++ b/src/locales/must/strings/en-US.json @@ -141,6 +141,7 @@ "odc.ImportDrawer.ImportForm.BatchSubmissionQuantity": "Batch Commit Quantity", "workspace.window.sql.explain.detail.failed": "Failed to view SQL execution details.", "odc.component.SnippetForm.SyntaxDescription": "Snippet Description", + "odc.components.ImportDrawer.NoMappingRelationshipSelected": "The mapping relationship is not selected", "odc.helper.page.openPage.CreateSynonym": "Create Synonym", "odc.components.SynonymPage.Created": "Created At:", "odc.src.d.ts.Function": "Function", @@ -244,6 +245,7 @@ "workspace.window.createSequence.params.cached.no": "No Cache", "workspace.window.createPackage.modal.title": "Create Package", "odc.components.TaskManagePage.TerminatedSuccessfully": "Aborted", + "odc.components.ImportDrawer.DuplicateMappingRelationships": "The mapping relationship is duplicate.", "odc.components.PackagePage.LastModifiedTime": "Last Modified At", "odc.CreateTriggerPage.component.AdvancedInfoFrom.SelectAtLeastOneEvent": "Select at least one event", "workspace.window.function.propstab.params": "Parameter", @@ -552,6 +554,7 @@ "odc.src.store.sql.Compile": "Compile", "odc.ImportDrawer.ImportForm.ClickOrDragTheFile": "Click or drag the file here to upload", "workspace.window.createTable.partition.value.listColumns.placelholder": "(Field 1, Field 2),(Field 1, Field 2)", + "workspace.window.sql.limit.placeholder": "1,000", "workspace.window.recyclebin.button.purgeAll": "Clear", "odc.component.GrammerHelpSider.SnippetslacksInTotal": "{snippetsLength} entries in total", "odc.helper.page.openPage.CreateATrigger": "Create Trigger", @@ -578,6 +581,7 @@ "odc.ImportDrawer.csvMapping.TargetFieldType": "Target Field Type", "odc.components.PLPage.SavedSuccessfully": "Saved.", "odc.TreeNodeMenu.config.view.ViewViewProperties": "Check View Properties", + "odc.components.ImportDrawer.SelectTheFieldsToBe": "Select the field to be mapped", "odc.components.PLPage.statusBar.ManualTermination": "Manually Abort", "odc.components.TypePage.ModificationTime": "Modified At:", "odc.component.CreateTypeModal.Cancel": "Cancel", @@ -1370,6 +1374,7 @@ "odc.component.CommonTaskDetailModal.TaskFlow.Handler": "Operator", "odc.component.helpDoc.doc.TheApprovalTimesOutAnd": "The approval has timed out. The task will expire.", "odc.component.DataTransferModal.ExecutionMethod": "Execution Mode", + "odc.TaskManagePage.component.TaskTools.InitiateAgain": "Initiate Again", "odc.component.CommonTaskDetailModal.TaskFlow.Handled": "Available Operator", "odc.components.FormRecordExportModal.Cancel": "Cancel", "odc.component.OSSDragger2.Cancel": "Cancel", @@ -1544,6 +1549,7 @@ "odc.component.CommonTaskDetailModal.TaskFlow.InitiateATask": "Initiate Task", "odc.components.FormRecordExportModal.Export": "Export", "odc.TaskManagePage.component.TaskTools.Reject": "Reject", + "odc.TaskManagePage.component.TaskTools.Terminate": "Stop", "odc.components.RecordPage.component.DetailedRules": "Operation Rules", "odc.component.DetailModal.dataMocker.RiskLevel": "Risk Level", "odc.component.TaskStatus.ApprovalExpired": "The approval has expired.", @@ -1991,6 +1997,7 @@ "odc.components.CreateShadowSyncModal.Submit": "Submit", "odc.StructConfigPanel.StructAnalysisResult.column.ExecutionResult": "Execution Result", "odc.component.CommonTaskDetailModal.TaskRecord.View": "View", + "odc.components.PartitionDrawer.Remarks": "Remarks", "odc.component.AddConnectionForm.AddressItems.ClusterName": "Cluster", "odc.AddConnectionForm.Account.PrivateAccount.TheEndOfTheAccount": "The account contains spaces at the beginning and end.", "odc.components.SQLPage.BecauseTheSqlIsToo": "Since SQL is too long, the editor will only support preview", @@ -2227,6 +2234,7 @@ "odc.Content.ListItem.TenantTenant": "Tenant: {tenant}", "odc.component.Crontab.const.Saturday": "Saturday", "odc.components.CreateSQLPlanTaskModal.Save": "Save", + "odc.TaskManagePage.component.TaskTools.View": "View", "odc.components.CreateSQLPlanTaskModal.CreateAnSqlPlan": "Create SQL Plan", "odc.components.RolePage.component.ResourceManagementPermissions": "Resource Management Permissions", "odc.src.d.ts.TableGroup": "Table Group", @@ -2331,7 +2339,7 @@ "odc.component.Login.RegisterForm.ConfirmPasswordInconsistency": "The passwords you entered do not match.", "odc.component.Login.RegisterForm.TheUsernameMustBeTo": "The username can contain 4 to 48 characters in length.", "odc.component.AskEventTrackingModal.Login": "Log On", - "odc.component.SQLConfig.ObtainTheColumnInformationOf": "Obtain Result Set Column Information", + "odc.component.SQLConfig.ObtainTheColumnInformationOf": "Obtain Column Information of Result Set", "odc.component.Login.RegisterForm.TheUsernameCannotBeEmpty": "The username is required.", "odc.component.AskEventTrackingModal.InformationCollectionList": "Information Collection List", "odc.component.Login.ActivateForm.Activate": "Activate", @@ -2795,6 +2803,7 @@ "odc.Project.User.DeletedSuccessfully": "Deleted.", "odc.SensitiveRule.components.CheckboxInput.PleaseEnter": "Enter a value", "odc.Secure.Approval.ExecutionValidityPeriod": "Execution Validity Period", + "odc.AlterDdlTask.CreateModal.LockTableTimeout": "Table Locking Timeout Period", "odc.NewSSODrawerButton.SSOForm.ConfigurationName": "Configuration Name", "odc.Datasource.Info.Delete": "Delete", "odc.components.SensitiveColumn.AreYouSureYouWant.1": "Are you sure that you want to delete the sensitive column?", @@ -2827,6 +2836,7 @@ "odc.NewSSODrawerButton.SSOForm.Type": "Type", "odc.component.FormModal.TitleName": "{title} Name", "odc.ResourceTree.Nodes.package.Subprogram": "Subprogram", + "odc.AlterDdlTask.CreateModal.Seconds": "Seconds", "odc.Project.Database.CharacterEncoding": "Character Set", "odc.page.Secure.RiskLevel": "Risk Level", "odc.DataArchiveTask.DetailContent.TaskType": "Task Type", @@ -2861,6 +2871,7 @@ "odc.AlterDdlTask.DetailContent.SourceTableCleanupPolicyAfter": "Cleanup Strategy for Source Table Upon Completion", "odc.SensitiveRule.components.DetectWay.Script": "Script", "odc.AlterDdlTask.CreateModal.AreYouSureYouWant": "Are you sure that you want to cancel the online schema change?", + "odc.AlterDdlTask.DetailContent.LockTableTimeout": "Table Locking Timeout Period", "odc.Project.Setting.ProjectInformation": "Project Information", "odc.Project.User.AreYouSureYouWant": "Are you sure that you want to delete this member?", "odc.SensitiveRule.components.FormSensitiveRuleDrawer.AreYouSureYouWant.1": "Are you sure that you want to cancel the creation?", @@ -3193,6 +3204,7 @@ "odc.AsyncTask.DetailContent.RollbackContent": "Rollback Content", "odc.NewSSODrawerButton.SSOForm.ObtainTheAccessTokenAddress": "The address provided by the authorization server for obtaining an access token.", "odc.ExternalIntegration.SSO.WhetherToEnable": "Enabled", + "odc.AlterDdlTask.CreateModal.NumberOfFailedRetries": "Retry Attempts on Failure", "odc.Env.components.InnerEnvironment.ConfigurationValue": "Current Value", "odc.Script.Snippet.Copy": "Copy", "odc.DataArchiveTask.CreateModal.Ok": "OK", @@ -3258,6 +3270,7 @@ "odc.Database.AddDataBaseButton.BoundProject": "- Bound Project:", "odc.component.EditPLParamsModal.StatementCannotBeEmpty": "Specify a statement.", "odc.component.FormModal.TheProcessNameCannotExceed": "The process name can be up to 128 characters in length.", + "odc.component.helpDoc.doc.AfterTheTableLockTime": "If the switching is not completed within the table locking period, the switching is automatically retried.", "odc.SensitiveRule.components.FormSensitiveRuleDrawer.UpdateFailed": "Update failed.", "odc.ExternalIntegration.SqlInterceptor.FollowTheSecuritySpecificationSql": "Follow the security specification-SQL development specification-SQL window rule path to use the configured and enabled SQL interception integration", "odc.Env.components.EditRuleDrawer.ExecutionIsProhibitedAndApproval": "Execution prohibited, unable to initiate approval", @@ -3265,6 +3278,7 @@ "odc.component.CommonDetailModal.TaskExecuteRecord.DatabaseChanges": "Database changes", "odc.components.RecordPage.column.EnterADataSource": "Please enter a data source", "odc.components.SQLResultSet.DBTimeline.SqlPostCheck": "SQL post check", + "odc.component.VersionModal.config.ThroughDataArchivingYouCan": "With data archiving, you can configure flexible archiving conditions to separate hot and cold data.", "odc.Sider.SpaceSelect.Ok": "OK", "odc.Record.RecordPage.column.DataSource": "Data Source", "odc.Record.RecordPage.interface.TransferProject": "Transfer Items", @@ -3308,6 +3322,7 @@ "odc.components.SQLResultSet.DBTimeline.SqlPrecheck": "SQL PreCheck", "odc.component.DescriptionInput.EnterADescriptionLessThan": "Please enter a description within 200 words; If not entered, the system will automatically generate description information according to the object and work order type.", "odc.DataArchiveTask.CreateModal.ExecutionTime": "Execution time", + "odc.component.VersionModal.config.OdcProvidesDatabaseObjectManagement": "ODC provides database object management, data import and export, SQL editing and execution, PL\nCompilation and debugging, data generation, execution analysis, database operation and maintenance and other tool capabilities.", "odc.Env.components.EditRuleDrawer.AllowExecution": "Permit Execution", "odc.ResourceTree.Datasource.DeletedSuccessfully": "Deleted successfully", "odc.DataArchiveTask.CreateModal.VariableConfig.DeleteAVariable": "Delete Variable", @@ -3756,14 +3771,17 @@ "odc.src.component.Task.component.CommonDetailModal.Nodes.HighRisk": "High Risk", "odc.src.page.Workspace.components.SQLResultSet.TheCurrentSQLExistenceMust": "The current SQL statement has items that require revision. Please make the necessary changes before executing.", "odc.src.component.Task.AlterDdlTask.CreateModal.LockUsers.1": "Lock User", + "odc.src.component.Task.AlterDdlTask.CreateModal.1BeforePerformingThe": "1. Before performing the online schema change, ensure that the database server has sufficient disk space.", "odc.src.component.Task.DataClearTask.CreateModal.PleaseEnter": "Please enter", "odc.src.component.Task.component.CommonDetailModal.Nodes.TheNumberOf": ". Pre-check processing exceeds the maximum limit of SQL statements. The current workflow will proceed based on the ", "odc.src.page.Project.Project.CreateProject.SecurityAdministrator": "Security Administrator", "odc.src.component.Task.component.CommonDetailModal.Share": "Share", "odc.src.page.Workspace.components.SQLResultSet.SQLContentHasBeenModified": "The SQL statement has been modified, and the original problematic line cannot be located. Please re-execute the SQL statement or initiate a pre-check.", "odc.src.component.Task.PartitionTask.DetailContent.Partition": "Partitioning Plan", + "odc.src.component.Task.DataArchiveTask.CreateModal.PleaseEnter": "Please enter", "odc.src.component.Task.PartitionTask.DetailContent.RiskLevel": "Risk Level", "odc.src.component.Task.component.CommonDetailModal.Nodes.GradeContinuesToAdvance": " level.", + "odc.src.component.Task.DataArchiveTask.CreateModal.PleaseChoose.1": "Please select", "odc.src.component.Task.PartitionTask.DetailContent.Remark": "Remarks", "odc.src.page.Workspace.components.SQLResultSet.TheCurrentSQLNeedsApproval": "The current SQL statement requires approval. Initiate the approval process or make the necessary modifications before execution.", "odc.src.component.Task.PartitionTask.DetailContent.CreationTime": "Created At", @@ -3772,7 +3790,10 @@ "odc.src.component.Task.component.CommonDetailModal.Replication": "Copied successfully", "odc.src.component.Task.DataClearTask.CreateModal.PleaseChoose": "Please select", "odc.src.component.Task.DataClearTask.CreateModal.PleaseChoose.1": "Please select", + "odc.src.component.Task.DataArchiveTask.CreateModal.PleaseChoose": "Please select", "odc.src.component.Task.NoCurrentWorkOrderView": "No permission to view the current ticket", + "odc.src.component.Task.AlterDdlTask.CreateModal.2WhenCreatingA": "2. When creating a ticket and selecting the source table cleanup strategy, it is recommended to retain the source table.", + "odc.src.component.Task.AlterDdlTask.CreateModal.3IfTheOB": "3. Before switching the table name, the specified database account will be locked, and the session corresponding to that account will be closed. During the table name switch, the locked account will prevent the application from accessing the database, so please do not execute this during peak business hours;", "src.component.Task.component.PartitionPolicyTable.F057FAAF": "Sequential Increment", "src.component.Task.StructureComparisonTask.DetailContent.E8DAF6BA": "Actions", "src.page.Datasource.Datasource.NewDatasourceDrawer.Form.Account.445C8BBC": "Default", @@ -3783,6 +3804,7 @@ "src.component.Task.ApplyDatabasePermission.CreateModal.B0247EF7": "Please enter the reason for the application.", "src.page.Project.User.ManageModal.B7377F46": "Once revoked, the operation cannot be undone.", "src.component.Task.StructureComparisonTask.CreateModal.A8C717F6": "Cancel", + "src.component.helpDoc.AEEC5916": "Regarding the third point of the considerations, specifying the account to be locked is to ensure data consistency during table name switching while minimizing the impact on the business as much as possible. Please ensure the accuracy of the specified account. If you do not specify any account, ODC will not perform any operations to lock accounts or kill sessions, and you will need to ensure data consistency during the switch.", "src.component.Task.ApplyDatabasePermission.DetailContent.2C812515": "Created By", "src.component.EditorToolBar.actions.3BDAC881": "Execute Current Statement", "src.component.Task.ApplyDatabasePermission.DetailContent.265A918A": "Reason for Application", @@ -3857,9 +3879,10 @@ "src.component.Task.ApplyDatabasePermission.DetailContent.AFAA55EA": "Ticket Type", "src.component.Task.ApplyDatabasePermission.CreateModal.B35BDC54": "Are you sure you want to cancel applying for database permissions?", "src.page.ExternalIntegration.SSO.NewSSODrawerButton.SSOForm.C5D23829": "Test login successful", - "src.component.ODCSetting.config.56F5CB81": "Delimiter", + "src.component.ODCSetting.config.56F5CB81": "Delimiter Settings", "src.component.Task.component.PartitionPolicyFormTable.7D6F23AE": "Number of Reserved Partitions", "src.page.Workspace.components.SessionContextWrap.SessionSelect.38EA55F4": "Project: ", + "src.component.Task.DataClearTask.DetailContent.D2882643": "Yes", "src.page.ExternalIntegration.SSO.NewSSODrawerButton.SSOForm.DBB95C01": "Please enter the server.", "src.page.Project.Notification.components.F80DF1C7": "Lark", "src.component.Task.StructureComparisonTask.DetailContent.3C7B0B00": "Target Database", @@ -4009,6 +4032,7 @@ "src.component.Task.0B2B1D60": "Manual Execution", "src.page.Project.Notification.components.DFE43F9E": "Request Method", "src.component.helpDoc.EFADD11A": "Able to manage sensitive columns in the project in addition to the privileges of a participant", + "src.component.Task.StructureComparisonTask.CreateModal.52828286": "Description", "src.page.Project.User.ManageModal.UserAuthList.C3B2211E": "Please enter", "src.page.ExternalIntegration.SSO.NewSSODrawerButton.SSOForm.DC978DE4": "Password", "src.component.Task.ApplyDatabasePermission.CreateModal.28506030": "Reason for Application", @@ -4021,6 +4045,7 @@ "src.component.Task.component.PartitionPolicyTable.8BC770B0": "Number of Partitions to be Created", "src.component.Task.StructureComparisonTask.CreateModal.B4FAB9EC": "Failed to create", "src.component.ODCSetting.647A18AA": "Are you sure you want to reset settings to default?", + "src.component.Task.DataClearTask.CreateModal.23542D89": "Please select", "src.page.Project.Notification.components.BA80DE5A": "Cancel", "src.component.ODCSetting.config.80241964": "Normal", "src.component.Task.component.PartitionPolicyFormTable.DF75EB9E": "How to configure?", @@ -4051,6 +4076,7 @@ "src.page.Project.Notification.components.713896D2": "Up to", "src.component.Task.StructureComparisonTask.CreateModal.45DB3909": "Create Schema Comparison", "src.component.Task.PartitionTask.CreateModal.E454F701": "Ignore Error and Continue", + "src.component.Task.StructureComparisonTask.CreateModal.67E284BD": "The description cannot exceed 200 characters", "src.page.Project.Notification.components.A50DD7D6": "Test notification sent successfully", "src.component.Task.9B79BD20": "Auto Execution", "src.page.ExternalIntegration.SSO.NewSSODrawerButton.SSOForm.2E3212A5": "Starting point or base point used when searching for groups during group queries.", @@ -4060,6 +4086,7 @@ "src.page.ExternalIntegration.SSO.NewSSODrawerButton.SSOForm.80683796": "Filter condition for user queries, allowing you to filter search results based on user attributes such as username, email, department, etc.", "src.component.Task.DataMockerTask.CreateModal.2C3DF5A5": "Create Mock Data Ticket", "src.page.Project.User.ManageModal.UserAuthList.19A27247": "Expired On", + "src.component.Task.DataClearTask.DetailContent.834E7D89": "No", "src.component.Task.component.PartitionPolicyTable.D94AE82A": "Creation Method", "src.page.Secure.Env.components.FF5B44FE": "Edit Environment", "src.page.Login.components.LDAPModal.AA5AB5DA": "The LDAP password cannot be empty.", @@ -4195,7 +4222,7 @@ "src.page.Datasource.Datasource.NewDatasourceDrawer.Form.BDA4C2AB": "Database", "src.component.ODCSetting.995A8948": "Cancel", "src.page.ExternalIntegration.SSO.NewSSODrawerButton.SSOForm.405C3356": "Please enter", - "src.component.ODCSetting.config.F785B55E": "Obtain Result Set Column Information", + "src.component.ODCSetting.config.F785B55E": "Fetch Result Set Column Information", "src.component.Task.component.PartitionPolicyTable.17F930BB": "Partitioning Strategy Details", "src.component.Task.ApplyDatabasePermission.CreateModal.5401D61D": "Please enter the reason for the application", "src.component.Task.component.PartitionPolicyTable.274DB973": "Enabled", @@ -4208,6 +4235,7 @@ "src.component.Task.AsyncTask.CreateModal.6EEFAEA6": "Create Database Change Ticket", "src.page.Project.Notification.components.95A710C5": "Request Method", "src.component.ODCSetting.config.1ACE7366": "The parameter modified will take effect after restarting ODC.", + "src.component.Task.PartitionTask.CreateModal.026392ED": "The description cannot exceed 200 characters, if left blank, a description will be automatically generated based on the object and ticket type", "src.page.Project.User.ManageModal.UserAuthList.583E307F": "Revoke", "src.page.Secure.Env.65EAAB75": "Are you sure you want to delete this environment?", "src.page.Login.components.LDAPModal.95DA8BD0": "LDAP Login", @@ -4228,6 +4256,7 @@ "src.page.Project.User.ManageModal.UserAuthList.8E0CB3F5": "Database", "src.page.Secure.Env.48529F6E": "All Environments", "src.component.Task.PartitionTask.CreateModal.0A49F493": "Hours", + "src.component.Task.DataClearTask.CreateModal.99D8FCD6": "Use Primary Key for Cleanup", "src.page.Project.Notification.components.F40AD99E": "Webhook URL", "src.page.ExternalIntegration.SSO.NewSSODrawerButton.SSOForm.5E8157A1": "Times", "src.component.Task.component.PartitionPolicyTable.5C3A7795": "Type", @@ -4297,6 +4326,7 @@ "src.component.Task.StructureComparisonTask.DetailContent.575F8B39": "Comparison Scope", "src.component.Task.ApplyDatabasePermission.CreateModal.87E335B0": "1 Year", "src.component.Task.component.PartitionPolicyFormTable.A9C95E9D": "Deleting partitions for tables with global indexes may result in index invalidation. If you choose to rebuild the global indexes, it may take a long time. Please proceed with caution.", + "src.component.Task.DataClearTask.DetailContent.2D1A14AB": "Use Primary Key for Cleanup", "src.component.Task.component.TaskTable.80E1D16A": "Schema Comparison", "src.page.Project.Notification.components.CA33D8AB": "The channel name already exists.", "src.page.Project.Notification.components.EA00E8BD": "Please select a channel type.", @@ -4868,6 +4898,7 @@ "src.component.SQLConfig.1A5CCA98": "SQL Execution", "src.page.Project.User.ManageModal.Table.TaskApplyList.EC52BFBC": "Expired On", "src.component.Task.ApplyTablePermission.CreateModal.E26609A0": "Export", + "src.component.Task.ApplyTablePermission.CreateModal.BC4488C7": "Never Expires", "src.component.Task.ApplyTablePermission.CreateModal.78B63403": "Please select", "src.component.Task.ApplyTablePermission.CreateModal.8955ACFE": "No projects available, please first ", "src.component.Task.ApplyTablePermission.CreateModal.77D8F632": "Please select a project.", @@ -4932,11 +4963,16 @@ "src.page.Workspace.SideBar.ResourceTree.DatabaseSearchModal.components.5DDBC7F0": "All Search Results", "src.page.Project.User.ManageModal.Table.TaskApplyList.47C16695": "Database", "src.page.Project.User.ManageModal.Database.BC592E63": "Access", - "src.component.SQLConfig.1D15916D": "Delimiter", + "src.component.SQLConfig.1D15916D": "Delimiter Settings", "src.page.Workspace.SideBar.ResourceTree.DatabaseSearchModal.components.FA5E6855": "Locate Database \"{databaseName}\"", "src.page.Workspace.components.DDLResultSet.6477DD60": "Aggregate SQL execution details, physical execution plans, and multidimensional views of end-to-end trace diagnostics to quickly pinpoint the root causes of slow query executions.", "src.component.ExecuteSqlDetailModal.1882C007": "Plan Statistics", + "src.component.Task.component.ShardingStrategyItem.E5A6B481": "Full table scan", + "src.component.Task.component.ShardingStrategyItem.F91EEC6C": "Condition Matching", + "src.component.Task.component.ShardingStrategyItem.3BD95C1A": "Search strategy", "src.component.Task.component.ShardingStrategyItem.D5F45B7A": "Please select a search strategy.", + "src.component.Task.DataClearTask.DetailContent.E977DA21": "Search strategy", + "src.component.Task.DataArchiveTask.DetailContent.4844C10F": "Search strategy", "src.page.Workspace.components.TablePage.ShowTableBaseInfoForm.56FAF24C": "Logic Table Name", "src.page.Workspace.components.TablePage.ShowTableBaseInfoForm.C1C0A345": "Logical Table Expression", "src.page.Workspace.components.TablePage.ShowTableBaseInfoForm.200BCB34": "Table Topology", @@ -5615,6 +5651,7 @@ "src.page.Console.9B6E647E": "Feedback", "src.page.Console.470D66DD": "Understanding real-time SQL diagnostics for OceanBase AP", "src.page.Console.67004EA1": "ODC SQL check for automatic identification of high-risk operations", + "src.page.Console.4F744942": "Integration with ODC enterprise-level account system", "src.page.Console.15BABD7A": "Enterprise-level control and collaboration: Safeguard every database change", "src.page.Console.A0F9F190": "Manage and modify sharded databases and tables via ODC", "src.page.Console.7BD8C2BB": "Data masking and control practices", @@ -5674,6 +5711,7 @@ "src.component.Task.component.PartitionPolicyFormTable.E0F368F6": "Numeric", "src.component.Task.component.PartitionPolicyFormTable.053D6705": "Datetime", "src.component.Task.component.PartitionPolicyFormTable.FCD94271": "Timestamp", + "src.component.Task.component.PartitionPolicyFormTable.9F024A60": "The current number of pre-created partitions is too small. If the scheduling fails, business operations may be affected. It is recommended to adjust it to 2 or more.", "src.component.Task.component.PartitionPolicyFormTable.E753A67D": "The creation strategy has not been set up yet. No SQL can be previewed.", "src.component.Task.component.PartitionPolicyFormTable.18397BFF": "The method of generating partition names", "src.component.Task.component.PartitionPolicyFormTable.044700A2": "The generation rule for the partition name, which can reference variables. For example: concat('P_', ${COL1}), where COL1 indicates the partition key of the partitioned table.", @@ -5733,6 +5771,7 @@ "src.component.ODCSetting.B7CA4CA1": "Enter the default number of entries per query", "src.component.ODCSetting.5127321B": "Do not exceed {sessionQueryLimit}.", "src.component.ODCSetting.A8E22CC2": "Do not exceed {queryLimit}.", + "src.component.ODCSetting.4C8CC366": "Do not exceed 100000.", "src.component.ODCSetting.DD41DED4": "Enter the maximum number of entries per query", "src.component.ODCSetting.6BCFD6DD": "Users", "src.component.ODCSetting.47586FD4": "Personal Workspaces", @@ -5755,7 +5794,7 @@ "src.component.ODCSetting.config.group.B3BD15DD": "Automatically Generate Backup Rollback Plan for Database Changes", "src.component.ODCSetting.config.group.75980064": "Yes", "src.component.ODCSetting.config.group.228F4926": "No", - "src.component.ODCSetting.config.group.0ACF68C5": "Task Description Prompt", + "src.component.ODCSetting.config.group.0ACF68C5": "Job Description Prompt", "src.component.ODCSetting.config.group.8EBEF215": "SQL Query", "src.component.ODCSetting.config.group.5F1DCFE2": "Maximum Entries per Query", "src.component.ODCSetting.config.group.7B82C09C": "Default Entries per Query", @@ -5797,14 +5836,21 @@ "src.component.Task.component.PartitionPolicyFormTable.RuleFormItem.13B22678": "yyyy-MM-dd HH:mm:ss", "src.component.Task.LogicDatabaseAsyncTask.CreateModal.C53810BF": "Comment parsing is currently unsupported for logical database changes. Adding comments may cause SQL statement parsing exceptions.", "src.component.CommonTable.2363A9C8": "Display Periodic Execution Tasks Only", + "src.page.Workspace.components.DDLResultSet.Sync.38BDFD2B": "View Refresh Records", "src.page.Auth.Role.component.FormModal.3D1B7861": "The currently granted permissions are overlapping. Only the records with larger permission scopes are retained.", + "src.d.ts.25D43093": "Data source does not exist", + "src.d.ts.CAC52ADB": "Instance does not exist", "src.d.ts.B89ABE6D": "Type mismatch", + "src.d.ts.7AE88D0A": "Imported", "src.component.Task.component.TaskTable.D4FAED98": "Import {activeTaskLabel}", "src.component.Task.component.TaskTable.55FC08BB": "Importing", "src.component.Task.component.PartitionPolicyFormTable.B988E243": "The current number of pre-created partitions is too small. If the scheduling fails, business operations may be affected. It is recommended to adjust it to 2 or more.", "src.component.Task.component.ImportModal.997B6AC7": "Scheduled Task Import Completed", + "src.component.Task.component.ImportModal.E256F212": "{resultLength} job(s) imported successfully. It is recommended to manually ", "src.component.Task.component.ImportModal.8AD83BC8": "Configure Message Notification", "src.component.Task.component.ImportModal.04E1F63D": "for the tasks to ensure that task exceptions can be found in time.", + "src.component.Task.component.ImportModal.E12ACFFB": "Scheduled Task Export Completed", + "src.component.Task.component.ImportModal.47DA5967": "{successCount} job(s) imported successfully.", "src.component.Task.component.ImportModal.32698C46": "{failedCount} job(s) failed to import. It is recommended to manually ", "src.component.Task.component.ImportModal.801362FE": "Configure Message Notification", "src.component.Task.component.ImportModal.60663407": "for the tasks to ensure that task exceptions can be found in time. For export details, ", @@ -5822,6 +5868,8 @@ "src.component.Task.component.ImportModal.B6BCF0C0": "Cancel", "src.component.Task.component.ImportModal.2F42ECBC": "Next: Preview", "src.component.Task.component.ImportModal.9FFF1F71": "Previous: Upload File", + "src.component.Task.component.ImportModal.81026A36": "Import", + "src.component.Task.component.ImportModal.0C3395D0": "Only configuration files exported by Alibaba Cloud OceanBase R&D are supported for import.\nPlease confirm that the instance associated with the job has been switched to the current project in OceanBase Cloud. The system will match or create the corresponding data source based on the instance information.", "src.component.Task.component.ImportModal.D8DFA444": "Please upload", "src.component.Task.component.ImportModal.7B17FBF4": "File upload failed due to {fileResponseDataErrMsg}", "src.component.Task.component.ImportModal.3CEE142A": "File Key", @@ -5830,14 +5878,30 @@ "src.component.Task.component.ImportModal.C6AC4C4C": "Please enter", "src.component.Task.component.ImportModal.889B80AD": "Import Project", "src.component.Task.component.ImportModal.30D6697A": "Please select", + "src.component.Task.component.ImportModal.B3686BE9": "Please confirm that the relevant data sources have been added to the project.", "src.component.Task.component.ImportModal.1E47B6CE": "Create Data Source", "src.component.Task.component.ImportModal.E82DB6D1": "Please select", "src.component.Task.component.ImportModal.68180A60": "Create Project", "src.component.Task.component.ImportModal.A8BC98CA": "Data Source", "src.component.Task.component.ImportModal.0B1F9DCD": "Database", "src.component.Task.component.ImportModal.263EFA83": "Type", + "src.component.Task.component.ImportModal.40440087": "Cloud Vendor", + "src.component.Task.component.ImportModal.F2C95E45": "Region", + "src.component.Task.component.ImportModal.EFE4E0D0": "Connection Information", + "src.component.Task.component.ImportModal.4A911A47": "Instance Name", + "src.component.Task.component.ImportModal.396EF2AD": "Tenant Name", "src.component.Task.component.ImportModal.5928CAE8": "Database Account", + "src.component.Task.component.ImportModal.B4E8467F": "Source Database", + "src.component.Task.component.ImportModal.50582618": "Source Database", + "src.component.Task.component.ImportModal.76F572C8": "Target Database", + "src.component.Task.component.ImportModal.776397D6": "Target Database", + "src.component.Task.component.ImportModal.B5C62BDC": "Ticket Description", + "src.component.Task.component.ImportModal.1EB75659": "Database", + "src.component.Task.component.ImportModal.8D781AC2": "Database", + "src.component.Task.component.ImportModal.5DE578E8": "Original No.", + "src.component.Task.component.ImportModal.2043ADA9": "Original Project", "src.component.Task.component.ImportModal.1BF95006": "No jobs to import", + "src.component.Task.component.ImportModal.E8DE787E": "Importable", "src.component.Task.component.AsyncTaskOperationButton.D9D9A882": "Please select a ticket first.", "src.component.Task.component.AsyncTaskOperationButton.683E8B65": "Cancel", "src.component.Task.component.AsyncTaskOperationButton.2D7C7380": "I acknowledge the relevant business risks.", @@ -5906,7 +5970,18 @@ "src.component.Task.component.AsyncTaskOperationButton.89CE28F7": "Termination error occurred", "src.component.Task.component.AsyncTaskOperationButton.08EE66D0": "Terminate Task", "src.component.Task.component.AsyncTaskOperationButton.5E6481EB": "{idsLength} task(s) are being terminated...", - "src.page.Workspace.components.DDLResultSet.Sync.38BDFD2B": "View Refresh Records", + "src.component.Task.AlterDdlTask.CreateModal.A37F2729": "Before switching the table name, lock the original table to prohibit writes. Please ensure that your database meets the following conditions:\n 1. OceanBase Version ≥ 4.2.5 and < 4.3.0.\n 2. The enable_lock_priority parameter has been set to true.", + "src.component.Task.AlterDdlTask.CreateModal.8AC211D6": "Before switching the table name, lock the original table to prohibit writes. Please ensure that your database meets the following conditions: \n 1. OceanBase Version ≥ 4.2.5 and < 4.3.0, and the enable_lock_priority parameter has been set to true.\n 2. ODP Version ≥ 4.3.1 and the following parameter settings have been made: \n alter proxyconfig set proxy_id=1;\n alter proxyconfig set client_session_id_version=2;\n alter proxyconfig set enable_single_leader_node_routing = false;", + "src.component.Task.AlterDdlTask.CreateModal.D1732875": "Note: Please ensure that the database server has sufficient disk space before performing the online schema change.", + "src.component.Task.AlterDdlTask.CreateModal.05BCC428": "Write Prohibition during Table Name Switch ", + "src.component.Task.AlterDdlTask.CreateModal.B0CFE0E1": "Before switching the table name, the system will lock the specified database account and terminate the session of the account. During the switch, all applications involving the account will not be able to access the database. It is recommended to avoid performing this operation during peak business hours to minimize the impact on your business.", + "src.component.Task.AlterDdlTask.CreateModal.D056FD5E": "Lock User", + "src.component.Task.AlterDdlTask.CreateModal.EA0D82E0": "Lock Table", + "src.component.Task.AlterDdlTask.CreateModal.125DF508": "Recommended", + "src.component.ODCSetting.2BE8F5C3": "Do not exceed {maxResultsetRows}", + "src.component.Task.AlterDdlTask.CreateModal.44A394CE": "Before switching the table name, lock the original table to prohibit writes. ", + "src.component.Task.AlterDdlTask.CreateModal.6DC73E39": "Before switching the table name, lock the original table to prevent writing. Please ensure that the ODP meets the following conditions: \nODP version ≥ 4.3.1 and the following parameter settings have been made: \n alter proxyconfig set proxy_id=1; \n alter proxyconfig set client_session_id_version=2; \n alter proxyconfig set enable_single_leader_node_routing = false; ", + "src.component.Task.AlterDdlTask.CreateModal.C62A7DBB": "Parameter enable_lock_priority needs to be set to true", "src.component.Task.component.ActionBar.797981FE": "No results", "src.component.Task.component.ActionBar.5218D741": "Query Results", "src.component.ODCSetting.9651E837": "Do not exceed {maxResultsetRows}", @@ -5919,71 +5994,8 @@ "src.component.ODCSetting.config.AB482BC7": "Support Viewing Query Results", "src.component.ODCSetting.config.3FE1F2A7": "Support Downloading Query Results", "src.component.ODCSetting.config.1ED38C41": "Query Results of Database Changes", - "src.component.Task.AlterDdlTask.CreateModal.D1732875": "Note: Please ensure that the database server has sufficient disk space before performing the online schema change.", - "src.component.Task.AlterDdlTask.CreateModal.05BCC428": "Write Prohibition during Table Name Switch ", - "src.component.Task.AlterDdlTask.CreateModal.B0CFE0E1": "Before switching the table name, the system will lock the specified database account and terminate the session of the account. During the switch, all applications involving the account will not be able to access the database. It is recommended to avoid performing this operation during peak business hours to minimize the impact on your business.", - "src.component.Task.AlterDdlTask.CreateModal.D056FD5E": "Lock User", - "src.component.Task.AlterDdlTask.CreateModal.EA0D82E0": "Lock Table", - "src.component.Task.AlterDdlTask.CreateModal.125DF508": "Recommended", - "src.component.Task.AlterDdlTask.CreateModal.44A394CE": "Before switching the table name, lock the original table to prohibit writes. ", - "src.component.Task.AlterDdlTask.CreateModal.6DC73E39": "Before switching the table name, lock the original table to prevent writing. Please ensure that the ODP meets the following conditions: \nODP version ≥ 4.3.1 and the following parameter settings have been made: \n alter proxyconfig set proxy_id=1; \n alter proxyconfig set client_session_id_version=2; \n alter proxyconfig set enable_single_leader_node_routing = false; ", - "src.component.Task.AlterDdlTask.CreateModal.C62A7DBB": "Parameter enable_lock_priority needs to be set to true", "src.page.Console.8F636FA5": "Explore ODC team workspaces", "src.component.helpDoc.7F2FDC2E": "Number of automatic retry attempts on lock failure", "src.component.Task.AlterDdlTask.CreateModal.1A8336C1": "Retry Attempts on Lock Failure", - "src.component.Task.AlterDdlTask.CreateModal.A56878AD": "Enter the number of retry attempts on lock failure", - "src.page.Workspace.components.SessionContextWrap.SessionSelect.SessionDropdown.49B355DA": "Manage Database", - "src.page.Console.components.RecentlyDatabase.806E294C": "Object storage does not currently support opening the SQL console.", - "src.d.ts.BE8F2539": "Already exists", - "src.component.Task.component.TaskTable.E20D58A0": "Personal space tasks can only be managed and viewed by you, and do not support alert configuration. Therefore, it is not recommended to use them in scenarios that heavily rely on production operations. If you need to use this feature in production, please prioritize using team space and set up the appropriate notification mechanism to ensure smooth business operations.", - "src.component.Task.component.ImportModal.A6892359": "(Contains {importedCount} imported tasks)", - "src.component.Task.component.ImportModal.AF106FF6": "{resultLength} jobs imported successfully {importedDescription}.", - "src.component.Task.component.ImportModal.97A1A7A3": "{resultLength} jobs imported successfully{importedDescription}.", - "src.component.Task.component.ImportModal.1B653B27": "It is recommended to manually complete the task.", - "src.component.Task.component.ImportModal.92D087FB": "(Contains {importedCount} imported tasks)", - "src.component.Task.component.ImportModal.94BFD01C": "{successCount} assignments imported successfully, {importedDescription}, {failedCount} assignments failed to import.", - "src.component.Task.component.ImportModal.AE88E265": "0 jobs imported successfully, 1,", - "src.component.Task.component.ImportModal.7C3EB72C": "Ticket", - "src.component.Task.component.ImportModal.DCD1BA1A": "Original database", - "src.component.Task.component.ImportModal.93C2FD0F": "Original database", - "src.component.Task.component.ImportModal.A8B5EF34": "Data Source: ", - "src.component.Task.component.ImportModal.4C0466BB": "Original database", - "src.component.Task.component.ImportModal.E78A5A7E": "Source Database", - "src.component.Task.component.ImportModal.47F8A3BA": "Target Database", - "src.component.Task.component.ImportModal.55E87B63": "Original state", - "src.component.Task.component.ImportModal.DFCD4ABB": "After successfully importing the work order, it will be re-enabled.", - "src.component.Task.component.ImportModal.37A63CCA": "Type", - "src.component.Task.component.ImportModal.D22149A7": "New Source Endpoint", - "src.component.Task.component.ImportModal.6992AD58": "New Target Endpoint", - "src.component.Task.component.ImportModal.01BA659E": "New database", - "src.component.Task.component.ImportModal.EA6397CD": "I have confirmed that the imported work order objects in the new and old databases are consistent.", - "src.component.Task.component.ImportModal.E756EF6A": "Import ({selectedRowKeysLength})", - "src.component.Task.component.ImportModal.A28A2A00": "Only configuration files exported from Alibaba Cloud OceanBase Data Development or ODC are supported. Before importing, please add the relevant data source and specify the corresponding project.", - "src.component.Task.component.ImportModal.C264F2F5": "Select the work orders you want to import, and they will be reactivated after import. Before importing, please check that the objects in the old and new databases are consistent; otherwise, the import or execution may fail.", - "src.component.Task.component.ImportModal.3137AA28": "Show only the tickets from the selected database.", - "src.component.Task.component.ImportModal.F8B503DB": "The following work orders have been imported and no further operations are required.", - "src.component.Task.component.ImportModal.49FDDB00": "The following work order types do not match and cannot be imported. It is recommended to select the corresponding work order type and re-import.", - "src.component.Task.component.ImportModal.F42820DF": "To be imported", - "src.component.Task.component.ImportModal.4559F373": "Host IP/Domain Name:", - "src.component.Task.component.ImportModal.FF8162DA": "Port", - "src.component.Task.component.ImportModal.825A80AC": "Please select a new database.", - "src.component.Task.component.ExecuteFailTip.BE7C5B92": "If the database connection fails or the project does not exist during the cycle execution, the task execution may fail; if the scheduling fails continuously within 30 days, the task will be automatically terminated.", - "src.page.Workspace.components.SQLResultSet.36FAD6DF": "Close Current Result Set", - "src.page.Workspace.components.SQLResultSet.A460FEDE": "Close Other Result Sets", - "src.page.Workspace.components.SQLResultSet.989D6FA1": "Close All Result Sets", - "src.component.helpDoc.7A52D62F": "The method to retrieve target record rows", - "src.component.ODCSetting.config.user.20C5DFB4": "Result Set Display", - "src.component.ODCSetting.config.user.2D4F8470": "The latest query results overwrite the previous query results. To view the results of multiple executions, you need to manually fix the result set (right-click in the Result Set tab).", - "src.component.ODCSetting.config.user.C8FE70F6": "Overwrite", - "src.component.ODCSetting.config.user.FACA6552": "Multiple query results are displayed in appended form and can be closed only if you manually close the Result Set tab.", - "src.component.ODCSetting.config.user.5BCACB4C": "Append", - "src.component.Task.component.Status.F318D350": "Execution exception", - "src.component.Task.component.ShardingStrategyItem.B6EB2F37": "Using a full table scan for data retrieval will make the process more stable, but performance may be affected.", - "src.component.Task.component.ShardingStrategyItem.61BD0252": "Data Retrieval through Full Table Scan", - "src.component.Task.modals.DataClearTask.DetailContent.E35B6945": "Data Retrieval through Full Table Scan", - "src.component.Task.modals.DataClearTask.DetailContent.A75CE867": "Yes", - "src.component.Task.modals.DataClearTask.DetailContent.CB4C9BFB": "No", - "src.component.Task.modals.DataArchiveTask.DetailContent.44776A6F": "Data Retrieval through Full Table Scan", - "src.component.Task.modals.DataArchiveTask.DetailContent.D405684B": "Yes", - "src.component.Task.modals.DataArchiveTask.DetailContent.53EB9D22": "No" + "src.component.Task.AlterDdlTask.CreateModal.A56878AD": "Enter the number of retry attempts on lock failure" } diff --git a/src/locales/must/strings/zh-CN.json b/src/locales/must/strings/zh-CN.json index 8c9cb136d..24d76291c 100644 --- a/src/locales/must/strings/zh-CN.json +++ b/src/locales/must/strings/zh-CN.json @@ -141,6 +141,7 @@ "odc.ImportDrawer.ImportForm.BatchSubmissionQuantity": "批量提交数量", "workspace.window.sql.explain.detail.failed": "查看 SQL 执行详情失败", "odc.component.SnippetForm.SyntaxDescription": "代码片段描述", + "odc.components.ImportDrawer.NoMappingRelationshipSelected": "未选择映射关系", "odc.helper.page.openPage.CreateSynonym": "新建同义词", "odc.components.SynonymPage.Created": "创建时间:", "odc.src.d.ts.Function": "函数", @@ -244,6 +245,7 @@ "workspace.window.createSequence.params.cached.no": "无缓存", "workspace.window.createPackage.modal.title": "新建程序包", "odc.components.TaskManagePage.TerminatedSuccessfully": "终止成功", + "odc.components.ImportDrawer.DuplicateMappingRelationships": "映射关系存在重复", "odc.components.PackagePage.LastModifiedTime": "最近修改时间:", "odc.CreateTriggerPage.component.AdvancedInfoFrom.SelectAtLeastOneEvent": "请至少选择一个事件", "workspace.window.function.propstab.params": "参数", @@ -552,6 +554,7 @@ "odc.src.store.sql.Compile": "编译", "odc.ImportDrawer.ImportForm.ClickOrDragTheFile": "点击或将文件拖拽到这里上传", "workspace.window.createTable.partition.value.listColumns.placelholder": "(字段1,字段2),(字段1,字段2)", + "workspace.window.sql.limit.placeholder": "1000", "workspace.window.recyclebin.button.purgeAll": "清空", "odc.component.GrammerHelpSider.SnippetslacksInTotal": "共 {snippetsLength} 条", "odc.helper.page.openPage.CreateATrigger": "新建触发器", @@ -578,6 +581,7 @@ "odc.ImportDrawer.csvMapping.TargetFieldType": "目标字段类型", "odc.components.PLPage.SavedSuccessfully": "保存成功", "odc.TreeNodeMenu.config.view.ViewViewProperties": "查看视图属性", + "odc.components.ImportDrawer.SelectTheFieldsToBe": "请选择需要映射的字段", "odc.components.PLPage.statusBar.ManualTermination": "手动终止", "odc.components.TypePage.ModificationTime": "修改时间:", "odc.component.CreateTypeModal.Cancel": "取消", @@ -1370,6 +1374,7 @@ "odc.component.CommonTaskDetailModal.TaskFlow.Handler": "处理人", "odc.component.helpDoc.doc.TheApprovalTimesOutAnd": "审批超时,任务将过期", "odc.component.DataTransferModal.ExecutionMethod": "执行方式", + "odc.TaskManagePage.component.TaskTools.InitiateAgain": "再次发起", "odc.component.CommonTaskDetailModal.TaskFlow.Handled": "可处理人", "odc.components.FormRecordExportModal.Cancel": "取消", "odc.component.OSSDragger2.Cancel": "取消", @@ -1544,6 +1549,7 @@ "odc.component.CommonTaskDetailModal.TaskFlow.InitiateATask": "发起任务", "odc.components.FormRecordExportModal.Export": "导出", "odc.TaskManagePage.component.TaskTools.Reject": "拒绝", + "odc.TaskManagePage.component.TaskTools.Terminate": "终止", "odc.components.RecordPage.component.DetailedRules": "执行细则", "odc.component.DetailModal.dataMocker.RiskLevel": "风险等级", "odc.component.TaskStatus.ApprovalExpired": "审批已过期", @@ -1991,6 +1997,7 @@ "odc.components.CreateShadowSyncModal.Submit": "提交", "odc.StructConfigPanel.StructAnalysisResult.column.ExecutionResult": "执行结果", "odc.component.CommonTaskDetailModal.TaskRecord.View": "查看", + "odc.components.PartitionDrawer.Remarks": "备注", "odc.component.AddConnectionForm.AddressItems.ClusterName": "集群名", "odc.AddConnectionForm.Account.PrivateAccount.TheEndOfTheAccount": "账号首尾包含空格", "odc.components.SQLPage.BecauseTheSqlIsToo": "由于 SQL 过长,编辑器将只支持预览", @@ -2227,6 +2234,7 @@ "odc.Content.ListItem.TenantTenant": "租户: {tenant}", "odc.component.Crontab.const.Saturday": "周六", "odc.components.CreateSQLPlanTaskModal.Save": "保存", + "odc.TaskManagePage.component.TaskTools.View": "查看", "odc.components.CreateSQLPlanTaskModal.CreateAnSqlPlan": "新建 SQL 计划", "odc.components.RolePage.component.ResourceManagementPermissions": "资源管理权限", "odc.src.d.ts.TableGroup": "表组", @@ -2795,6 +2803,7 @@ "odc.Project.User.DeletedSuccessfully": "删除成功", "odc.SensitiveRule.components.CheckboxInput.PleaseEnter": "请输入", "odc.Secure.Approval.ExecutionValidityPeriod": "执行有效期", + "odc.AlterDdlTask.CreateModal.LockTableTimeout": "锁表超时时间", "odc.NewSSODrawerButton.SSOForm.ConfigurationName": "配置名称", "odc.Datasource.Info.Delete": "删除", "odc.components.SensitiveColumn.AreYouSureYouWant.1": "确认要删除敏感列吗?", @@ -2827,6 +2836,7 @@ "odc.NewSSODrawerButton.SSOForm.Type": "类型", "odc.component.FormModal.TitleName": "{title}名称", "odc.ResourceTree.Nodes.package.Subprogram": "子程序", + "odc.AlterDdlTask.CreateModal.Seconds": "秒", "odc.Project.Database.CharacterEncoding": "字符编码", "odc.page.Secure.RiskLevel": "风险等级", "odc.DataArchiveTask.DetailContent.TaskType": "任务类型", @@ -2861,6 +2871,7 @@ "odc.AlterDdlTask.DetailContent.SourceTableCleanupPolicyAfter": "完成后源表清理策略", "odc.SensitiveRule.components.DetectWay.Script": "脚本", "odc.AlterDdlTask.CreateModal.AreYouSureYouWant": "是否确认取消无锁结构变更?", + "odc.AlterDdlTask.DetailContent.LockTableTimeout": "锁表超时时间", "odc.Project.Setting.ProjectInformation": "项目信息", "odc.Project.User.AreYouSureYouWant": "是否确定删除该成员?", "odc.SensitiveRule.components.FormSensitiveRuleDrawer.AreYouSureYouWant.1": "是否确认取消新建?", @@ -3193,6 +3204,7 @@ "odc.AsyncTask.DetailContent.RollbackContent": "回滚内容", "odc.NewSSODrawerButton.SSOForm.ObtainTheAccessTokenAddress": "授权服务器提供的获取 access-token 地址", "odc.ExternalIntegration.SSO.WhetherToEnable": "是否启用", + "odc.AlterDdlTask.CreateModal.NumberOfFailedRetries": "失败重试次数", "odc.Env.components.InnerEnvironment.ConfigurationValue": "配置值", "odc.Script.Snippet.Copy": "复制", "odc.DataArchiveTask.CreateModal.Ok": "确定", @@ -3258,6 +3270,7 @@ "odc.Database.AddDataBaseButton.BoundProject": "- 已绑定项目:", "odc.component.EditPLParamsModal.StatementCannotBeEmpty": "语句不能为空", "odc.component.FormModal.TheProcessNameCannotExceed": "流程名称不超过 128 个字符", + "odc.component.helpDoc.doc.AfterTheTableLockTime": "超过锁表时间后,未切换完成可自动重试", "odc.SensitiveRule.components.FormSensitiveRuleDrawer.UpdateFailed": "更新失败", "odc.ExternalIntegration.SqlInterceptor.FollowTheSecuritySpecificationSql": "请按照 安全规范 - SQL 开发规范 - SQL 窗口规则 路径,使用您已配置并开启的 SQL 拦截集成", "odc.Env.components.EditRuleDrawer.ExecutionIsProhibitedAndApproval": "禁止执行,无法发起审批", @@ -3265,6 +3278,7 @@ "odc.component.CommonDetailModal.TaskExecuteRecord.DatabaseChanges": "数据库变更", "odc.components.RecordPage.column.EnterADataSource": "请输入所属数据源", "odc.components.SQLResultSet.DBTimeline.SqlPostCheck": "SQL 后置检查", + "odc.component.VersionModal.config.ThroughDataArchivingYouCan": "通过数据归档,您可以配置灵活的归档条件,实现冷热数据分离。", "odc.Sider.SpaceSelect.Ok": "确定", "odc.Record.RecordPage.column.DataSource": "数据源", "odc.Record.RecordPage.interface.TransferProject": "转移项目", @@ -3308,6 +3322,7 @@ "odc.components.SQLResultSet.DBTimeline.SqlPrecheck": "SQL 预检查", "odc.component.DescriptionInput.EnterADescriptionLessThan": "请输入描述,200字以内;未输入时,系统会根据对象和工单类型自动生成描述信息", "odc.DataArchiveTask.CreateModal.ExecutionTime": "执行时间", + "odc.component.VersionModal.config.OdcProvidesDatabaseObjectManagement": "ODC 提供了数据库对象管理、数据导入导出、SQL 编辑与执行、PL\n 编译与调试、数据生成、执行分析、数据库运维等工具能力。", "odc.Env.components.EditRuleDrawer.AllowExecution": "允许执行", "odc.ResourceTree.Datasource.DeletedSuccessfully": "删除成功", "odc.DataArchiveTask.CreateModal.VariableConfig.DeleteAVariable": "删除变量", @@ -3756,14 +3771,17 @@ "odc.src.component.Task.component.CommonDetailModal.Nodes.HighRisk": "高风险", "odc.src.page.Workspace.components.SQLResultSet.TheCurrentSQLExistenceMust": "当前 SQL 存在必须改进项,请修改后再执行", "odc.src.component.Task.AlterDdlTask.CreateModal.LockUsers.1": "锁定用户", + "odc.src.component.Task.AlterDdlTask.CreateModal.1BeforePerformingThe": "1. 执行无锁结构变更前请确保数据库服务器磁盘空间充足;", "odc.src.component.Task.DataClearTask.CreateModal.PleaseEnter": "请输入", "odc.src.component.Task.component.CommonDetailModal.Nodes.TheNumberOf": ",预检查处理 SQL 条数超过最大限制,当前任务流程将按", "odc.src.page.Project.Project.CreateProject.SecurityAdministrator": "安全管理员", "odc.src.component.Task.component.CommonDetailModal.Share": "分享", "odc.src.page.Workspace.components.SQLResultSet.SQLContentHasBeenModified": "SQL 内容已修改,已无法定位原问题行,请重新执行 SQL 语句或发起预检查", "odc.src.component.Task.PartitionTask.DetailContent.Partition": "分区计划", + "odc.src.component.Task.DataArchiveTask.CreateModal.PleaseEnter": "请输入", "odc.src.component.Task.PartitionTask.DetailContent.RiskLevel": "风险等级", "odc.src.component.Task.component.CommonDetailModal.Nodes.GradeContinuesToAdvance": "等级继续推进", + "odc.src.component.Task.DataArchiveTask.CreateModal.PleaseChoose.1": "请选择", "odc.src.component.Task.PartitionTask.DetailContent.Remark": "备注", "odc.src.page.Workspace.components.SQLResultSet.TheCurrentSQLNeedsApproval": "当前 SQL 存在需要审批项,请发起审批或修改后再执行", "odc.src.component.Task.PartitionTask.DetailContent.CreationTime": "创建时间", @@ -3772,7 +3790,10 @@ "odc.src.component.Task.component.CommonDetailModal.Replication": "复制成功", "odc.src.component.Task.DataClearTask.CreateModal.PleaseChoose": "请选择", "odc.src.component.Task.DataClearTask.CreateModal.PleaseChoose.1": "请选择", + "odc.src.component.Task.DataArchiveTask.CreateModal.PleaseChoose": "请选择", "odc.src.component.Task.NoCurrentWorkOrderView": "无当前工单查看权限", + "odc.src.component.Task.AlterDdlTask.CreateModal.2WhenCreatingA": "2. 创建工单选择源表清理策略时建议选择保留源表;", + "odc.src.component.Task.AlterDdlTask.CreateModal.3IfTheOB": "3. 表名切换之前会锁定您指定的数据库账号,并关闭该账号对应的会话。表名切换期间,锁定账号涉及应用将无法访问数据库,请勿在业务高峰期执行;", "src.component.Task.component.PartitionPolicyTable.F057FAAF": "顺序递增", "src.component.Task.StructureComparisonTask.DetailContent.E8DAF6BA": "操作", "src.page.Datasource.Datasource.NewDatasourceDrawer.Form.Account.445C8BBC": "默认", @@ -3783,6 +3804,7 @@ "src.component.Task.ApplyDatabasePermission.CreateModal.B0247EF7": "请输入原因描述", "src.page.Project.User.ManageModal.B7377F46": "回收后不可撤回", "src.component.Task.StructureComparisonTask.CreateModal.A8C717F6": "取消", + "src.component.helpDoc.AEEC5916": "关于注意事项第 3 条,由您指定将要锁定的账号,是为了保障表名切换期间数据一致性的同时尽可能降低对业务的影响。请您确保指定账号的准确性,若您未指定任何账号,ODC 将不会进行任何账号锁定及 Kill Session 操作,切换期间数据的一致性将需要由您来保障。", "src.component.Task.ApplyDatabasePermission.DetailContent.2C812515": "创建人", "src.component.EditorToolBar.actions.3BDAC881": "运行当前语句 ", "src.component.Task.ApplyDatabasePermission.DetailContent.265A918A": "申请原因", @@ -3860,6 +3882,7 @@ "src.component.ODCSetting.config.56F5CB81": "Delimiter 设置", "src.component.Task.component.PartitionPolicyFormTable.7D6F23AE": "分区保留数量", "src.page.Workspace.components.SessionContextWrap.SessionSelect.38EA55F4": "项目:", + "src.component.Task.DataClearTask.DetailContent.D2882643": "是", "src.page.ExternalIntegration.SSO.NewSSODrawerButton.SSOForm.DBB95C01": "请输入server", "src.page.Project.Notification.components.F80DF1C7": "飞书", "src.component.Task.StructureComparisonTask.DetailContent.3C7B0B00": "目标端数据库", @@ -4009,6 +4032,7 @@ "src.component.Task.0B2B1D60": "手动执行", "src.page.Project.Notification.components.DFE43F9E": "请求方法", "src.component.helpDoc.EFADD11A": "在参与者的基础上,同时可以管理敏感列", + "src.component.Task.StructureComparisonTask.CreateModal.52828286": "描述", "src.page.Project.User.ManageModal.UserAuthList.C3B2211E": "请输入", "src.page.ExternalIntegration.SSO.NewSSODrawerButton.SSOForm.DC978DE4": "访问密码", "src.component.Task.ApplyDatabasePermission.CreateModal.28506030": "申请原因", @@ -4021,6 +4045,7 @@ "src.component.Task.component.PartitionPolicyTable.8BC770B0": "预创建数量", "src.component.Task.StructureComparisonTask.CreateModal.B4FAB9EC": "新建失败", "src.component.ODCSetting.647A18AA": "确定要恢复默认设置吗?", + "src.component.Task.DataClearTask.CreateModal.23542D89": "请选择", "src.page.Project.Notification.components.BA80DE5A": "取消", "src.component.ODCSetting.config.80241964": "正常", "src.component.Task.component.PartitionPolicyFormTable.DF75EB9E": "如何配置", @@ -4051,6 +4076,7 @@ "src.page.Project.Notification.components.713896D2": "不超过", "src.component.Task.StructureComparisonTask.CreateModal.45DB3909": "新建结构比对", "src.component.Task.PartitionTask.CreateModal.E454F701": "忽略错误继续任务", + "src.component.Task.StructureComparisonTask.CreateModal.67E284BD": "描述不超过 200 个字符", "src.page.Project.Notification.components.A50DD7D6": "测试消息发送成功!", "src.component.Task.9B79BD20": "自动执行", "src.page.ExternalIntegration.SSO.NewSSODrawerButton.SSOForm.2E3212A5": "搜索群组查询时使用的起始点或基准点", @@ -4060,6 +4086,7 @@ "src.page.ExternalIntegration.SSO.NewSSODrawerButton.SSOForm.80683796": "用户查询的过滤条件,可以基于用户的属性(如用户名、邮箱、部门等)来过滤搜索结果", "src.component.Task.DataMockerTask.CreateModal.2C3DF5A5": "新建模拟数据", "src.page.Project.User.ManageModal.UserAuthList.19A27247": "过期时间", + "src.component.Task.DataClearTask.DetailContent.834E7D89": "否", "src.component.Task.component.PartitionPolicyTable.D94AE82A": "创建方式", "src.page.Secure.Env.components.FF5B44FE": "编辑环境", "src.page.Login.components.LDAPModal.AA5AB5DA": "LDAP 密码不能为空", @@ -4208,6 +4235,7 @@ "src.component.Task.AsyncTask.CreateModal.6EEFAEA6": "新建数据库变更", "src.page.Project.Notification.components.95A710C5": "请求方法", "src.component.ODCSetting.config.1ACE7366": "修改此参数,将在 ODC 重启后生效", + "src.component.Task.PartitionTask.CreateModal.026392ED": "请输入描述,不超过200个字符;未输入时,系统会根据对象和工单类型自动生成描述信息", "src.page.Project.User.ManageModal.UserAuthList.583E307F": "回收", "src.page.Secure.Env.65EAAB75": "确认删除该环境么?", "src.page.Login.components.LDAPModal.95DA8BD0": "LDAP 登录", @@ -4228,6 +4256,7 @@ "src.page.Project.User.ManageModal.UserAuthList.8E0CB3F5": "数据库", "src.page.Secure.Env.48529F6E": "全部环境", "src.component.Task.PartitionTask.CreateModal.0A49F493": "小时", + "src.component.Task.DataClearTask.CreateModal.99D8FCD6": "使用主键清理", "src.page.Project.Notification.components.F40AD99E": "Webhook地址", "src.page.ExternalIntegration.SSO.NewSSODrawerButton.SSOForm.5E8157A1": "次", "src.component.Task.component.PartitionPolicyTable.5C3A7795": "类型", @@ -4297,6 +4326,7 @@ "src.component.Task.StructureComparisonTask.DetailContent.575F8B39": "比对范围", "src.component.Task.ApplyDatabasePermission.CreateModal.87E335B0": "1 年", "src.component.Task.component.PartitionPolicyFormTable.A9C95E9D": "当前表如果包含全局索引,删除分区会导致全局索引失效,如果选择重建全局索引可能耗时很久,请谨慎操作", + "src.component.Task.DataClearTask.DetailContent.2D1A14AB": "使用主键清理", "src.component.Task.component.TaskTable.80E1D16A": "结构比对", "src.page.Project.Notification.components.CA33D8AB": "通道名称已存在", "src.page.Project.Notification.components.EA00E8BD": "请选择通道类型", @@ -4868,6 +4898,7 @@ "src.component.SQLConfig.1A5CCA98": "SQL 执行", "src.page.Project.User.ManageModal.Table.TaskApplyList.EC52BFBC": "过期时间", "src.component.Task.ApplyTablePermission.CreateModal.E26609A0": "导出", + "src.component.Task.ApplyTablePermission.CreateModal.BC4488C7": "永不过期", "src.component.Task.ApplyTablePermission.CreateModal.78B63403": "请选择", "src.component.Task.ApplyTablePermission.CreateModal.8955ACFE": "暂无项目,请先", "src.component.Task.ApplyTablePermission.CreateModal.77D8F632": "请选择项目", @@ -4936,7 +4967,12 @@ "src.page.Workspace.SideBar.ResourceTree.DatabaseSearchModal.components.FA5E6855": "定位到数据库 \"{databaseName}\"", "src.page.Workspace.components.DDLResultSet.6477DD60": "集合 SQL 的执行详情、物理执行计划、全链路诊断的多维度视图,帮助快速定位执行慢查询的根因", "src.component.ExecuteSqlDetailModal.1882C007": "计划统计", + "src.component.Task.component.ShardingStrategyItem.E5A6B481": "全表扫描", + "src.component.Task.component.ShardingStrategyItem.F91EEC6C": "条件匹配", + "src.component.Task.component.ShardingStrategyItem.3BD95C1A": "搜索策略", "src.component.Task.component.ShardingStrategyItem.D5F45B7A": "请选择搜索策略", + "src.component.Task.DataClearTask.DetailContent.E977DA21": "搜索策略", + "src.component.Task.DataArchiveTask.DetailContent.4844C10F": "搜索策略", "src.page.Workspace.components.TablePage.ShowTableBaseInfoForm.56FAF24C": "逻辑表名称", "src.page.Workspace.components.TablePage.ShowTableBaseInfoForm.C1C0A345": "逻辑表表达式", "src.page.Workspace.components.TablePage.ShowTableBaseInfoForm.200BCB34": "表拓扑", @@ -5615,6 +5651,7 @@ "src.page.Console.9B6E647E": "反馈建议", "src.page.Console.470D66DD": "OceanBase AP 实时 SQL 诊断能力解析", "src.page.Console.67004EA1": "ODC SQL 检查自动识别高危操作", + "src.page.Console.4F744942": "快速集成 ODC 的企业级账号体系", "src.page.Console.15BABD7A": "企业级管控协同:守护数据库的每一次变更", "src.page.Console.A0F9F190": "通过 ODC 实现分库分表的管理和变更", "src.page.Console.7BD8C2BB": "数据脱敏管控实践", @@ -5674,6 +5711,7 @@ "src.component.Task.component.PartitionPolicyFormTable.E0F368F6": "数值", "src.component.Task.component.PartitionPolicyFormTable.053D6705": "日期时间", "src.component.Task.component.PartitionPolicyFormTable.FCD94271": "时间戳", + "src.component.Task.component.PartitionPolicyFormTable.9F024A60": "当前预创建分区数量过小,若调度失败恐影响业务运行,建议调整预创建分区数量至2个及以上。", "src.component.Task.component.PartitionPolicyFormTable.E753A67D": "暂未设置创建策略,无 SQL 可预览", "src.component.Task.component.PartitionPolicyFormTable.18397BFF": "分区名的生成方式", "src.component.Task.component.PartitionPolicyFormTable.044700A2": "分区名的生成规则,可引用变量。比如:concat('P_',${COL1}),其中 COL1 表示分区表的分区键。", @@ -5733,6 +5771,7 @@ "src.component.ODCSetting.B7CA4CA1": "请输入查询条数默认值", "src.component.ODCSetting.5127321B": "不超过 {sessionQueryLimit}", "src.component.ODCSetting.A8E22CC2": "不超过 {queryLimit}", + "src.component.ODCSetting.4C8CC366": "不超过 100000", "src.component.ODCSetting.DD41DED4": "请输入查询条数上限", "src.component.ODCSetting.6BCFD6DD": "用户", "src.component.ODCSetting.47586FD4": "个人空间", @@ -5797,14 +5836,21 @@ "src.component.Task.component.PartitionPolicyFormTable.RuleFormItem.13B22678": "yyyy年MM月dd日 HH:mm:ss", "src.component.Task.LogicDatabaseAsyncTask.CreateModal.C53810BF": "逻辑库变更内容暂不支持注释解析,编写注释可能会导致 SQL 语句解析异常。", "src.component.CommonTable.2363A9C8": "仅展示周期执行任务", + "src.page.Workspace.components.DDLResultSet.Sync.38BDFD2B": "查看刷新记录", "src.page.Auth.Role.component.FormModal.3D1B7861": "当前授予的权限存在包含关系,仅保留权限范围大的记录。", + "src.d.ts.25D43093": "数据源不存在", + "src.d.ts.CAC52ADB": "实例不存在", "src.d.ts.B89ABE6D": "类型不匹配", + "src.d.ts.7AE88D0A": "已导入", "src.component.Task.component.TaskTable.D4FAED98": "导入{activeTaskLabel}", "src.component.Task.component.TaskTable.55FC08BB": "正在导入中", "src.component.Task.component.PartitionPolicyFormTable.B988E243": "当前预创建分区数量过小,若调度失败恐影响业务运行,建议调整预创建分区数量至2个及以上。", "src.component.Task.component.ImportModal.997B6AC7": "导入定时任务已完成", + "src.component.Task.component.ImportModal.E256F212": "{resultLength} 个作业导入成功。 建议手动为任务", "src.component.Task.component.ImportModal.8AD83BC8": "配置消息通知", "src.component.Task.component.ImportModal.04E1F63D": ",保证任务异常能够被及时发现。", + "src.component.Task.component.ImportModal.E12ACFFB": "导出定时任务已完成", + "src.component.Task.component.ImportModal.47DA5967": "{successCount} 个作业导入成功,", "src.component.Task.component.ImportModal.32698C46": "{failedCount} 个作业导入失败。 建议手动为任务", "src.component.Task.component.ImportModal.801362FE": "配置消息通知", "src.component.Task.component.ImportModal.60663407": ",保证任务异常能够被及时发现。如需了解导出详情,可", @@ -5822,6 +5868,8 @@ "src.component.Task.component.ImportModal.B6BCF0C0": "取消", "src.component.Task.component.ImportModal.2F42ECBC": "下一步:预览", "src.component.Task.component.ImportModal.9FFF1F71": "上一步:上传文件", + "src.component.Task.component.ImportModal.81026A36": "导入", + "src.component.Task.component.ImportModal.0C3395D0": "仅支持导入由阿里云 OceanBase 数据研发导出的配置文件。\n请确认作业关联的实例已切换至 OB Cloud 当前项目中,系统将根据实例信息匹配或新建对应的数据源。", "src.component.Task.component.ImportModal.D8DFA444": "请上传", "src.component.Task.component.ImportModal.7B17FBF4": "文件上传失败,失败原因:{fileResponseDataErrMsg}", "src.component.Task.component.ImportModal.3CEE142A": "文件密钥", @@ -5830,14 +5878,30 @@ "src.component.Task.component.ImportModal.C6AC4C4C": "请输入", "src.component.Task.component.ImportModal.889B80AD": "导入项目", "src.component.Task.component.ImportModal.30D6697A": "请选择", + "src.component.Task.component.ImportModal.B3686BE9": "请确认相关数据源已加至项目", "src.component.Task.component.ImportModal.1E47B6CE": "新建数据源", "src.component.Task.component.ImportModal.E82DB6D1": "请选择", "src.component.Task.component.ImportModal.68180A60": "新建项目", "src.component.Task.component.ImportModal.A8BC98CA": "数据源", "src.component.Task.component.ImportModal.0B1F9DCD": "数据库", "src.component.Task.component.ImportModal.263EFA83": "类型", + "src.component.Task.component.ImportModal.40440087": "云厂商", + "src.component.Task.component.ImportModal.F2C95E45": "地域", + "src.component.Task.component.ImportModal.EFE4E0D0": "连接信息", + "src.component.Task.component.ImportModal.4A911A47": "实例名称", + "src.component.Task.component.ImportModal.396EF2AD": "租户名称", "src.component.Task.component.ImportModal.5928CAE8": "数据库账号", + "src.component.Task.component.ImportModal.B4E8467F": "源端数据库", + "src.component.Task.component.ImportModal.50582618": "源端数据库", + "src.component.Task.component.ImportModal.76F572C8": "目标端数据库", + "src.component.Task.component.ImportModal.776397D6": "目标端数据库", + "src.component.Task.component.ImportModal.B5C62BDC": "工单描述", + "src.component.Task.component.ImportModal.1EB75659": "数据库", + "src.component.Task.component.ImportModal.8D781AC2": "数据库", + "src.component.Task.component.ImportModal.5DE578E8": "原编号", + "src.component.Task.component.ImportModal.2043ADA9": "原项目", "src.component.Task.component.ImportModal.1BF95006": "暂无可导入的作业", + "src.component.Task.component.ImportModal.E8DE787E": "可导入", "src.component.Task.component.AsyncTaskOperationButton.D9D9A882": "请先选择工单", "src.component.Task.component.AsyncTaskOperationButton.683E8B65": "取消", "src.component.Task.component.AsyncTaskOperationButton.2D7C7380": "我已确认相关业务风险", @@ -5906,7 +5970,18 @@ "src.component.Task.component.AsyncTaskOperationButton.89CE28F7": "终止错误", "src.component.Task.component.AsyncTaskOperationButton.08EE66D0": "终止任务", "src.component.Task.component.AsyncTaskOperationButton.5E6481EB": "{idsLength} 个任务正在终止中...", - "src.page.Workspace.components.DDLResultSet.Sync.38BDFD2B": "查看刷新记录", + "src.component.Task.AlterDdlTask.CreateModal.A37F2729": "在表名切换前,锁定原表禁写。请确保您的数据库满足以下条件:\n 1. OB 版本号 ≥ 4.2.5 且 < 4.3.0 \n 2. 参数 enable_lock_priority 已设置为 true", + "src.component.Task.AlterDdlTask.CreateModal.8AC211D6": "在表名切换前,锁定原表禁写。请确保您的数据库满足以下条件:\n1. OB 版本号 ≥ 4.2.5 且 < 4.3.0 ,且参数 enable_lock_priority 已设置为 true \n2. ODP 版本 ≥ 4.3.1 且已进行以下参数设置:\n alter proxyconfig set proxy_id=1;\n alter proxyconfig set client_session_id_version=2;\n alter proxyconfig set enable_single_leader_node_routing = false;", + "src.component.Task.AlterDdlTask.CreateModal.D1732875": "注意:执行无锁结构变更前请确保数据库服务器磁盘空间充足。", + "src.component.Task.AlterDdlTask.CreateModal.05BCC428": "表名切换禁写策略", + "src.component.Task.AlterDdlTask.CreateModal.B0CFE0E1": "在表名切换前,系统将锁定您指定的数据库账号,并终止该账号的当前会话。切换期间,所有涉及该账号的应用将无法访问数据库。建议避免在业务高峰期执行此操作,以减少对业务的影响。", + "src.component.Task.AlterDdlTask.CreateModal.D056FD5E": "锁定用户", + "src.component.Task.AlterDdlTask.CreateModal.EA0D82E0": "锁定表", + "src.component.Task.AlterDdlTask.CreateModal.125DF508": "推荐", + "src.component.ODCSetting.2BE8F5C3": "不超过{maxResultsetRows}", + "src.component.Task.AlterDdlTask.CreateModal.44A394CE": "在表名切换前,锁定原表禁写。", + "src.component.Task.AlterDdlTask.CreateModal.6DC73E39": "在表名切换前,锁定原表禁写。请您确保 ODP 满足以下条件:\nODP 版本 ≥ 4.3.1 且已进行以下参数设置:\n alter proxyconfig set proxy_id=1;\n alter proxyconfig set client_session_id_version=2;\n alter proxyconfig set enable_single_leader_node_routing = false;", + "src.component.Task.AlterDdlTask.CreateModal.C62A7DBB": "需要将参数 enable_lock_priority 设置为 true", "src.component.Task.component.ActionBar.797981FE": "无可查看的结果信息", "src.component.Task.component.ActionBar.5218D741": "查询结果", "src.component.ODCSetting.9651E837": "不超过{maxResultsetRows}", @@ -5919,71 +5994,8 @@ "src.component.ODCSetting.config.AB482BC7": "支持查看查询结果", "src.component.ODCSetting.config.3FE1F2A7": "支持下载查询结果", "src.component.ODCSetting.config.1ED38C41": "数据库变更查询结果", - "src.component.Task.AlterDdlTask.CreateModal.D1732875": "注意:执行无锁结构变更前请确保数据库服务器磁盘空间充足。", - "src.component.Task.AlterDdlTask.CreateModal.05BCC428": "表名切换禁写策略", - "src.component.Task.AlterDdlTask.CreateModal.B0CFE0E1": "在表名切换前,系统将锁定您指定的数据库账号,并终止该账号的当前会话。切换期间,所有涉及该账号的应用将无法访问数据库。建议避免在业务高峰期执行此操作,以减少对业务的影响。", - "src.component.Task.AlterDdlTask.CreateModal.D056FD5E": "锁定用户", - "src.component.Task.AlterDdlTask.CreateModal.EA0D82E0": "锁定表", - "src.component.Task.AlterDdlTask.CreateModal.125DF508": "推荐", - "src.component.Task.AlterDdlTask.CreateModal.44A394CE": "在表名切换前,锁定原表禁写。", - "src.component.Task.AlterDdlTask.CreateModal.6DC73E39": "在表名切换前,锁定原表禁写。请您确保 ODP 满足以下条件:\nODP 版本 ≥ 4.3.1 且已进行以下参数设置:\n alter proxyconfig set proxy_id=1;\n alter proxyconfig set client_session_id_version=2;\n alter proxyconfig set enable_single_leader_node_routing = false;", - "src.component.Task.AlterDdlTask.CreateModal.C62A7DBB": "需要将参数 enable_lock_priority 设置为 true", "src.page.Console.8F636FA5": "带你走进 ODC 团队空间的精彩世界", "src.component.helpDoc.7F2FDC2E": "若锁定失败,可自动重试次数", "src.component.Task.AlterDdlTask.CreateModal.1A8336C1": "锁定失败重试次数", - "src.component.Task.AlterDdlTask.CreateModal.A56878AD": "请输入锁定失败重试次数", - "src.page.Workspace.components.SessionContextWrap.SessionSelect.SessionDropdown.49B355DA": "管理数据库", - "src.page.Console.components.RecentlyDatabase.806E294C": "对象存储暂不支持打开 SQL 控制台", - "src.d.ts.BE8F2539": "已存在", - "src.component.Task.component.TaskTable.E20D58A0": "个人空间任务仅支持本人管理和查看,且不提供告警配置功能,因此不建议将其用于对生产业务有强依赖的场景。如需在生产业务中使用该功能,请优先选择团队空间,并配置相应的消息通知机制,以保障业务的正常运转。", - "src.component.Task.component.ImportModal.A6892359": "(包含 {importedCount} 个已导入的任务)", - "src.component.Task.component.ImportModal.AF106FF6": "{resultLength} 个作业导入成功{importedDescription}。", - "src.component.Task.component.ImportModal.97A1A7A3": "{resultLength} 个作业导入成功{importedDescription}。", - "src.component.Task.component.ImportModal.1B653B27": "建议手动为任务", - "src.component.Task.component.ImportModal.92D087FB": "(包含 {importedCount} 个已导入的任务)", - "src.component.Task.component.ImportModal.94BFD01C": "{successCount} 个作业导入成功{importedDescription}, {failedCount} 个作业导入失败。可", - "src.component.Task.component.ImportModal.AE88E265": "{successCount} 个作业导入成功{importedDescription}, ", - "src.component.Task.component.ImportModal.7C3EB72C": "工单", - "src.component.Task.component.ImportModal.DCD1BA1A": "原数据库", - "src.component.Task.component.ImportModal.93C2FD0F": "原数据库", - "src.component.Task.component.ImportModal.A8B5EF34": "数据源:", - "src.component.Task.component.ImportModal.4C0466BB": "原数据库", - "src.component.Task.component.ImportModal.E78A5A7E": "源端数据库", - "src.component.Task.component.ImportModal.47F8A3BA": "目标端数据库", - "src.component.Task.component.ImportModal.55E87B63": "原状态", - "src.component.Task.component.ImportModal.DFCD4ABB": "工单导入成功后将重新启用", - "src.component.Task.component.ImportModal.37A63CCA": "类型", - "src.component.Task.component.ImportModal.D22149A7": "新源端", - "src.component.Task.component.ImportModal.6992AD58": "新目标端", - "src.component.Task.component.ImportModal.01BA659E": "新数据库", - "src.component.Task.component.ImportModal.EA6397CD": "我己确认导入的工单新旧数据库对象一致", - "src.component.Task.component.ImportModal.E756EF6A": "导入 ({selectedRowKeysLength})", - "src.component.Task.component.ImportModal.A28A2A00": "仅支持导入由 阿里云 OceanBase 数据研发 或 ODC\n 导出的配置文件;在导入之前,请先将添加相关数据源、 井指定对应的项目。", - "src.component.Task.component.ImportModal.C264F2F5": "勾选需要导入的工单,导入后将重新启用。导入前请检查涉及的新旧数据库对象是否一致,否则导入或执行时可能出现失败。", - "src.component.Task.component.ImportModal.3137AA28": "仅显示已选择数据库的工单", - "src.component.Task.component.ImportModal.F8B503DB": "以下工单已导入,无需重复操作。", - "src.component.Task.component.ImportModal.49FDDB00": "以下工单类型不匹配、无法导入,建议选择对应工单类型重新导入。", - "src.component.Task.component.ImportModal.F42820DF": "待导入", - "src.component.Task.component.ImportModal.4559F373": "主机 IP/域名:", - "src.component.Task.component.ImportModal.FF8162DA": "端口", - "src.component.Task.component.ImportModal.825A80AC": "请选择新的数据库", - "src.component.Task.component.ExecuteFailTip.BE7C5B92": "周期执行期间若数据库连接失败或项目不存在,可能导致任务执行失败; 30 天内连续调度失败, 任务将自动终止。", - "src.page.Workspace.components.SQLResultSet.36FAD6DF": "关闭该结果集", - "src.page.Workspace.components.SQLResultSet.A460FEDE": "关闭其它结果集", - "src.page.Workspace.components.SQLResultSet.989D6FA1": "关闭所有结果集", - "src.component.helpDoc.7A52D62F": "检索目标记录行的方式", - "src.component.ODCSetting.config.user.20C5DFB4": "结果集展示", - "src.component.ODCSetting.config.user.2D4F8470": "最近查询结果覆盖上一次查询结果,若要查看多次执行结果需手动固定结果集(在结果集页签右键操作)。", - "src.component.ODCSetting.config.user.C8FE70F6": "覆盖", - "src.component.ODCSetting.config.user.FACA6552": "多次查询结果追加展示,需手动关闭结果集页签才会关闭。", - "src.component.ODCSetting.config.user.5BCACB4C": "追加", - "src.component.Task.component.Status.F318D350": "执行异常", - "src.component.Task.component.ShardingStrategyItem.B6EB2F37": "若使用全表扫描方式进行数据搜索,处理过程将更加稳定但性能可能会受到影响", - "src.component.Task.component.ShardingStrategyItem.61BD0252": "通过全表扫描进行数据搜索", - "src.component.Task.modals.DataClearTask.DetailContent.E35B6945": "通过全表扫描进行数据搜索", - "src.component.Task.modals.DataClearTask.DetailContent.A75CE867": "是", - "src.component.Task.modals.DataClearTask.DetailContent.CB4C9BFB": "否", - "src.component.Task.modals.DataArchiveTask.DetailContent.44776A6F": "通过全表扫描进行数据搜索", - "src.component.Task.modals.DataArchiveTask.DetailContent.D405684B": "是", - "src.component.Task.modals.DataArchiveTask.DetailContent.53EB9D22": "否" + "src.component.Task.AlterDdlTask.CreateModal.A56878AD": "请输入锁定失败重试次数" } diff --git a/src/locales/must/strings/zh-TW.json b/src/locales/must/strings/zh-TW.json index b55dc4e23..688b047b6 100644 --- a/src/locales/must/strings/zh-TW.json +++ b/src/locales/must/strings/zh-TW.json @@ -141,6 +141,7 @@ "odc.ImportDrawer.ImportForm.BatchSubmissionQuantity": "批量提交數量", "workspace.window.sql.explain.detail.failed": "查看 SQL 執行詳情失敗", "odc.component.SnippetForm.SyntaxDescription": "片段描述", + "odc.components.ImportDrawer.NoMappingRelationshipSelected": "未選擇映射關係", "odc.helper.page.openPage.CreateSynonym": "建立同義字", "odc.components.SynonymPage.Created": "建立時間:", "odc.src.d.ts.Function": "函數", @@ -244,6 +245,7 @@ "workspace.window.createSequence.params.cached.no": "無緩衝", "workspace.window.createPackage.modal.title": "建立程式包", "odc.components.TaskManagePage.TerminatedSuccessfully": "終止成功", + "odc.components.ImportDrawer.DuplicateMappingRelationships": "映射關係存在重複", "odc.components.PackagePage.LastModifiedTime": "最近修改時間:", "odc.CreateTriggerPage.component.AdvancedInfoFrom.SelectAtLeastOneEvent": "請至少選擇一個事件", "workspace.window.function.propstab.params": "參數", @@ -552,6 +554,7 @@ "odc.src.store.sql.Compile": "編譯", "odc.ImportDrawer.ImportForm.ClickOrDragTheFile": "點擊或將文件拖拽到這裡上傳", "workspace.window.createTable.partition.value.listColumns.placelholder": "(欄位1,欄位2),(欄位1,欄位2)", + "workspace.window.sql.limit.placeholder": "1000", "workspace.window.recyclebin.button.purgeAll": "清空", "odc.component.GrammerHelpSider.SnippetslacksInTotal": "共{snippetsLength}條", "odc.helper.page.openPage.CreateATrigger": "建立觸發器", @@ -578,6 +581,7 @@ "odc.ImportDrawer.csvMapping.TargetFieldType": "目標欄位類型", "odc.components.PLPage.SavedSuccessfully": "儲存成功", "odc.TreeNodeMenu.config.view.ViewViewProperties": "查看視圖屬性", + "odc.components.ImportDrawer.SelectTheFieldsToBe": "請選擇需要映射的欄位", "odc.components.PLPage.statusBar.ManualTermination": "手動終止", "odc.components.TypePage.ModificationTime": "修改時間:", "odc.component.CreateTypeModal.Cancel": "取消", @@ -1370,6 +1374,7 @@ "odc.component.CommonTaskDetailModal.TaskFlow.Handler": "處理人", "odc.component.helpDoc.doc.TheApprovalTimesOutAnd": "審批逾時,任務將到期", "odc.component.DataTransferModal.ExecutionMethod": "執行方式", + "odc.TaskManagePage.component.TaskTools.InitiateAgain": "再次發起", "odc.component.CommonTaskDetailModal.TaskFlow.Handled": "可處理人", "odc.components.FormRecordExportModal.Cancel": "取消", "odc.component.OSSDragger2.Cancel": "取消", @@ -1544,6 +1549,7 @@ "odc.component.CommonTaskDetailModal.TaskFlow.InitiateATask": "發起任務", "odc.components.FormRecordExportModal.Export": "匯出", "odc.TaskManagePage.component.TaskTools.Reject": "拒絕", + "odc.TaskManagePage.component.TaskTools.Terminate": "終止", "odc.components.RecordPage.component.DetailedRules": "執行細則", "odc.component.DetailModal.dataMocker.RiskLevel": "風險等級", "odc.component.TaskStatus.ApprovalExpired": "審批已到期", @@ -1991,6 +1997,7 @@ "odc.components.CreateShadowSyncModal.Submit": "提交", "odc.StructConfigPanel.StructAnalysisResult.column.ExecutionResult": "執行結果", "odc.component.CommonTaskDetailModal.TaskRecord.View": "查看", + "odc.components.PartitionDrawer.Remarks": "備忘", "odc.component.AddConnectionForm.AddressItems.ClusterName": "集群名", "odc.AddConnectionForm.Account.PrivateAccount.TheEndOfTheAccount": "賬號首尾包含空格", "odc.components.SQLPage.BecauseTheSqlIsToo": "由於 SQL 過長,編輯器將只支持預覽", @@ -2227,6 +2234,7 @@ "odc.Content.ListItem.TenantTenant": "租戶: {tenant}", "odc.component.Crontab.const.Saturday": "周六", "odc.components.CreateSQLPlanTaskModal.Save": "儲存", + "odc.TaskManagePage.component.TaskTools.View": "查看", "odc.components.CreateSQLPlanTaskModal.CreateAnSqlPlan": "建立 SQL 計劃", "odc.components.RolePage.component.ResourceManagementPermissions": "資源系統管理權限", "odc.src.d.ts.TableGroup": "錶組", @@ -2795,6 +2803,7 @@ "odc.Project.User.DeletedSuccessfully": "刪除成功", "odc.SensitiveRule.components.CheckboxInput.PleaseEnter": "請輸入", "odc.Secure.Approval.ExecutionValidityPeriod": "執行有效期間", + "odc.AlterDdlTask.CreateModal.LockTableTimeout": "鎖表逾時時間", "odc.NewSSODrawerButton.SSOForm.ConfigurationName": "配置名稱", "odc.Datasource.Info.Delete": "刪除", "odc.components.SensitiveColumn.AreYouSureYouWant.1": "確認要刪除敏感列嗎?", @@ -2827,6 +2836,7 @@ "odc.NewSSODrawerButton.SSOForm.Type": "類型", "odc.component.FormModal.TitleName": "{title}名稱", "odc.ResourceTree.Nodes.package.Subprogram": "子程式", + "odc.AlterDdlTask.CreateModal.Seconds": "秒", "odc.Project.Database.CharacterEncoding": "字元編碼", "odc.page.Secure.RiskLevel": "風險等級", "odc.DataArchiveTask.DetailContent.TaskType": "任務類型", @@ -2861,6 +2871,7 @@ "odc.AlterDdlTask.DetailContent.SourceTableCleanupPolicyAfter": "完成後源表清理策略", "odc.SensitiveRule.components.DetectWay.Script": "指令碼", "odc.AlterDdlTask.CreateModal.AreYouSureYouWant": "是否確認取消無鎖結構變更?", + "odc.AlterDdlTask.DetailContent.LockTableTimeout": "鎖表逾時時間", "odc.Project.Setting.ProjectInformation": "專案資訊", "odc.Project.User.AreYouSureYouWant": "是否確定刪除該成員?", "odc.SensitiveRule.components.FormSensitiveRuleDrawer.AreYouSureYouWant.1": "是否確認取消建立?", @@ -3193,6 +3204,7 @@ "odc.AsyncTask.DetailContent.RollbackContent": "復原內容", "odc.NewSSODrawerButton.SSOForm.ObtainTheAccessTokenAddress": "授權伺服器提供的擷取 access-token 地址", "odc.ExternalIntegration.SSO.WhetherToEnable": "是否啟用", + "odc.AlterDdlTask.CreateModal.NumberOfFailedRetries": "失敗重試次數", "odc.Env.components.InnerEnvironment.ConfigurationValue": "配置值", "odc.Script.Snippet.Copy": "複製", "odc.DataArchiveTask.CreateModal.Ok": "確定", @@ -3258,6 +3270,7 @@ "odc.Database.AddDataBaseButton.BoundProject": "- 已繫結項目:", "odc.component.EditPLParamsModal.StatementCannotBeEmpty": "語句不可為空", "odc.component.FormModal.TheProcessNameCannotExceed": "流程名稱不超過 128 個字元", + "odc.component.helpDoc.doc.AfterTheTableLockTime": "超過鎖表時間後,未切換完成可自動重試", "odc.SensitiveRule.components.FormSensitiveRuleDrawer.UpdateFailed": "更新失敗", "odc.ExternalIntegration.SqlInterceptor.FollowTheSecuritySpecificationSql": "請按照 安全規範 - SQL 開發規範 - SQL 窗口規則 路徑,使用您已配置並開啟的 SQL 攔截集成", "odc.Env.components.EditRuleDrawer.ExecutionIsProhibitedAndApproval": "禁止執行,無法發起審批", @@ -3265,6 +3278,7 @@ "odc.component.CommonDetailModal.TaskExecuteRecord.DatabaseChanges": "數據庫變更", "odc.components.RecordPage.column.EnterADataSource": "請輸入所屬數據源", "odc.components.SQLResultSet.DBTimeline.SqlPostCheck": "SQL 後置檢查", + "odc.component.VersionModal.config.ThroughDataArchivingYouCan": "通過數據歸檔,您可以配置靈活的歸檔條件,實現冷熱數據分離。", "odc.Sider.SpaceSelect.Ok": "確定", "odc.Record.RecordPage.column.DataSource": "數據源", "odc.Record.RecordPage.interface.TransferProject": "轉移項目", @@ -3308,6 +3322,7 @@ "odc.components.SQLResultSet.DBTimeline.SqlPrecheck": "SQL 預檢查", "odc.component.DescriptionInput.EnterADescriptionLessThan": "請輸入描述,200字以內;未輸入時,系統會根據對象和工單類型自動生成描述信息", "odc.DataArchiveTask.CreateModal.ExecutionTime": "執行時間", + "odc.component.VersionModal.config.OdcProvidesDatabaseObjectManagement": "ODC 提供了數據庫對象管理、數據導入導出、SQL 編輯與執行、PL\n編譯與調試、數據生成、執行分析、數據庫運維等工具能力。", "odc.Env.components.EditRuleDrawer.AllowExecution": "允許執行", "odc.ResourceTree.Datasource.DeletedSuccessfully": "刪除成功", "odc.DataArchiveTask.CreateModal.VariableConfig.DeleteAVariable": "刪除變量", @@ -3756,14 +3771,17 @@ "odc.src.component.Task.component.CommonDetailModal.Nodes.HighRisk": "高風險", "odc.src.page.Workspace.components.SQLResultSet.TheCurrentSQLExistenceMust": "當前 SQL\n 存在必須改進項,請修改後再執行", "odc.src.component.Task.AlterDdlTask.CreateModal.LockUsers.1": "鎖定使用者", + "odc.src.component.Task.AlterDdlTask.CreateModal.1BeforePerformingThe": "1. 執行無鎖結構變更前請確保資料庫伺服器磁碟空間充足;", "odc.src.component.Task.DataClearTask.CreateModal.PleaseEnter": "請輸入", "odc.src.component.Task.component.CommonDetailModal.Nodes.TheNumberOf": ",預檢查處理\n SQL 條數超過最大限制,當前任務流程將按", "odc.src.page.Project.Project.CreateProject.SecurityAdministrator": "安全性系統管理員", "odc.src.component.Task.component.CommonDetailModal.Share": "分享", "odc.src.page.Workspace.components.SQLResultSet.SQLContentHasBeenModified": "SQL 內容已修改,已無法定位原問題行,請重新執行 SQL 陳述式或發起預檢查", "odc.src.component.Task.PartitionTask.DetailContent.Partition": "分區計劃", + "odc.src.component.Task.DataArchiveTask.CreateModal.PleaseEnter": "請輸入", "odc.src.component.Task.PartitionTask.DetailContent.RiskLevel": "風險等級", "odc.src.component.Task.component.CommonDetailModal.Nodes.GradeContinuesToAdvance": "等級繼續推進", + "odc.src.component.Task.DataArchiveTask.CreateModal.PleaseChoose.1": "請選擇", "odc.src.component.Task.PartitionTask.DetailContent.Remark": "備忘", "odc.src.page.Workspace.components.SQLResultSet.TheCurrentSQLNeedsApproval": "當前 SQL 存在需要審批項,請發起審批或修改後再執行", "odc.src.component.Task.PartitionTask.DetailContent.CreationTime": "建立時間", @@ -3772,7 +3790,10 @@ "odc.src.component.Task.component.CommonDetailModal.Replication": "複製成功", "odc.src.component.Task.DataClearTask.CreateModal.PleaseChoose": "請選擇", "odc.src.component.Task.DataClearTask.CreateModal.PleaseChoose.1": "請選擇", + "odc.src.component.Task.DataArchiveTask.CreateModal.PleaseChoose": "請選擇", "odc.src.component.Task.NoCurrentWorkOrderView": "無當前工單查看許可權", + "odc.src.component.Task.AlterDdlTask.CreateModal.2WhenCreatingA": "2. 建立工單選擇源表清理策略時建議選擇保留源表;", + "odc.src.component.Task.AlterDdlTask.CreateModal.3IfTheOB": "3. 表名切換之前會鎖定您指定的資料庫帳號,並關閉該帳號對應的會話。表名切換期間,鎖定帳號涉及應用將無法訪問資料庫,請勿在業務高峰期執行;", "src.component.Task.component.PartitionPolicyTable.F057FAAF": "順序遞增", "src.component.Task.StructureComparisonTask.DetailContent.E8DAF6BA": "操作", "src.page.Datasource.Datasource.NewDatasourceDrawer.Form.Account.445C8BBC": "默認", @@ -3783,6 +3804,7 @@ "src.component.Task.ApplyDatabasePermission.CreateModal.B0247EF7": "請輸入原因描述", "src.page.Project.User.ManageModal.B7377F46": "回收後不可撤回", "src.component.Task.StructureComparisonTask.CreateModal.A8C717F6": "取消", + "src.component.helpDoc.AEEC5916": "關於注意事項第 3 條,由您指定將要鎖定的賬號,是為了保障錶名切換期間數據一致性的同時盡可能降低對業務的影響。 請您確保指定賬號的準確性,若您未指定任何賬號,ODC 將不會進行任何賬號鎖定及 Kill Session 操作,切換期間數據的一致性將需要由您來保障。", "src.component.Task.ApplyDatabasePermission.DetailContent.2C812515": "創建人", "src.component.EditorToolBar.actions.3BDAC881": "運行當前語句", "src.component.Task.ApplyDatabasePermission.DetailContent.265A918A": "申請原因", @@ -3860,6 +3882,7 @@ "src.component.ODCSetting.config.56F5CB81": "Delimiter 設置", "src.component.Task.component.PartitionPolicyFormTable.7D6F23AE": "分區保留數量", "src.page.Workspace.components.SessionContextWrap.SessionSelect.38EA55F4": "項目:", + "src.component.Task.DataClearTask.DetailContent.D2882643": "是", "src.page.ExternalIntegration.SSO.NewSSODrawerButton.SSOForm.DBB95C01": "請輸入server", "src.page.Project.Notification.components.F80DF1C7": "飛書", "src.component.Task.StructureComparisonTask.DetailContent.3C7B0B00": "目標端數據庫", @@ -4009,6 +4032,7 @@ "src.component.Task.0B2B1D60": "手動執行", "src.page.Project.Notification.components.DFE43F9E": "請求方法", "src.component.helpDoc.EFADD11A": "在參與者的基礎上,同時可以管理敏感列", + "src.component.Task.StructureComparisonTask.CreateModal.52828286": "描述", "src.page.Project.User.ManageModal.UserAuthList.C3B2211E": "請輸入", "src.page.ExternalIntegration.SSO.NewSSODrawerButton.SSOForm.DC978DE4": "訪問密碼", "src.component.Task.ApplyDatabasePermission.CreateModal.28506030": "申請原因", @@ -4021,6 +4045,7 @@ "src.component.Task.component.PartitionPolicyTable.8BC770B0": "預創建數量", "src.component.Task.StructureComparisonTask.CreateModal.B4FAB9EC": "新建失敗", "src.component.ODCSetting.647A18AA": "確定要恢復默認設置嗎?", + "src.component.Task.DataClearTask.CreateModal.23542D89": "請選擇", "src.page.Project.Notification.components.BA80DE5A": "取消", "src.component.ODCSetting.config.80241964": "正常", "src.component.Task.component.PartitionPolicyFormTable.DF75EB9E": "如何配置", @@ -4051,6 +4076,7 @@ "src.page.Project.Notification.components.713896D2": "不超過", "src.component.Task.StructureComparisonTask.CreateModal.45DB3909": "新建結構比對", "src.component.Task.PartitionTask.CreateModal.E454F701": "忽略錯誤繼續任務", + "src.component.Task.StructureComparisonTask.CreateModal.67E284BD": "描述不超過 200 個字符", "src.page.Project.Notification.components.A50DD7D6": "測試消息發送成功!", "src.component.Task.9B79BD20": "自動執行", "src.page.ExternalIntegration.SSO.NewSSODrawerButton.SSOForm.2E3212A5": "搜索群組查詢時使用的起始點或基準點", @@ -4060,6 +4086,7 @@ "src.page.ExternalIntegration.SSO.NewSSODrawerButton.SSOForm.80683796": "用戶查詢的過濾條件,可以基於用戶的屬性(如用戶名、郵箱、部門等)來過濾搜索結果", "src.component.Task.DataMockerTask.CreateModal.2C3DF5A5": "新建模擬數據", "src.page.Project.User.ManageModal.UserAuthList.19A27247": "過期時間", + "src.component.Task.DataClearTask.DetailContent.834E7D89": "否", "src.component.Task.component.PartitionPolicyTable.D94AE82A": "創建方式", "src.page.Secure.Env.components.FF5B44FE": "編輯環境", "src.page.Login.components.LDAPModal.AA5AB5DA": "LDAP 密碼不能為空", @@ -4208,6 +4235,7 @@ "src.component.Task.AsyncTask.CreateModal.6EEFAEA6": "新建數據庫變更", "src.page.Project.Notification.components.95A710C5": "請求方法", "src.component.ODCSetting.config.1ACE7366": "修改此參數,將在 ODC 重啟後生效", + "src.component.Task.PartitionTask.CreateModal.026392ED": "請輸入描述,不超過200個字符;未輸入時,系統會根據對象和工單類型自動生成描述信息", "src.page.Project.User.ManageModal.UserAuthList.583E307F": "回收", "src.page.Secure.Env.65EAAB75": "確認刪除該環境麼?", "src.page.Login.components.LDAPModal.95DA8BD0": "LDAP 登錄", @@ -4228,6 +4256,7 @@ "src.page.Project.User.ManageModal.UserAuthList.8E0CB3F5": "數據庫", "src.page.Secure.Env.48529F6E": "全部環境", "src.component.Task.PartitionTask.CreateModal.0A49F493": "小時", + "src.component.Task.DataClearTask.CreateModal.99D8FCD6": "使用主鍵清理", "src.page.Project.Notification.components.F40AD99E": "Webhook地址", "src.page.ExternalIntegration.SSO.NewSSODrawerButton.SSOForm.5E8157A1": "次", "src.component.Task.component.PartitionPolicyTable.5C3A7795": "類型", @@ -4297,6 +4326,7 @@ "src.component.Task.StructureComparisonTask.DetailContent.575F8B39": "比對範圍", "src.component.Task.ApplyDatabasePermission.CreateModal.87E335B0": "1 年", "src.component.Task.component.PartitionPolicyFormTable.A9C95E9D": "當前表如果包含全局索引,刪除分區會導致全局索引失效,如果選擇重建全局索引可能耗時很久,請謹慎操作", + "src.component.Task.DataClearTask.DetailContent.2D1A14AB": "使用主鍵清理", "src.component.Task.component.TaskTable.80E1D16A": "結構比對", "src.page.Project.Notification.components.CA33D8AB": "通道名稱已存在", "src.page.Project.Notification.components.EA00E8BD": "請選擇通道類型", @@ -4868,6 +4898,7 @@ "src.component.SQLConfig.1A5CCA98": "SQL 執行", "src.page.Project.User.ManageModal.Table.TaskApplyList.EC52BFBC": "過期時間", "src.component.Task.ApplyTablePermission.CreateModal.E26609A0": "匯出", + "src.component.Task.ApplyTablePermission.CreateModal.BC4488C7": "永不過期", "src.component.Task.ApplyTablePermission.CreateModal.78B63403": "請選擇", "src.component.Task.ApplyTablePermission.CreateModal.8955ACFE": "暫無項目,請先", "src.component.Task.ApplyTablePermission.CreateModal.77D8F632": "請選擇項目", @@ -4936,7 +4967,12 @@ "src.page.Workspace.SideBar.ResourceTree.DatabaseSearchModal.components.FA5E6855": "定位到數據庫 \"{databaseName}\"", "src.page.Workspace.components.DDLResultSet.6477DD60": "集合 SQL 的執行詳情、物理執行計劃、全鏈路診斷的多維度視圖,幫助快速定位執行慢查詢的根因", "src.component.ExecuteSqlDetailModal.1882C007": "計劃統計", + "src.component.Task.component.ShardingStrategyItem.E5A6B481": "全表掃描", + "src.component.Task.component.ShardingStrategyItem.F91EEC6C": "條件匹配", + "src.component.Task.component.ShardingStrategyItem.3BD95C1A": "搜尋策略", "src.component.Task.component.ShardingStrategyItem.D5F45B7A": "請選擇搜索策略", + "src.component.Task.DataClearTask.DetailContent.E977DA21": "搜索策略", + "src.component.Task.DataArchiveTask.DetailContent.4844C10F": "搜尋策略", "src.page.Workspace.components.TablePage.ShowTableBaseInfoForm.56FAF24C": "邏輯表名稱", "src.page.Workspace.components.TablePage.ShowTableBaseInfoForm.C1C0A345": "邏輯表表達式", "src.page.Workspace.components.TablePage.ShowTableBaseInfoForm.200BCB34": "表拓撲", @@ -5615,6 +5651,7 @@ "src.page.Console.9B6E647E": "反饋建議", "src.page.Console.470D66DD": "OceanBase AP 實時 SQL 診斷能力解析", "src.page.Console.67004EA1": "ODC SQL 檢查自動辨識高風險操作", + "src.page.Console.4F744942": "快速整合 ODC 的企業級帳號體系", "src.page.Console.15BABD7A": "企業級管控協同:守護資料庫的每一次變更", "src.page.Console.A0F9F190": "透過 ODC 實現分庫分表的管理和變更", "src.page.Console.7BD8C2BB": "資料脫敏管控實踐", @@ -5674,6 +5711,7 @@ "src.component.Task.component.PartitionPolicyFormTable.E0F368F6": "數值", "src.component.Task.component.PartitionPolicyFormTable.053D6705": "日期時間", "src.component.Task.component.PartitionPolicyFormTable.FCD94271": "時間戳", + "src.component.Task.component.PartitionPolicyFormTable.9F024A60": "當前預創建分區數量過小,若調度失敗恐影響業務運行,建議調整預創建分區數量至2個及以上。", "src.component.Task.component.PartitionPolicyFormTable.E753A67D": "暫未設定創建策略,無 SQL 可預覽", "src.component.Task.component.PartitionPolicyFormTable.18397BFF": "分區名的生成方式", "src.component.Task.component.PartitionPolicyFormTable.044700A2": "分區名的生成規則,可引用變數。比如:concat('P_',${COL1}),其中 COL1 表示分區表的分區鍵。", @@ -5733,6 +5771,7 @@ "src.component.ODCSetting.B7CA4CA1": "請輸入查詢條數預設值", "src.component.ODCSetting.5127321B": "不超過 {sessionQueryLimit}", "src.component.ODCSetting.A8E22CC2": "不超過 {queryLimit}", + "src.component.ODCSetting.4C8CC366": "不超過 100000", "src.component.ODCSetting.DD41DED4": "請輸入查詢條數上限", "src.component.ODCSetting.6BCFD6DD": "使用者", "src.component.ODCSetting.47586FD4": "個人空間", @@ -5797,14 +5836,21 @@ "src.component.Task.component.PartitionPolicyFormTable.RuleFormItem.13B22678": "yyyy年MM月dd日 HH:mm:ss", "src.component.Task.LogicDatabaseAsyncTask.CreateModal.C53810BF": "邏輯庫變更內容暫不支援註解解析,編寫註解可能會導致 SQL 語句解析異常。", "src.component.CommonTable.2363A9C8": "僅展示週期執行任務", + "src.page.Workspace.components.DDLResultSet.Sync.38BDFD2B": "查看刷新記錄", "src.page.Auth.Role.component.FormModal.3D1B7861": "目前已授予的權限存在重疊。僅保留具有更大權限範圍的記錄。", + "src.d.ts.25D43093": "資料來源不存在", + "src.d.ts.CAC52ADB": "實例不存在", "src.d.ts.B89ABE6D": "類型不匹配", + "src.d.ts.7AE88D0A": "進口", "src.component.Task.component.TaskTable.D4FAED98": "匯入 {activeTaskLabel}", "src.component.Task.component.TaskTable.55FC08BB": "匯入", "src.component.Task.component.PartitionPolicyFormTable.B988E243": "當前預創建分區數量過小,若調度失敗恐影響業務運行,建議調整預創建分區數量至2個及以上。", "src.component.Task.component.ImportModal.997B6AC7": "排程任務匯入完成", + "src.component.Task.component.ImportModal.E256F212": "{resultLength} 工作已成功匯入。建議手動", "src.component.Task.component.ImportModal.8AD83BC8": "配置訊息通知", "src.component.Task.component.ImportModal.04E1F63D": "為了確保任務異常能及時被發現。", + "src.component.Task.component.ImportModal.E12ACFFB": "計劃任務匯出已完成", + "src.component.Task.component.ImportModal.47DA5967": "{successCount} 個工作成功匯入。", "src.component.Task.component.ImportModal.32698C46": "{failedCount} 個作業匯入失敗。建議手動", "src.component.Task.component.ImportModal.801362FE": "配置訊息通知", "src.component.Task.component.ImportModal.60663407": "確保任務異常能夠及時發現。有關匯出詳細資訊,", @@ -5822,6 +5868,8 @@ "src.component.Task.component.ImportModal.B6BCF0C0": "取消", "src.component.Task.component.ImportModal.2F42ECBC": "下一步:預覽", "src.component.Task.component.ImportModal.9FFF1F71": "先前:上傳檔案", + "src.component.Task.component.ImportModal.81026A36": "導入", + "src.component.Task.component.ImportModal.0C3395D0": "僅支援由阿里雲 OceanBase 研發部門匯出的配置檔案進行匯入。 \n請確認與該任務相關聯的實例已切換至 OceanBase Cloud 的當前專案。系統將根據實例資訊匹配或創建相應的資料來源。", "src.component.Task.component.ImportModal.D8DFA444": "請上傳", "src.component.Task.component.ImportModal.7B17FBF4": "檔案上傳失敗,原因是 {fileResponseDataErrMsg}", "src.component.Task.component.ImportModal.3CEE142A": "檔案金鑰", @@ -5830,14 +5878,30 @@ "src.component.Task.component.ImportModal.C6AC4C4C": "請輸入", "src.component.Task.component.ImportModal.889B80AD": "導入專案", "src.component.Task.component.ImportModal.30D6697A": "請選擇", + "src.component.Task.component.ImportModal.B3686BE9": "請確認相關數據源已添加到專案中。", "src.component.Task.component.ImportModal.1E47B6CE": "建立資料來源", "src.component.Task.component.ImportModal.E82DB6D1": "請選擇", "src.component.Task.component.ImportModal.68180A60": "創建項目", "src.component.Task.component.ImportModal.A8BC98CA": "數據源", "src.component.Task.component.ImportModal.0B1F9DCD": "數據庫", "src.component.Task.component.ImportModal.263EFA83": "類型", + "src.component.Task.component.ImportModal.40440087": "雲端供應商", + "src.component.Task.component.ImportModal.F2C95E45": "地域", + "src.component.Task.component.ImportModal.EFE4E0D0": "連接資訊", + "src.component.Task.component.ImportModal.4A911A47": "實例名稱", + "src.component.Task.component.ImportModal.396EF2AD": "租戶名稱", "src.component.Task.component.ImportModal.5928CAE8": "資料庫帳號", + "src.component.Task.component.ImportModal.B4E8467F": "源端數據庫", + "src.component.Task.component.ImportModal.50582618": "源端數據庫", + "src.component.Task.component.ImportModal.76F572C8": "目標端數據庫", + "src.component.Task.component.ImportModal.776397D6": "目標端數據庫", + "src.component.Task.component.ImportModal.B5C62BDC": "任務描述", + "src.component.Task.component.ImportModal.1EB75659": "數據庫", + "src.component.Task.component.ImportModal.8D781AC2": "數據庫", + "src.component.Task.component.ImportModal.5DE578E8": "原始編號", + "src.component.Task.component.ImportModal.2043ADA9": "原始專案", "src.component.Task.component.ImportModal.1BF95006": "沒有工作可匯入", + "src.component.Task.component.ImportModal.E8DE787E": "可匯入的", "src.component.Task.component.AsyncTaskOperationButton.D9D9A882": "請先選擇一張票。", "src.component.Task.component.AsyncTaskOperationButton.683E8B65": "取消", "src.component.Task.component.AsyncTaskOperationButton.2D7C7380": "我承認相關的商業風險。", @@ -5906,7 +5970,18 @@ "src.component.Task.component.AsyncTaskOperationButton.89CE28F7": "終止錯誤已發生", "src.component.Task.component.AsyncTaskOperationButton.08EE66D0": "終止任務", "src.component.Task.component.AsyncTaskOperationButton.5E6481EB": "{idsLength} 個任務正在終止中...", - "src.page.Workspace.components.DDLResultSet.Sync.38BDFD2B": "查看刷新記錄", + "src.component.Task.AlterDdlTask.CreateModal.A37F2729": "在表名切換前,鎖定原表禁寫。請確保您的資料庫符合以下條件:\n 1. OB 版本號 ≥ 4.2.5 且 < 4.3.0\n 2. 參數 enable_lock_priority 已設定為 true", + "src.component.Task.AlterDdlTask.CreateModal.8AC211D6": "在表名切換前,鎖定原表禁寫。請確保您的資料庫符合以下條件:\n1. OB 版本號 ≥ 4.2.5 且 < 4.3.0 ,且參數 enable_lock_priority 已設定為 true\n2. ODP 版本 ≥ 4.3.1 且已進行以下參數設定:\n alter proxyconfig set proxy_id=1;\n alter proxyconfig set client_session_id_version=2;\n alter proxyconfig set enable_single_leader_node_routing = false;", + "src.component.Task.AlterDdlTask.CreateModal.D1732875": "注意:執行無鎖結構變更前,請確保資料庫伺服器磁碟空間充足。", + "src.component.Task.AlterDdlTask.CreateModal.05BCC428": "表名切換禁寫策略", + "src.component.Task.AlterDdlTask.CreateModal.B0CFE0E1": "在表名切換前,系統將鎖定您指定的資料庫帳號,並終止該帳號的當前會話。切換期間,所有涉及該帳號的應用將無法訪問資料庫。建議避免在業務高峰期執行此操作,以減少對業務的影響。", + "src.component.Task.AlterDdlTask.CreateModal.D056FD5E": "鎖定使用者", + "src.component.Task.AlterDdlTask.CreateModal.EA0D82E0": "鎖定表", + "src.component.Task.AlterDdlTask.CreateModal.125DF508": "推薦", + "src.component.ODCSetting.2BE8F5C3": "不超過{maxResultsetRows}", + "src.component.Task.AlterDdlTask.CreateModal.44A394CE": "在表名切換前,鎖定原表禁寫。", + "src.component.Task.AlterDdlTask.CreateModal.6DC73E39": "在表名切換前,鎖定原表禁寫。請您確保 ODP 滿足以下條件:\nODP 版本 ≥ 4.3.1 且已進行以下參數設定:\n alter proxyconfig set proxy_id=1;\n alter proxyconfig set client_session_id_version=2;\n alter proxyconfig set enable_single_leader_node_routing = false;", + "src.component.Task.AlterDdlTask.CreateModal.C62A7DBB": "需要將參數 enable_lock_priority 設置為 true", "src.component.Task.component.ActionBar.797981FE": "無可查看的結果資訊", "src.component.Task.component.ActionBar.5218D741": "查詢結果", "src.component.ODCSetting.9651E837": "不超過{maxResultsetRows}", @@ -5919,71 +5994,8 @@ "src.component.ODCSetting.config.AB482BC7": "支援查看查詢結果", "src.component.ODCSetting.config.3FE1F2A7": "支援下載查詢結果", "src.component.ODCSetting.config.1ED38C41": "資料庫變更查詢結果", - "src.component.Task.AlterDdlTask.CreateModal.D1732875": "注意:執行無鎖結構變更前,請確保資料庫伺服器磁碟空間充足。", - "src.component.Task.AlterDdlTask.CreateModal.05BCC428": "表名切換禁寫策略", - "src.component.Task.AlterDdlTask.CreateModal.B0CFE0E1": "在表名切換前,系統將鎖定您指定的資料庫帳號,並終止該帳號的當前會話。切換期間,所有涉及該帳號的應用將無法訪問資料庫。建議避免在業務高峰期執行此操作,以減少對業務的影響。", - "src.component.Task.AlterDdlTask.CreateModal.D056FD5E": "鎖定使用者", - "src.component.Task.AlterDdlTask.CreateModal.EA0D82E0": "鎖定表", - "src.component.Task.AlterDdlTask.CreateModal.125DF508": "推薦", - "src.component.Task.AlterDdlTask.CreateModal.44A394CE": "在表名切換前,鎖定原表禁寫。", - "src.component.Task.AlterDdlTask.CreateModal.6DC73E39": "在表名切換前,鎖定原表禁寫。請您確保 ODP 滿足以下條件:\nODP 版本 ≥ 4.3.1 且已進行以下參數設定:\n alter proxyconfig set proxy_id=1;\n alter proxyconfig set client_session_id_version=2;\n alter proxyconfig set enable_single_leader_node_routing = false;", - "src.component.Task.AlterDdlTask.CreateModal.C62A7DBB": "需要將參數 enable_lock_priority 設置為 true", "src.page.Console.8F636FA5": "帶您進入 ODC 團隊空間的美妙世界", "src.component.helpDoc.7F2FDC2E": "若鎖定失敗,可自動重試次數", "src.component.Task.AlterDdlTask.CreateModal.1A8336C1": "鎖定失敗重試次數", - "src.component.Task.AlterDdlTask.CreateModal.A56878AD": "請輸入鎖定失敗重試次數", - "src.page.Workspace.components.SessionContextWrap.SessionSelect.SessionDropdown.49B355DA": "管理資料庫", - "src.page.Console.components.RecentlyDatabase.806E294C": "對象存儲目前不支援開啟 SQL 控制台。", - "src.d.ts.BE8F2539": "已存在。", - "src.component.Task.component.TaskTable.E20D58A0": "個人空間任務僅支援本人管理和查看,且不提供警告配置功能,因此不建議將其用於對生產業務有強烈依賴的場景。如需在生產業務中使用此功能,請優先選擇團隊空間,並配置相應的訊息通知機制,以保障業務的正常運作。", - "src.component.Task.component.ImportModal.A6892359": "(包含 {importedCount} 個已匯入的任務)", - "src.component.Task.component.ImportModal.AF106FF6": "{resultLength} 個作業匯入成功{importedDescription}。", - "src.component.Task.component.ImportModal.97A1A7A3": "{resultLength} 個作業匯入成功{importedDescription}。", - "src.component.Task.component.ImportModal.1B653B27": "建議手動執行任務", - "src.component.Task.component.ImportModal.92D087FB": "(包含 {importedCount} 個已匯入的任務)", - "src.component.Task.component.ImportModal.94BFD01C": "{successCount} 個作業匯入成功{importedDescription}, {failedCount} 個作業匯入失敗。 可", - "src.component.Task.component.ImportModal.AE88E265": "{successCount} 個作業導入成功{importedDescription},", - "src.component.Task.component.ImportModal.7C3EB72C": "工單", - "src.component.Task.component.ImportModal.DCD1BA1A": "原資料庫", - "src.component.Task.component.ImportModal.93C2FD0F": "原資料庫", - "src.component.Task.component.ImportModal.A8B5EF34": "數據源:", - "src.component.Task.component.ImportModal.4C0466BB": "原資料庫", - "src.component.Task.component.ImportModal.E78A5A7E": "源端數據庫", - "src.component.Task.component.ImportModal.47F8A3BA": "目標端數據庫", - "src.component.Task.component.ImportModal.55E87B63": "原狀態", - "src.component.Task.component.ImportModal.DFCD4ABB": "工單導入成功後將重新啟用", - "src.component.Task.component.ImportModal.37A63CCA": "類型", - "src.component.Task.component.ImportModal.D22149A7": "新源端", - "src.component.Task.component.ImportModal.6992AD58": "新目標端", - "src.component.Task.component.ImportModal.01BA659E": "新資料庫", - "src.component.Task.component.ImportModal.EA6397CD": "我已確認導入的工單新舊資料庫物件一致。", - "src.component.Task.component.ImportModal.E756EF6A": "導入 ({selectedRowKeysLength})", - "src.component.Task.component.ImportModal.A28A2A00": "僅支持匯入由 阿里雲 OceanBase 數據研發 或 ODC 匯出的配置檔;在匯入之前,請先添加相關資料來源,並指定對應的專案。", - "src.component.Task.component.ImportModal.C264F2F5": "勾選需要匯入的工單,匯入後將重新啟用。匯入前請檢查涉及的新舊資料庫物件是否一致,否則匯入或執行時可能會出現失敗。", - "src.component.Task.component.ImportModal.3137AA28": "僅顯示已選擇資料庫的工單", - "src.component.Task.component.ImportModal.F8B503DB": "以下工單已導入,無需重複操作。", - "src.component.Task.component.ImportModal.49FDDB00": "以下工單類型不匹配,無法導入。建議您選擇對應的工單類型重新導入。", - "src.component.Task.component.ImportModal.F42820DF": "待導入", - "src.component.Task.component.ImportModal.4559F373": "主機 IP/域名:", - "src.component.Task.component.ImportModal.FF8162DA": "連接埠", - "src.component.Task.component.ImportModal.825A80AC": "請選擇新的資料庫", - "src.component.Task.component.ExecuteFailTip.BE7C5B92": "週期執行期間若資料庫連線失敗或專案不存在,可能導致任務執行失敗;30 天內連續調度失敗,任務將自動終止。", - "src.page.Workspace.components.SQLResultSet.36FAD6DF": "關閉該結果集", - "src.page.Workspace.components.SQLResultSet.A460FEDE": "關閉其他結果集", - "src.page.Workspace.components.SQLResultSet.989D6FA1": "關閉所有結果集", - "src.component.helpDoc.7A52D62F": "檢索目標記錄行的方式", - "src.component.ODCSetting.config.user.20C5DFB4": "結果集顯示", - "src.component.ODCSetting.config.user.2D4F8470": "最近的查詢結果會覆蓋上一次的查詢結果,若要查看多次執行的結果,請手動固定結果集(在結果集頁籤右鍵操作)。", - "src.component.ODCSetting.config.user.C8FE70F6": "覆蓋", - "src.component.ODCSetting.config.user.FACA6552": "多次查詢結果追加展示,需手動關閉結果集頁籤才會關閉。", - "src.component.ODCSetting.config.user.5BCACB4C": "追加", - "src.component.Task.component.Status.F318D350": "執行異常", - "src.component.Task.component.ShardingStrategyItem.B6EB2F37": "若使用全表掃描方式進行資料搜尋,處理過程將更加穩定,但性能可能會受到影響。", - "src.component.Task.component.ShardingStrategyItem.61BD0252": "透過全表掃描進行資料搜尋", - "src.component.Task.modals.DataClearTask.DetailContent.E35B6945": "透過全表掃描進行資料搜尋", - "src.component.Task.modals.DataClearTask.DetailContent.A75CE867": "是", - "src.component.Task.modals.DataClearTask.DetailContent.CB4C9BFB": "否", - "src.component.Task.modals.DataArchiveTask.DetailContent.44776A6F": "透過全表掃描進行資料搜尋", - "src.component.Task.modals.DataArchiveTask.DetailContent.D405684B": "是", - "src.component.Task.modals.DataArchiveTask.DetailContent.53EB9D22": "否" + "src.component.Task.AlterDdlTask.CreateModal.A56878AD": "請輸入鎖定失敗重試次數" } diff --git a/src/page/Project/Sensitive/components/SensitiveColumn/components/SacnRule.tsx b/src/page/Project/Sensitive/components/SensitiveColumn/components/SacnRule.tsx index dd1cdf120..1cfd6f3cc 100644 --- a/src/page/Project/Sensitive/components/SensitiveColumn/components/SacnRule.tsx +++ b/src/page/Project/Sensitive/components/SensitiveColumn/components/SacnRule.tsx @@ -18,27 +18,60 @@ import { getDataSourceStyleByConnectType } from '@/common/datasource'; import { getConnectionList } from '@/common/network/connection'; import { listSensitiveRules } from '@/common/network/sensitiveRule'; import ConnectionPopover from '@/component/ConnectionPopover'; -import { IConnection, IResponseData } from '@/d.ts'; +import { IConnection, IResponseData, ConnectType } from '@/d.ts'; +import { SensitiveRuleType } from '@/d.ts/sensitiveRule'; import ProjectContext from '@/page/Project/ProjectContext'; import { SelectItemProps } from '@/page/Project/Sensitive/interface'; import { formatMessage } from '@/util/intl'; import Icon from '@ant-design/icons'; -import { Button, Divider, Form, Popover, Select } from 'antd'; +import { Button, Divider, Form, Popover, Radio, Select } from 'antd'; import { useWatch } from 'antd/es/form/Form'; import { useContext, useEffect, useState } from 'react'; import SensitiveContext from '../../../SensitiveContext'; import MultipleDatabaseSelect from '@/component/Task/component/MultipleDatabaseSelect/index'; import { isConnectTypeBeFileSystemGroup } from '@/util/connection'; +import { isAIAvailable } from '@/common/network/ai'; const ScanRule = ({ formRef, reset, setManageSensitiveRuleDrawerOpen }) => { const context = useContext(ProjectContext); const sensitiveContext = useContext(SensitiveContext); const [dataSourceId, setDataSourceId] = useState(-1); const databaseIds = useWatch('databaseIds', formRef); + const scanningMode = useWatch('scanningMode', formRef); + const connectionId = useWatch('connectionId', formRef); const [selectOpen, setSelectOpen] = useState(false); const [dataSourceOptions, setDataSourceOptions] = useState([]); const [sensitiveOptions, setSensitiveOptions] = useState([]); const [rawData, setRawData] = useState>(); + const [aiAvailable, setAiAvailable] = useState(true); + // 检查AI功能状态 + useEffect(() => { + const checkAIStatus = async () => { + try { + const available = await isAIAvailable(); + setAiAvailable(available); + + // 如果AI不可用且当前选择的是AI增强识别,自动切换到传统规则识别 + if (!available && scanningMode === 'JOINT_RECOGNITION') { + await formRef.setFieldsValue({ + scanningMode: 'RULES_ONLY', + }); + } + } catch (error) { + console.warn('检查AI状态失败:', error); + setAiAvailable(false); + + // AI检查失败时也切换到传统规则识别 + if (scanningMode === 'JOINT_RECOGNITION') { + await formRef.setFieldsValue({ + scanningMode: 'RULES_ONLY', + }); + } + } + }; + checkAIStatus(); + }, []); + const initDataSources = async () => { const rawData = await getConnectionList({ projectId: sensitiveContext.projectId, @@ -58,9 +91,13 @@ const ScanRule = ({ formRef, reset, setManageSensitiveRuleDrawerOpen }) => { const rawData = await listSensitiveRules(projectId, { enabled: [true], }); + + // 根据扫描模式禁用相应的规则类型 const resData = rawData?.contents?.map((content) => ({ label: content.name, value: content.id, + disabled: scanningMode === 'RULES_ONLY' && content.type === SensitiveRuleType.AI, + type: content.type, // 保存类型信息用于后续判断 })); setSensitiveOptions( resData?.length > 0 @@ -104,10 +141,36 @@ const ScanRule = ({ formRef, reset, setManageSensitiveRuleDrawerOpen }) => { } reset(); }; + + const handleScanningModeChange = async () => { + // 重新加载规则列表 + await initDetectRules(); + + const currentValues = formRef.getFieldValue('sensitiveRuleIds') || []; + let filteredValues = currentValues; + + // 如果切换到传统规则识别模式,需要移除已选择的AI规则 + if (scanningMode === 'RULES_ONLY') { + filteredValues = currentValues.filter((id) => { + const option = sensitiveOptions.find((opt) => opt.value === id); + return option && (!option.type || option.type !== SensitiveRuleType.AI); + }); + } + + if (filteredValues.length !== currentValues.length) { + await formRef.setFieldsValue({ + sensitiveRuleIds: filteredValues, + }); + } + + reset(); + }; + useEffect(() => { initDataSources(); initDetectRules(); }, []); + useEffect(() => { if (dataSourceId !== -1) { formRef.setFieldsValue({ @@ -117,163 +180,236 @@ const ScanRule = ({ formRef, reset, setManageSensitiveRuleDrawerOpen }) => { } }, [dataSourceId]); + useEffect(() => { + if (scanningMode) { + handleScanningModeChange(); + } + }, [scanningMode]); + + useEffect(() => { + if (connectionId) { + setDataSourceId(connectionId); + } else { + setDataSourceId(-1); + } + }, [connectionId]); + return ( -
- + {/* 第一行:数据源和数据库选择 */} +
- - - - - + + { - initDetectRules(); - setSelectOpen(visible); - }} - dropdownRender={(menu) => ( - <> - {menu} - - - - - )} + dataSourceId={dataSourceId === -1 ? undefined : dataSourceId} + projectId={context.projectId} + onSelect={handleSelect} + disabled={dataSourceId === -1} + isAdaptiveWidth /> - +
+ + {/* 第二行:扫描模式 */} +
+ + + + {formatMessage({ + id: 'odc.SensitiveColumn.components.SacnRule.TraditionalRules', + defaultMessage: '传统规则识别', + })} + + + {formatMessage({ + id: 'odc.SensitiveColumn.components.SacnRule.AIEnhanced', + defaultMessage: 'AI增强识别', + })} + {!aiAvailable && (AI功能未开启)} + + + +
+ + {/* 第三行:识别规则 */} +
+ + {formatMessage({ + id: 'odc.SensitiveColumn.components.SacnRule.YouCanUseThePath', + defaultMessage: '可选择路径、正则、Groovy和AI识别方式', + })} +
+ {formatMessage({ + id: 'odc.SensitiveColumn.components.SacnRule.AIPerformanceNote', + defaultMessage: '性能提示:建议适量使用AI规则', + })} +
+
+ } + name="sensitiveRuleIds" + rules={[ + { + required: true, + message: formatMessage({ + id: 'odc.SensitiveColumn.components.SacnRule.SelectAnIdentificationRule', + defaultMessage: '请选择识别规则', + }), //请选择识别规则 + }, + ]} + > + setInputValue(e.target.value)} + onBlur={handleInputConfirm} + onPressEnter={handleInputConfirm} + placeholder={formatMessage({ + id: 'odc.SensitiveRule.components.DetectWay.CustomTypeNamePlaceholder', + defaultMessage: '输入自定义类别名称', + })} + maxLength={20} + autoFocus + /> + ) : ( + + )} +
+ + {/* 已选择的类别显示区域 */} + {selectedTypes.length > 0 && ( +
+
+ {formatMessage( + { + id: 'odc.SensitiveRule.components.DetectWay.SelectedTypesCount', + defaultMessage: '已选择({total}个,其中自定义{custom}/10):', + }, + { + total: selectedTypes.length, + custom: selectedTypes.filter( + (type) => + !predefinedTypes.some((predefined) => predefined.value === type), + ).length, + }, + )} +
+
+ {selectedTypes.map((tag) => { + const predefinedType = predefinedTypes.find((t) => t.value === tag); + const displayName = predefinedType ? predefinedType.label : tag; + return ( + handleTagClose(tag)} + color={predefinedType ? 'blue' : 'green'} + > + {displayName} + + ); + })} +
+
+ )} + + + + +
+ {formatMessage({ + id: 'odc.SensitiveRule.components.DetectWay.CustomPromptDescription', + defaultMessage: + '用于补充描述特殊的敏感字段识别场景,帮助AI更准确地识别敏感数据', + })} +
+
+ {formatMessage({ + id: 'odc.SensitiveRule.components.DetectWay.UsageSuggestions', + defaultMessage: '使用建议:', + })} +
+
+ •{' '} + {formatMessage({ + id: 'odc.SensitiveRule.components.DetectWay.DescribeBusinessMeaning', + defaultMessage: '描述字段的业务含义和敏感特征', + })} +
+
+ •{' '} + {formatMessage({ + id: 'odc.SensitiveRule.components.DetectWay.ProvideFieldPatterns', + defaultMessage: '提供具体的字段名称模式或数据格式', + })} +
+
+ •{' '} + {formatMessage({ + id: 'odc.SensitiveRule.components.DetectWay.AvoidIrrelevantContent', + defaultMessage: '避免输入无关或过于宽泛的内容', + })} +
+
+ {formatMessage({ + id: 'odc.SensitiveRule.components.DetectWay.PromptQualityNote', + defaultMessage: '注意:提示词质量直接影响识别准确性', + })} +
+ + } + name="aiCustomPrompt" + rules={[ + { + max: 100, + message: formatMessage({ + id: 'odc.SensitiveRule.components.DetectWay.CustomPromptLengthError', + defaultMessage: '自定义提示词不能超过100字', + }), + }, + ]} + > + +
+ + )} ); diff --git a/src/page/Project/Sensitive/components/SensitiveRule/components/FormSensitiveRuleDrawer.tsx b/src/page/Project/Sensitive/components/SensitiveRule/components/FormSensitiveRuleDrawer.tsx index 406bb9b80..9a221f080 100644 --- a/src/page/Project/Sensitive/components/SensitiveRule/components/FormSensitiveRuleDrawer.tsx +++ b/src/page/Project/Sensitive/components/SensitiveRule/components/FormSensitiveRuleDrawer.tsx @@ -31,116 +31,170 @@ const FormSensitiveRuleDrawer = ({ handleFormDrawerClose, isEdit, selectedRecord, + projectId, }) => { const [formRef] = useForm(); const context = useContext(ProjectContext); const sensitiveContext = useContext(SensitiveContext); const [script, setScript] = useState(''); const [hasValidated, setHasValidated] = useState(false); + const [currentType, setCurrentType] = useState(SensitiveRuleType.PATH); const handleSubmit = async () => { - const rawData = await formRef.validateFields().catch(); - const { enabled, maskingAlgorithmId, name, type, regExp = {}, description } = rawData; - if (type === SensitiveRuleType.GROOVY && script?.length === 0) { - setHasValidated(true); - return; - } - let data: Partial = { - enabled, - maskingAlgorithmId, - name, - type, - projectId: context.projectId, - description, - }; - const wrapPath = (origin: string) => { - if (origin?.includes(',')) { - return origin?.split(',')?.map((o) => o.trim()); - } - return origin === '' ? [] : [origin]; - }; - switch (type) { - case SensitiveRuleType.PATH: { - data = { - ...data, - pathIncludes: wrapPath( - Array.isArray(rawData.pathIncludes) - ? rawData?.pathIncludes?.join(',') - : rawData.pathIncludes, - ), - pathExcludes: wrapPath( - Array.isArray(rawData.pathExcludes) - ? rawData?.pathExcludes?.join(',') - : rawData.pathExcludes, - ), - }; - break; - } - case SensitiveRuleType.REGEX: { - const resRegExp = {}; - Object.keys(regExp)?.forEach((key) => { - if (regExp?.[key]?.checked?.length > 0) { - resRegExp[`${key}`] = regExp[key].regExp; - } - }); - data = { - ...data, - ...resRegExp, - }; - break; + try { + const rawData = await formRef.validateFields(); + const { enabled, maskingAlgorithmId, name, type, regExp = {}, description } = rawData; + if (type === SensitiveRuleType.GROOVY && script?.length === 0) { + setHasValidated(true); + return; } - case SensitiveRuleType.GROOVY: { + let data: Partial = { + enabled, + name, + type, + projectId: projectId, + description, + }; + + // 在编辑模式下,保留原有记录的所有字段 + if (isEdit && selectedRecord) { data = { - ...data, - groovyScript: script, + ...selectedRecord, // 保留原有字段 + enabled, + name, + type, + description, + // 清空其他类型的字段,避免冲突 + pathIncludes: [], + pathExcludes: [], + databaseRegexExpression: '', + tableRegexExpression: '', + columnRegexExpression: '', + columnCommentRegexExpression: '', + groovyScript: '', + aiSensitiveTypes: [], + aiCustomPrompt: '', }; - break; } - } - if (isEdit) { - const result = await updateSensitiveRule( - context.projectId, - selectedRecord.id, - data as ISensitiveRule, - ); - if (result) { - message.success( - formatMessage({ - id: 'odc.SensitiveRule.components.FormSensitiveRuleDrawer.UpdatedSuccessfully', - defaultMessage: '更新成功', - }), //更新成功 - ); - handleFormDrawerClose(formRef.resetFields); + // AI类型使用默认脱敏算法 + if (type === SensitiveRuleType.AI) { + // 为AI类型设置默认脱敏算法(取第一个可用的算法) + const defaultValue = sensitiveContext?.maskingAlgorithmOptions?.[0]?.value; + data.maskingAlgorithmId = defaultValue ? Number(defaultValue) : null; } else { - message.error( - formatMessage({ - id: 'odc.SensitiveRule.components.FormSensitiveRuleDrawer.UpdateFailed', - defaultMessage: '更新失败', - }), //更新失败 - ); + data.maskingAlgorithmId = maskingAlgorithmId; } - } else { - const result = await createSensitiveRule(context.projectId, data); - if (result) { - message.success( - formatMessage({ - id: 'odc.SensitiveRule.components.FormSensitiveRuleDrawer.New', - defaultMessage: '新建成功', - }), //新建成功 + const wrapPath = (origin: string) => { + if (origin?.includes(',')) { + return origin?.split(',')?.map((o) => o.trim()); + } + return origin === '' ? [] : [origin]; + }; + switch (type) { + case SensitiveRuleType.PATH: { + data = { + ...data, + pathIncludes: wrapPath( + Array.isArray(rawData.pathIncludes) + ? rawData?.pathIncludes?.join(',') + : rawData.pathIncludes, + ), + pathExcludes: wrapPath( + Array.isArray(rawData.pathExcludes) + ? rawData?.pathExcludes?.join(',') + : rawData.pathExcludes, + ), + }; + break; + } + case SensitiveRuleType.REGEX: { + const resRegExp = {}; + Object.keys(regExp)?.forEach((key) => { + if (regExp?.[key]?.checked?.length > 0) { + resRegExp[`${key}`] = regExp[key].regExp; + } + }); + data = { + ...data, + ...resRegExp, + }; + break; + } + case SensitiveRuleType.GROOVY: { + data = { + ...data, + groovyScript: script, + }; + break; + } + case SensitiveRuleType.AI: { + data = { + ...data, + aiSensitiveTypes: rawData.aiSensitiveTypes || [], + aiCustomPrompt: rawData.aiCustomPrompt || '', + }; + break; + } + } + if (isEdit) { + console.log('提交编辑数据:', { + projectId, + selectedRecordId: selectedRecord.id, + data, + }); + + const result = await updateSensitiveRule( + projectId, + selectedRecord.id, + data as ISensitiveRule, ); - handleFormDrawerClose(); + console.log('API调用结果:', result); + + if (result) { + message.success( + formatMessage({ + id: 'odc.SensitiveRule.components.FormSensitiveRuleDrawer.UpdatedSuccessfully', + defaultMessage: '更新成功', + }), //更新成功 + ); + + handleFormDrawerClose(formRef.resetFields); + } else { + message.error( + formatMessage({ + id: 'odc.SensitiveRule.components.FormSensitiveRuleDrawer.UpdateFailed', + defaultMessage: '更新失败', + }), //更新失败 + ); + } } else { - message.error( - formatMessage({ - id: 'odc.SensitiveRule.components.FormSensitiveRuleDrawer.FailedToCreate', - defaultMessage: '新建失败', - }), //新建失败 - ); + const result = await createSensitiveRule(projectId, data); + if (result) { + message.success( + formatMessage({ + id: 'odc.SensitiveRule.components.FormSensitiveRuleDrawer.New', + defaultMessage: '新建成功', + }), //新建成功 + ); + + handleFormDrawerClose(); + } else { + message.error( + formatMessage({ + id: 'odc.SensitiveRule.components.FormSensitiveRuleDrawer.FailedToCreate', + defaultMessage: '新建失败', + }), //新建失败 + ); + } } - } - setHasValidated(false); + setHasValidated(false); + } catch (error) { + console.error('表单验证或提交失败:', error); + // 如果是表单验证失败,不显示错误消息,让表单自己处理 + // 如果是其他错误,可以在这里处理 + } }; const onCancel = () => { return Modal.confirm({ @@ -173,7 +227,11 @@ const FormSensitiveRuleDrawer = ({ }; useEffect(() => { - if (isEdit) { + if (!formDrawerVisible) { + return; + } + + if (isEdit && selectedRecord) { const { name, enabled, @@ -187,12 +245,15 @@ const FormSensitiveRuleDrawer = ({ columnRegexExpression = '', columnCommentRegexExpression = '', description = '', + aiSensitiveTypes = [], + aiCustomPrompt = '', } = selectedRecord; const hasDatabaseRegexExpression = !!databaseRegexExpression; const hasTableRegexExpression = !!tableRegexExpression; const hasColumnRegexExpression = !!columnRegexExpression; const hasColumnCommentRegexExpression = !!columnCommentRegexExpression; - setScript(groovyScript); + setScript(groovyScript || ''); + setCurrentType(SensitiveRuleType[type]); formRef.setFieldsValue({ name, enabled, @@ -240,9 +301,13 @@ const FormSensitiveRuleDrawer = ({ }, maskingAlgorithmId: maskingAlgorithmId, description, + aiSensitiveTypes, + aiCustomPrompt, }); - } else { + } else if (!isEdit) { + // 新建模式下重置表单 setScript(''); + setCurrentType(SensitiveRuleType.PATH); formRef.setFieldsValue({ name: undefined, enabled: true, @@ -290,9 +355,11 @@ const FormSensitiveRuleDrawer = ({ }, maskingAlgorithmId: undefined, description: '', + aiSensitiveTypes: [], + aiCustomPrompt: '', }); } - }, [formDrawerVisible, isEdit, selectedRecord]); + }, [isEdit, selectedRecord, formDrawerVisible]); return ( - - - + + + )} { ); } + case SensitiveRuleType.AI: { + const { aiSensitiveTypes = [], aiCustomPrompt = '' } = params; + + // 敏感类别映射表 + const sensitiveTypeMap = { + 'personal-name-chinese': formatMessage({ + id: 'odc.SensitiveRule.components.DetectWay.PersonalNameChinese', + defaultMessage: '个人姓名(汉字类型)', + }), + 'personal-name-alphabet': formatMessage({ + id: 'odc.SensitiveRule.components.DetectWay.PersonalNameAlphabet', + defaultMessage: '个人姓名(字母类型)', + }), + nickname: formatMessage({ + id: 'odc.SensitiveRule.components.DetectWay.Nickname', + defaultMessage: '昵称', + }), + email: formatMessage({ + id: 'odc.SensitiveRule.components.DetectWay.Email', + defaultMessage: '邮箱', + }), + address: formatMessage({ + id: 'odc.SensitiveRule.components.DetectWay.Address', + defaultMessage: '地址', + }), + 'phone-number': formatMessage({ + id: 'odc.SensitiveRule.components.DetectWay.PhoneNumber', + defaultMessage: '手机号码', + }), + 'fixed-line-phone-number': formatMessage({ + id: 'odc.SensitiveRule.components.DetectWay.FixedLinePhoneNumber', + defaultMessage: '固定电话', + }), + 'certificate-number': formatMessage({ + id: 'odc.SensitiveRule.components.DetectWay.CertificateNumber', + defaultMessage: '证件号码', + }), + 'bank-card-number': formatMessage({ + id: 'odc.SensitiveRule.components.DetectWay.BankCardNumber', + defaultMessage: '银行卡号', + }), + 'license-plate-number': formatMessage({ + id: 'odc.SensitiveRule.components.DetectWay.LicensePlateNumber', + defaultMessage: '车牌号', + }), + 'device-id': formatMessage({ + id: 'odc.SensitiveRule.components.DetectWay.DeviceId', + defaultMessage: '设备唯一识别号', + }), + ip: formatMessage({ + id: 'odc.SensitiveRule.components.DetectWay.IpAddress', + defaultMessage: 'IP 地址', + }), + mac: formatMessage({ + id: 'odc.SensitiveRule.components.DetectWay.MacAddress', + defaultMessage: 'MAC 地址', + }), + }; + + const displayTypes = aiSensitiveTypes + ?.map((type) => sensitiveTypeMap[type] || type) + .join(', '); + + return ( + <> + + {displayTypes || '-'} + + + {aiCustomPrompt && ( + +
+ {aiCustomPrompt} +
+
+ )} + + ); + } } }; const ViewSensitiveRuleDrawer = ({ @@ -337,7 +436,9 @@ const ViewSensitiveRuleDrawer = ({ }) //脱敏算法 } > - {maskingAlgorithmIdMap[record?.maskingAlgorithmId]} + {record?.type === SensitiveRuleType.AI + ? '-' + : maskingAlgorithmIdMap[record?.maskingAlgorithmId]} { @@ -149,6 +157,11 @@ const getColumns: (columnsFunction: { }; }, render: (text, record, index) => { + // AI规则的脱敏算法显示为"-" + if (record?.type === SensitiveRuleType.AI) { + return ; + } + const target = maskingAlgorithms?.find( (maskingAlgorithm) => maskingAlgorithm?.id === record?.maskingAlgorithmId, ); @@ -460,6 +473,7 @@ const SensitiveRule = ({ projectId }) => { selectedRecord, formDrawerVisible, handleFormDrawerClose, + projectId, }} /> diff --git a/src/page/Project/Sensitive/interface.ts b/src/page/Project/Sensitive/interface.ts index 7e6cc6762..592d83ded 100644 --- a/src/page/Project/Sensitive/interface.ts +++ b/src/page/Project/Sensitive/interface.ts @@ -16,6 +16,7 @@ import { ConnectType } from '@/d.ts'; import { ESensitiveColumnType } from '@/d.ts/sensitiveColumn'; +import { SensitiveRuleType } from '@/d.ts/sensitiveRule'; import { formatMessage } from '@/util/intl'; export enum AddSensitiveColumnType { Manual, @@ -24,7 +25,7 @@ export enum AddSensitiveColumnType { export interface SelectItemProps { label: string; value: string | number; - type?: ConnectType; + type?: ConnectType | SensitiveRuleType; disabled?: boolean; } @@ -71,4 +72,5 @@ export const DetectRuleTypeMap = { PATH: formatMessage({ id: 'odc.Project.Sensitive.interface.Path', defaultMessage: '路径' }), //路径 REGEX: formatMessage({ id: 'odc.Project.Sensitive.interface.Regular', defaultMessage: '正则' }), //正则 GROOVY: formatMessage({ id: 'odc.Project.Sensitive.interface.Script', defaultMessage: '脚本' }), //脚本 + AI: formatMessage({ id: 'odc.Project.Sensitive.interface.AI', defaultMessage: 'AI' }), //AI }; diff --git a/src/page/Workspace/SideBar/ResourceTree/TreeNodeMenu/config/externalTable.tsx b/src/page/Workspace/SideBar/ResourceTree/TreeNodeMenu/config/externalTable.tsx index 565daa853..cbff2d020 100644 --- a/src/page/Workspace/SideBar/ResourceTree/TreeNodeMenu/config/externalTable.tsx +++ b/src/page/Workspace/SideBar/ResourceTree/TreeNodeMenu/config/externalTable.tsx @@ -42,6 +42,7 @@ import { IMenuItemConfig } from '../type'; import { isSupportExport } from './helper'; import { isLogicalDatabase } from '@/util/database'; import { DatabasePermissionType } from '@/d.ts/database'; +import { sensitiveColumnScanner } from '@/service/sensitiveColumnScanner'; import request from '@/util/request'; import DatabaseStore from '@/store/sessionManager/database'; @@ -61,6 +62,8 @@ export const externalTableMenusConfig: Partial> = { [ResourceNodeType.TableRoot]: [ { @@ -83,6 +84,8 @@ export const tableMenusConfig: Partial = function (props) { const setSearchKey = useCallback( debounce( (v) => { - console.log(v); + // 处理值变化 _setSearchKey(v); }, 500, @@ -1276,6 +1277,21 @@ const DDLResultSet: React.FC = function (props) { /> ) : null} + {/* 敏感列标识 - 在SQL执行结果中显示 */} + ({ + columnName: col.name, + columnType: (col as any).type || 'VARCHAR', + }))} + sessionId={session?.sessionId} + triggerSource="SQL_RESULT" + autoScan={true} + showDetails={true} + size="small" + style={{ marginRight: 8 }} + /> = function ({ params, pageStore, pageKey, sett const refresh = useCallback(async () => { await fetchTable(); + // 刷新表信息时清除该表的敏感列缓存 + sensitiveColumnScanner.clearCache(params.tableName, session?.database?.dbName); }, [params.tableName, session]); useEffect(() => { @@ -207,6 +211,20 @@ const TablePage: React.FC = function ({ params, pageStore, pageKey, sett )} + {/* 敏感列标识 - 在表结构查看时显示 */} + ({ + columnName: (col as any).columnName || (col as any).name, + columnType: (col as any).dataType || (col as any).type || 'VARCHAR', + }))} + triggerSource="TABLE_VIEW" + autoScan={true} + showDetails={true} + size="default" + /> {settingStore.enableDBExport && getDataSourceModeConfig(session?.connection?.type)?.features?.task?.includes( TaskType.EXPORT, diff --git a/src/service/sensitiveColumnScanner.ts b/src/service/sensitiveColumnScanner.ts new file mode 100644 index 000000000..bda37a2a4 --- /dev/null +++ b/src/service/sensitiveColumnScanner.ts @@ -0,0 +1,919 @@ +/** + * 敏感列扫描服务 + * 用于在表结构查看和SQL执行结果中触发敏感列扫描 + */ + +import { formatMessage } from '@/util/intl'; +import logger from '@/util/logger'; +import notification from '@/util/notification'; +import { ISensitiveColumnInfo, Level } from '@/d.ts/sensitiveColumn'; +import { IServerTableColumn } from '@/d.ts/table'; +import { scanSingleTableAsync, getSingleTableScanResult } from '@/common/network/sensitiveColumn'; +import sessionManagerStore from '@/store/sessionManager'; +import login from '@/store/login'; + +// 扫描结果接口 +export interface IScanResult { + tableName: string; + databaseName: string; + sensitiveColumns: ISensitiveColumnInfo[]; + scanTime: number; + scanId: string; +} + +// 持久化缓存数据接口 +interface IPersistentCacheData { + result: IScanResult; + expireTime: number; + version: string; +} + +// 缓存配置常量 +const CACHE_CONFIG = { + // 缓存过期时间:24小时 + EXPIRE_TIME: 24 * 60 * 60 * 1000, + // 缓存版本,用于缓存格式升级时清理旧缓存 + VERSION: '1.0.0', + // localStorage键前缀 + STORAGE_PREFIX: 'sensitive-scan', +}; + +// 扫描参数接口 +export interface IScanParams { + tableName: string; + databaseName: string; + sessionId: string; + columns: IServerTableColumn[]; + projectId?: number; + connectionId?: number; + databaseId?: number; + triggerSource: 'TABLE_VIEW' | 'SQL_RESULT'; // 触发来源 +} + +class SensitiveColumnScanner { + private scanCache: Map = new Map(); + private scanningTasks: Set = new Set(); + private cacheChangeListeners: Set<(tableName: string, databaseName: string) => void> = new Set(); + + /** + * 生成内存缓存键 + */ + private getCacheKey(tableName: string, databaseName: string): string { + return `${databaseName}.${tableName}`; + } + + /** + * 生成localStorage缓存键 + */ + private getStorageKey(tableName: string, databaseName: string): string { + const orgId = login?.organizationId || 'default'; + return `${CACHE_CONFIG.STORAGE_PREFIX}-${orgId}-${databaseName}.${tableName}`; + } + + /** + * 从localStorage读取缓存 + */ + private getFromStorage(tableName: string, databaseName: string): IScanResult | null { + try { + const key = this.getStorageKey(tableName, databaseName); + const cached = localStorage.getItem(key); + if (!cached) { + logger.debug(`localStorage缓存为空: ${tableName}`); + return null; + } + + const data: IPersistentCacheData = JSON.parse(cached); + + // 检查版本 + if (data.version !== CACHE_CONFIG.VERSION) { + localStorage.removeItem(key); + logger.info( + `清除版本不匹配的缓存: ${tableName}, 版本 ${data.version} -> ${CACHE_CONFIG.VERSION}`, + ); + return null; + } + + // 检查是否过期 + if (Date.now() > data.expireTime) { + localStorage.removeItem(key); + logger.info(`清除过期缓存: ${tableName}`); + return null; + } + + logger.info(`从localStorage读取敏感列缓存: ${tableName}`); + return data.result; + } catch (error) { + logger.error('读取localStorage缓存失败:', error); + // 清除损坏的缓存 + try { + const key = this.getStorageKey(tableName, databaseName); + localStorage.removeItem(key); + logger.info(`已清除损坏的缓存: ${tableName}`); + } catch (cleanupError) { + logger.error('清除损坏缓存失败:', cleanupError); + } + return null; + } + } + + /** + * 保存到localStorage + */ + private saveToStorage(result: IScanResult): void { + try { + const key = this.getStorageKey(result.tableName, result.databaseName); + const data: IPersistentCacheData = { + result, + expireTime: Date.now() + CACHE_CONFIG.EXPIRE_TIME, + version: CACHE_CONFIG.VERSION, + }; + + const dataStr = JSON.stringify(data); + localStorage.setItem(key, dataStr); + logger.info( + `保存敏感列缓存到localStorage: ${result.tableName}, 大小: ${Math.round( + dataStr.length / 1024, + )}KB`, + ); + } catch (error) { + logger.error('保存localStorage缓存失败:', error); + // 如果是存储空间不足,尝试清理过期缓存后重试 + if (error.name === 'QuotaExceededError' || error.message.includes('quota')) { + logger.warn('localStorage空间不足,尝试清理过期缓存后重试'); + try { + this.cleanExpiredStorage(); + const key = this.getStorageKey(result.tableName, result.databaseName); + const data: IPersistentCacheData = { + result, + expireTime: Date.now() + CACHE_CONFIG.EXPIRE_TIME, + version: CACHE_CONFIG.VERSION, + }; + localStorage.setItem(key, JSON.stringify(data)); + logger.info(`重试保存成功: ${result.tableName}`); + } catch (retryError) { + logger.error('重试保存失败:', retryError); + } + } + } + } + + /** + * 保存持久化缓存 + */ + private savePersistentCache(cache: Record): void { + try { + const cacheStr = JSON.stringify(cache); + const orgId = login?.organizationId || 'default'; + const key = `${CACHE_CONFIG.STORAGE_PREFIX}-${orgId}-persistent`; + localStorage.setItem(key, cacheStr); + const cacheCount = Object.keys(cache).length; + logger.debug( + `保存持久化缓存成功: ${cacheCount} 项, 大小: ${Math.round(cacheStr.length / 1024)}KB`, + ); + } catch (error) { + logger.error('保存持久化缓存失败:', error); + // 如果是存储空间不足,尝试清理后重试 + if (error.name === 'QuotaExceededError' || error.message.includes('quota')) { + logger.warn('localStorage空间不足,尝试清理后重试保存持久化缓存'); + try { + // 清理所有过期缓存 + this.cleanExpiredStorage(); + // 重试保存 + const orgId = login?.organizationId || 'default'; + const key = `${CACHE_CONFIG.STORAGE_PREFIX}-${orgId}-persistent`; + localStorage.setItem(key, JSON.stringify(cache)); + logger.info('重试保存持久化缓存成功'); + } catch (retryError) { + logger.error('重试保存持久化缓存失败:', retryError); + } + } + } + } + + /** + * 保存到持久化缓存 + */ + private saveToPersistentCache( + cacheKey: string, + cacheData: { result: IScanResult; timestamp: number }, + ): void { + try { + const persistentCache = this.loadPersistentCache(); + persistentCache[cacheKey] = { + result: cacheData.result, + expireTime: cacheData.timestamp + 24 * 60 * 60 * 1000, // 24小时后过期 + version: CACHE_CONFIG.VERSION, + }; + this.savePersistentCache(persistentCache); + logger.debug(`保存到持久化缓存成功: ${cacheKey}`); + } catch (error) { + logger.error(`保存到持久化缓存失败: ${cacheKey}`, error); + } + } + + /** + * 从持久化缓存加载 + */ + private loadPersistentCache(): Record { + try { + const orgId = login?.organizationId || 'default'; + const key = `${CACHE_CONFIG.STORAGE_PREFIX}-${orgId}-persistent`; + const cached = localStorage.getItem(key); + if (!cached) { + return {}; + } + return JSON.parse(cached); + } catch (error) { + logger.error('加载持久化缓存失败:', error); + return {}; + } + } + + /** + * 从localStorage删除缓存 + */ + private removeFromStorage(tableName: string, databaseName: string): void { + try { + const key = this.getStorageKey(tableName, databaseName); + localStorage.removeItem(key); + logger.debug(`从localStorage删除敏感列缓存: ${tableName}`); + } catch (error) { + logger.error('删除localStorage缓存失败:', error); + } + } + + /** + * 清理过期的localStorage缓存 + */ + private cleanExpiredStorage(): void { + try { + const orgId = login?.organizationId || 'default'; + const prefix = `${CACHE_CONFIG.STORAGE_PREFIX}-${orgId}-`; + const keysToRemove: string[] = []; + + for (let i = 0; i < localStorage.length; i++) { + const key = localStorage.key(i); + if (key && key.startsWith(prefix)) { + try { + const cached = localStorage.getItem(key); + if (cached) { + const data: IPersistentCacheData = JSON.parse(cached); + if (Date.now() > data.expireTime || data.version !== CACHE_CONFIG.VERSION) { + keysToRemove.push(key); + } + } + } catch (error) { + // 解析失败的缓存也删除 + keysToRemove.push(key); + } + } + } + + keysToRemove.forEach((key) => localStorage.removeItem(key)); + if (keysToRemove.length > 0) { + logger.info(`缓存清理完成: 持久化缓存清理 ${keysToRemove.length} 项`); + } + } catch (error) { + logger.error('清理过期缓存失败:', error); + } + } + + /** + * 真实AI敏感列扫描 + * 调用后端API进行单表AI扫描 + */ + private async realAIScan(params: IScanParams): Promise { + const { tableName, databaseName } = params; + + try { + logger.info(`开始AI扫描敏感列: ${databaseName}.${tableName}`); + + // 获取项目ID和数据库ID + const { projectId, databaseId } = await this.getScanContext(params); + logger.info(`获取扫描上下文成功: projectId=${projectId}, databaseId=${databaseId}`); + + if (!projectId || !databaseId) { + const errorMsg = formatMessage({ + id: 'service.sensitiveColumn.scan.context.missing', + defaultMessage: '无法获取必要的扫描参数', + }); + logger.error(errorMsg); + throw new Error(errorMsg); + } + + // 启动单表AI扫描任务 + logger.info(`启动单表AI扫描任务: databaseId=${databaseId}, tableName=${tableName}`); + const taskId = await scanSingleTableAsync(projectId, { + databaseId, + tableName, + scanningMode: 'AI_ONLY', // AI专用模式(仅使用AI识别) + }); + logger.info(`AI扫描任务已启动: taskId=${taskId}`); + + if (!taskId) { + const errorMsg = formatMessage({ + id: 'service.sensitiveColumn.scan.task.start.failed', + defaultMessage: '启动单表扫描任务失败', + }); + logger.error(errorMsg); + throw new Error(errorMsg); + } + + // 轮询单表扫描结果 + logger.info(`开始轮询AI扫描结果: taskId=${taskId}`); + const result = await this.pollSingleTableScanResults(projectId, taskId); + logger.info(`AI扫描完成,找到 ${result.sensitiveColumns?.length || 0} 个敏感列`); + + // 转换扫描结果格式 + const sensitiveColumnsData = result.result || result.sensitiveColumns || []; + const tableSensitiveColumns = sensitiveColumnsData.map((col) => { + const mappedLevel = this.mapSensitivityLevel(col.level); + return { + columnName: col.columnName, + sensitivityLevel: mappedLevel, + reason: + col.reason || + formatMessage({ + id: 'service.sensitiveColumn.scan.reason.default', + defaultMessage: '规则匹配', + }), + confidence: col.confidence || 0.9, + }; + }); + + logger.info(`AI扫描结果转换完成,共找到 ${tableSensitiveColumns.length} 个敏感列`); + return tableSensitiveColumns; + } catch (error) { + logger.error('AI扫描敏感列失败:', error); + notification.error({ + track: formatMessage({ + id: 'service.sensitiveColumn.scan.ai.failed', + defaultMessage: 'AI扫描敏感列失败', + }), + supportRepeat: true, + }); + throw error; + } + } + + private async getScanContext(params: IScanParams): Promise<{ + projectId: number; + connectionId?: number; + databaseId: number; + }> { + const { sessionId, databaseName } = params; + + // 从会话管理器获取连接信息 + const session = sessionManagerStore.sessionMap.get(sessionId); + if (!session) { + const errorMsg = formatMessage({ + id: 'service.sensitiveColumn.scan.session.notfound', + defaultMessage: '无法找到会话信息', + }); + logger.error(`${errorMsg}: sessionId=${sessionId}`); + throw new Error(errorMsg); + } + + const connectionId = session.connection?.id; + const projectId = session.odcDatabase?.project?.id; + const databaseId = session.odcDatabase?.id; + + if (!projectId || !databaseId) { + const errorMsg = formatMessage({ + id: 'service.sensitiveColumn.scan.session.incomplete', + defaultMessage: '会话信息不完整', + }); + logger.error(`${errorMsg}: projectId=${projectId}, databaseId=${databaseId}`); + throw new Error(errorMsg); + } + + return { projectId, connectionId, databaseId }; + } + + private async pollScanResults( + projectId: number, + taskId: string, + maxAttempts: number = 60, + ): Promise { + logger.info(`开始轮询扫描结果: taskId=${taskId}, maxAttempts=${maxAttempts}`); + + for (let attempt = 0; attempt < maxAttempts; attempt++) { + try { + const result = await getSingleTableScanResult(projectId, taskId); + + if (result && (result.status === 'SUCCESS' || result.status === 'COMPLETED')) { + logger.info(`扫描任务完成: taskId=${taskId}, attempt=${attempt + 1}`); + return result; + } + + if (result && (result.status === 'FAILED' || result.status === 'CANCELLED')) { + const errorMsg = formatMessage({ + id: 'service.sensitiveColumn.scan.task.failed', + defaultMessage: '扫描任务失败', + }); + const fullErrorMsg = `${errorMsg}: ${ + result.errorMsg || + formatMessage({ + id: 'service.sensitiveColumn.scan.error.unknown', + defaultMessage: '未知错误', + }) + }`; + logger.error(fullErrorMsg); + throw new Error(fullErrorMsg); + } + + if (attempt % 10 === 0) { + logger.info( + `轮询进度: taskId=${taskId}, attempt=${attempt + 1}/${maxAttempts}, status=${ + result?.status || 'unknown' + }`, + ); + } + + // 等待2秒后重试 + await new Promise((resolve) => setTimeout(resolve, 2000)); + } catch (error) { + if (attempt === maxAttempts - 1) { + logger.error(`轮询扫描结果失败: taskId=${taskId}`, error); + throw error; + } + logger.warn(`轮询扫描结果出错,将重试: taskId=${taskId}, attempt=${attempt + 1}`, error); + await new Promise((resolve) => setTimeout(resolve, 2000)); + } + } + + const timeoutMsg = formatMessage({ + id: 'service.sensitiveColumn.scan.task.timeout', + defaultMessage: '扫描任务超时', + }); + logger.error(`${timeoutMsg}: taskId=${taskId}`); + throw new Error(timeoutMsg); + } + + /** + * 轮询单表扫描结果 + */ + private async pollSingleTableScanResults( + projectId: number, + taskId: string, + maxAttempts: number = 60, + ): Promise { + logger.info(`开始轮询单表扫描结果: taskId=${taskId}, maxAttempts=${maxAttempts}`); + + for (let attempt = 0; attempt < maxAttempts; attempt++) { + try { + const result = await getSingleTableScanResult(projectId, taskId); + + if (result && (result.status === 'SUCCESS' || result.status === 'COMPLETED')) { + logger.info(`单表扫描任务完成: taskId=${taskId}, attempt=${attempt + 1}`); + return result; + } + + if (result && (result.status === 'FAILED' || result.status === 'CANCELLED')) { + const errorMsg = formatMessage({ + id: 'service.sensitiveColumn.scan.task.failed', + defaultMessage: '扫描任务失败', + }); + const fullErrorMsg = `${errorMsg}: ${ + result.errorMsg || + result.errorCode || + formatMessage({ + id: 'service.sensitiveColumn.scan.error.unknown', + defaultMessage: '未知错误', + }) + }`; + logger.error(fullErrorMsg); + throw new Error(fullErrorMsg); + } + + if (attempt % 10 === 0) { + logger.info( + `轮询进度: taskId=${taskId}, attempt=${attempt + 1}/${maxAttempts}, status=${ + result?.status || 'unknown' + }`, + ); + } + + // 等待2秒后重试 + await new Promise((resolve) => setTimeout(resolve, 2000)); + } catch (error) { + if (attempt === maxAttempts - 1) { + logger.error(`轮询单表扫描结果失败: taskId=${taskId}`, error); + throw error; + } + logger.warn( + `轮询单表扫描结果出错,将重试: taskId=${taskId}, attempt=${attempt + 1}`, + error, + ); + await new Promise((resolve) => setTimeout(resolve, 2000)); + } + } + + const timeoutMsg = formatMessage({ + id: 'service.sensitiveColumn.scan.task.timeout', + defaultMessage: '扫描任务超时', + }); + logger.error(`${timeoutMsg}: taskId=${taskId}`); + throw new Error(timeoutMsg); + } + + private mapSensitivityLevel(level: any): 'HIGH' | 'MEDIUM' | 'LOW' { + logger.debug(`mapSensitivityLevel 输入:`, { level, type: typeof level, value: level }); + + // 处理 Level 枚举或字符串 + if (typeof level === 'number') { + logger.debug(`处理数字类型敏感等级: ${level}`); + // Level 枚举: LOW=0, MEDIUM=1, HIGH=2, EXTREME_HIGH=3 + switch (level) { + case 0: // Level.LOW + logger.debug(`映射 ${level} -> LOW`); + return 'LOW'; + case 1: // Level.MEDIUM + logger.debug(`映射 ${level} -> MEDIUM`); + return 'MEDIUM'; + case 2: // Level.HIGH + case 3: // Level.EXTREME_HIGH + logger.debug(`映射 ${level} -> HIGH`); + return 'HIGH'; + default: + logger.debug(`未知数字等级 ${level},默认返回 MEDIUM`); + return 'MEDIUM'; + } + } + + // 处理字符串类型 + const levelStr = String(level).toUpperCase(); + logger.debug(`处理字符串类型敏感等级: ${level} -> ${levelStr}`); + switch (levelStr) { + case 'HIGH': + case '高': + logger.debug(`映射 ${levelStr} -> HIGH`); + return 'HIGH'; + case 'MEDIUM': + case '中': + logger.debug(`映射 ${levelStr} -> MEDIUM`); + return 'MEDIUM'; + case 'LOW': + case '低': + logger.debug(`映射 ${levelStr} -> LOW`); + return 'LOW'; + default: + logger.debug(`未知字符串等级 ${levelStr},默认返回 MEDIUM`); + return 'MEDIUM'; + } + } + + /** + * 模拟AI敏感列扫描 + * 这里使用Mock数据模拟AI扫描行为 + */ + private async mockAIScan(params: IScanParams): Promise { + const { tableName, columns } = params; + logger.info(`开始Mock AI扫描: ${tableName}`); + + // 保留原有的模拟逻辑作为备用 + if (!columns || columns.length === 0) { + logger.info(`Mock AI扫描完成: ${tableName}, 无列信息`); + return []; + } + + const sensitiveColumns: ISensitiveColumnInfo[] = []; + + // 敏感列匹配模式 + const sensitivePatterns = { + HIGH: [ + /password/i, + /pwd/i, + /secret/i, + /token/i, + /key/i, + /身份证/i, + /idcard/i, + /id_card/i, + /ssn/i, + /银行卡/i, + /bank_card/i, + /credit_card/i, + /手机号/i, + /phone/i, + /mobile/i, + /tel/i, + ], + MEDIUM: [ + /email/i, + /mail/i, + /邮箱/i, + /address/i, + /addr/i, + /地址/i, + /name/i, + /姓名/i, + /username/i, + /user_name/i, + /salary/i, + /income/i, + /工资/i, + /收入/i, + ], + LOW: [ + /age/i, + /年龄/i, + /birthday/i, + /birth/i, + /生日/i, + /gender/i, + /sex/i, + /性别/i, + /department/i, + /dept/i, + /部门/i, + ], + }; + + // 模拟AI识别逻辑 + columns.forEach((column) => { + const columnName = column.name; + + // 检查高敏感 + for (const pattern of sensitivePatterns.HIGH) { + if (pattern.test(columnName)) { + sensitiveColumns.push({ + columnName, + sensitivityLevel: 'HIGH', + reason: `列名匹配高敏感模式: ${pattern.source}`, + confidence: 0.9 + Math.random() * 0.1, + }); + return; + } + } + + // 检查中敏感 + for (const pattern of sensitivePatterns.MEDIUM) { + if (pattern.test(columnName)) { + sensitiveColumns.push({ + columnName, + sensitivityLevel: 'MEDIUM', + reason: `列名匹配中敏感模式: ${pattern.source}`, + confidence: 0.7 + Math.random() * 0.2, + }); + return; + } + } + + // 检查低敏感 + for (const pattern of sensitivePatterns.LOW) { + if (pattern.test(columnName)) { + sensitiveColumns.push({ + columnName, + sensitivityLevel: 'LOW', + reason: `列名匹配低敏感模式: ${pattern.source}`, + confidence: 0.5 + Math.random() * 0.3, + }); + return; + } + } + }); + + logger.info(`Mock AI扫描完成: ${tableName}, 返回 ${sensitiveColumns.length} 个敏感列`); + return sensitiveColumns; + } + + /** + * 执行敏感列扫描 + */ + private async performScan(params: IScanParams): Promise { + const { tableName } = params; + logger.info(`执行敏感列扫描: ${tableName}`); + + try { + // 直接使用真实AI扫描 + const result = await this.realAIScan(params); + + logger.info(`敏感列扫描完成: ${tableName}, 找到 ${result.length} 个敏感列`); + return result; + } catch (error) { + logger.error(`敏感列扫描执行失败: ${tableName}`, error); + throw error; + } + } + + /** + * 扫描敏感列 + */ + public async scanSensitiveColumns(params: IScanParams): Promise { + const cacheKey = this.getCacheKey(params.tableName, params.databaseName); + + logger.info(`开始敏感列扫描: ${cacheKey} (来源: ${params.triggerSource})`); + + try { + // 检查是否正在扫描 + if (this.scanningTasks.has(cacheKey)) { + logger.info(`敏感列扫描正在进行中: ${cacheKey}`); + return null; + } + + // 首先检查内存缓存(5分钟有效期,用于快速访问) + const memoryCached = this.scanCache.get(cacheKey); + if (memoryCached && Date.now() - memoryCached.scanTime < 5 * 60 * 1000) { + logger.info(`使用内存缓存的敏感列扫描结果: ${cacheKey}`); + return memoryCached; + } + + // 检查localStorage持久化缓存(24小时有效期) + const storageCached = this.getFromStorage(params.tableName, params.databaseName); + if (storageCached) { + // 将持久化缓存加载到内存缓存中 + this.scanCache.set(cacheKey, storageCached); + logger.info(`使用localStorage缓存的敏感列扫描结果: ${cacheKey}`); + return storageCached; + } + + try { + this.scanningTasks.add(cacheKey); + + logger.info(`开始新的敏感列扫描任务: ${params.tableName}`); + + // 清理过期缓存(异步执行,不阻塞扫描) + setTimeout(() => this.cleanExpiredStorage(), 0); + + // 使用统一的扫描方法 + const sensitiveColumns = await this.performScan(params); + + const result: IScanResult = { + tableName: params.tableName, + databaseName: params.databaseName, + sensitiveColumns, + scanTime: Date.now(), + scanId: `scan_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`, + }; + + // 同时保存到内存缓存和localStorage + this.scanCache.set(cacheKey, result); + this.saveToStorage(result); + + logger.info( + `敏感列扫描完成: ${params.tableName}, 找到 ${sensitiveColumns.length} 个敏感列`, + ); + return result; + } finally { + this.scanningTasks.delete(cacheKey); + } + } catch (error) { + logger.error(`敏感列扫描失败: ${cacheKey}`, error); + notification.error({ + track: formatMessage({ + id: 'service.sensitiveColumn.scan.failed', + defaultMessage: '敏感列扫描失败', + }), + supportRepeat: true, + }); + return null; + } + } + + /** + * 获取缓存的扫描结果 + */ + getCachedResult(tableName: string, databaseName: string): IScanResult | null { + const cacheKey = this.getCacheKey(tableName, databaseName); + + // 首先检查内存缓存 + const memoryCached = this.scanCache.get(cacheKey); + if (memoryCached && Date.now() - memoryCached.scanTime < 5 * 60 * 1000) { + return memoryCached; + } + + // 检查localStorage缓存 + const storageCached = this.getFromStorage(tableName, databaseName); + if (storageCached) { + // 将持久化缓存加载到内存缓存中 + this.scanCache.set(cacheKey, storageCached); + return storageCached; + } + + return null; + } + + /** + * 清除缓存 + */ + public clearCache(tableName?: string, databaseName?: string): void { + if (tableName && databaseName) { + const cacheKey = this.getCacheKey(tableName, databaseName); + this.scanCache.delete(cacheKey); + this.removeFromStorage(tableName, databaseName); + logger.info(`清除敏感列缓存: ${tableName}`); + // 通知监听器缓存已清除 + this.notifyCacheChange(tableName, databaseName); + } else { + // 清除所有内存缓存 + this.scanCache.clear(); + + // 清除所有localStorage缓存 + try { + const orgId = login?.organizationId || 'default'; + const prefix = `${CACHE_CONFIG.STORAGE_PREFIX}-${orgId}-`; + const keysToRemove: string[] = []; + + for (let i = 0; i < localStorage.length; i++) { + const key = localStorage.key(i); + if (key && key.startsWith(prefix)) { + keysToRemove.push(key); + } + } + + keysToRemove.forEach((key) => localStorage.removeItem(key)); + logger.info(`清除所有敏感列缓存,共${keysToRemove.length}个`); + } catch (error) { + logger.error('清除localStorage缓存失败:', error); + } + } + } + + /** + * 获取扫描状态 + */ + public isScanning(tableName: string, databaseName: string): boolean { + const cacheKey = this.getCacheKey(tableName, databaseName); + return this.scanningTasks.has(cacheKey); + } + + /** + * 按数据库清除缓存 + * 用于数据库刷新时清除该数据库下所有表的敏感列缓存 + */ + public clearCacheByDatabase(databaseName: string): void { + try { + // 清除内存缓存 + const keysToDelete: string[] = []; + const tablesToNotify: string[] = []; + this.scanCache.forEach((_, key) => { + if (key.startsWith(`${databaseName}.`)) { + keysToDelete.push(key); + // 提取表名用于通知 + const tableName = key.substring(`${databaseName}.`.length); + tablesToNotify.push(tableName); + } + }); + keysToDelete.forEach((key) => this.scanCache.delete(key)); + + // 清除localStorage缓存 + const orgId = login?.organizationId || 'default'; + const prefix = `${CACHE_CONFIG.STORAGE_PREFIX}-${orgId}-${databaseName}.`; + const storageKeysToRemove: string[] = []; + + for (let i = 0; i < localStorage.length; i++) { + const key = localStorage.key(i); + if (key && key.startsWith(prefix)) { + storageKeysToRemove.push(key); + } + } + + storageKeysToRemove.forEach((key) => localStorage.removeItem(key)); + + const totalCleared = keysToDelete.length + storageKeysToRemove.length; + if (totalCleared > 0) { + logger.info(`清除数据库 ${databaseName} 的敏感列缓存,共${totalCleared}个`); + // 通知所有受影响的表 + tablesToNotify.forEach((tableName) => { + this.notifyCacheChange(tableName, databaseName); + }); + } + } catch (error) { + logger.error(`清除数据库 ${databaseName} 缓存失败:`, error); + } + } + + /** + * 添加缓存变更监听器 + */ + public addCacheChangeListener(listener: (tableName: string, databaseName: string) => void): void { + this.cacheChangeListeners.add(listener); + } + + /** + * 移除缓存变更监听器 + */ + public removeCacheChangeListener( + listener: (tableName: string, databaseName: string) => void, + ): void { + this.cacheChangeListeners.delete(listener); + } + + /** + * 通知缓存变更 + */ + private notifyCacheChange(tableName: string, databaseName: string): void { + this.cacheChangeListeners.forEach((listener) => { + try { + listener(tableName, databaseName); + } catch (error) { + logger.error('缓存变更监听器执行失败:', error); + } + }); + } +} + +// 导出单例 +export const sensitiveColumnScanner = new SensitiveColumnScanner(); +export default sensitiveColumnScanner;