feat: 执行结果结构化-每条SQL记录status/affected_rows/duration_ms + 去掉工单详情优先级字段

后端:
- 新增 SqlExecItem/SqlExecResult 结构体
- MySQL/Redis/gh-ost 三条执行路径全部改为 JSON 结构化结果
- 每条 SQL 记录完整语句、状态、影响行数、耗时(ms)、错误信息
- 总耗时/开始结束时间一并记录

前端:
- ExecuteResult 接口匹配后端 SqlExecResult
- 表格展示每条 SQL 执行结果(序号/SQL/状态/影响行数/耗时/错误)
- 汇总卡片: 状态/总耗时/SQL数量
- 去掉工单详情中的优先级和变更原因字段
- 保留 plain text fallback 兼容旧数据

E2E验证: INSERT+UPDATE+ALTER 三语句全部成功, 结构化结果正确
This commit is contained in:
logan committed 2026-06-03 02:15:37 +08:00
1 parent 1364c737ef
commit bec2cafec2
2 files changed
+140 -73

No files matched your search

+58 -55
View File
@@ -84,16 +84,20 @@ interface TicketStatistics {
failed: number;
}
interface ExecuteResult {
status: string;
interface SqlExecItem {
sql: string;
status: string; // success / fail
affected_rows: number;
duration_ms: number;
validation_passed: boolean;
warnings: string[];
errors: string[];
raw_output: string;
before_snapshot?: string;
after_snapshot?: string;
error: string;
}
interface ExecuteResult {
status: string; // success / failed
total_ms: number;
items: SqlExecItem[];
start_time?: string;
end_time?: string;
}
/* ------------------------------------------------------------------ */
@@ -725,8 +729,6 @@ function TicketDetailDialog({ ticket: initialTicket, onClose, canApprove, canExe
{ label: '目标库', value: ticket.target_database },
{ label: '数据库类型', value: ticket.db_type.toUpperCase() },
{ label: '创建时间', value: new Date(ticket.create_time).toLocaleString() },
{ label: '变更原因', value: ticket.reason || '-' },
{ label: '优先级', value: ticket.priority === 'high' ? '高' : ticket.priority === 'low' ? '低' : '中' },
].map((item, idx) => (
<div key={idx} className="flex justify-between py-1.5">
<span className="text-xs text-[var(--dg-text-muted)]">{item.label}</span>
@@ -745,15 +747,15 @@ function TicketDetailDialog({ ticket: initialTicket, onClose, canApprove, canExe
</pre>
</div>
{/* Execute Result — 结构化展示 */}
{/* Execute Result — 结构化表格展示 */}
{execResult && (
<div>
<h3 className="text-sm font-medium mb-2 flex items-center gap-1.5">
<Play className="w-4 h-4 text-purple-400" /> 执行结果
</h3>
{/* Summary */}
<div className="grid grid-cols-4 gap-3 mb-3">
{/* Summary row */}
<div className="grid grid-cols-3 gap-3 mb-3">
<div className="p-3 bg-[var(--dg-bg-tertiary)] rounded-lg text-center">
<div className="text-[10px] text-[var(--dg-text-muted)] mb-1">状态</div>
<div className={`text-sm font-medium ${execResult.status === 'success' ? 'text-green-400' : 'text-red-400'}`}>
@@ -761,53 +763,54 @@ function TicketDetailDialog({ ticket: initialTicket, onClose, canApprove, canExe
</div>
</div>
<div className="p-3 bg-[var(--dg-bg-tertiary)] rounded-lg text-center">
<div className="text-[10px] text-[var(--dg-text-muted)] mb-1">影响行数</div>
<div className="text-sm font-medium">{execResult.affected_rows}</div>
<div className="text-[10px] text-[var(--dg-text-muted)] mb-1">总耗时</div>
<div className="text-sm font-medium">{execResult.total_ms}ms</div>
</div>
<div className="p-3 bg-[var(--dg-bg-tertiary)] rounded-lg text-center">
<div className="text-[10px] text-[var(--dg-text-muted)] mb-1">执行耗时</div>
<div className="text-sm font-medium">{execResult.duration_ms}ms</div>
</div>
<div className="p-3 bg-[var(--dg-bg-tertiary)] rounded-lg text-center">
<div className="text-[10px] text-[var(--dg-text-muted)] mb-1">验证</div>
<div className={`text-sm font-medium ${execResult.validation_passed ? 'text-green-400' : 'text-red-400'}`}>
{execResult.validation_passed ? '通过' : '未通过'}
</div>
<div className="text-[10px] text-[var(--dg-text-muted)] mb-1">SQL 数量</div>
<div className="text-sm font-medium">{execResult.items?.length || 0}</div>
</div>
</div>
{/* Raw Output */}
{execResult.raw_output && (
<div className="mb-3">
<div className="text-[10px] text-[var(--dg-text-muted)] mb-1">原始输出</div>
<pre className="p-3 bg-[var(--dg-bg-tertiary)] rounded-lg text-xs font-mono overflow-x-auto whitespace-pre-wrap max-h-[120px] overflow-y-auto">
{execResult.raw_output}
</pre>
</div>
)}
{/* Errors */}
{execResult.errors && execResult.errors.length > 0 && (
<div>
<div className="text-[10px] text-red-400 mb-1">错误信息</div>
{execResult.errors.map((err, i) => (
<div key={i} className="flex items-start gap-2 p-2 mb-1 bg-red-500/5 border border-red-500/20 rounded-md text-xs text-red-400">
<AlertTriangle className="w-3 h-3 mt-0.5 flex-shrink-0" />
{err}
</div>
))}
</div>
)}
{/* Warnings */}
{execResult.warnings && execResult.warnings.length > 0 && (
<div className="mt-3">
<div className="text-[10px] text-amber-400 mb-1">警告信息</div>
{execResult.warnings.map((w, i) => (
<div key={i} className="p-2 mb-1 bg-amber-500/5 border border-amber-500/20 rounded-md text-xs text-amber-400">
{w}
</div>
))}
{/* Per-SQL result table */}
{execResult.items && execResult.items.length > 0 && (
<div className="overflow-x-auto">
<table className="w-full text-xs">
<thead>
<tr className="text-[var(--dg-text-muted)] text-left">
<th className="pb-2 pr-3 font-medium w-6">#</th>
<th className="pb-2 pr-3 font-medium">SQL 语句</th>
<th className="pb-2 pr-3 font-medium w-16">执行结果</th>
<th className="pb-2 pr-3 font-medium w-16">影响行数</th>
<th className="pb-2 pr-3 font-medium w-20">执行时间</th>
<th className="pb-2 font-medium">错误信息</th>
</tr>
</thead>
<tbody>
{execResult.items.map((item, idx) => (
<tr key={idx} className="border-t border-[var(--dg-border)]">
<td className="py-2 pr-3 text-[var(--dg-text-muted)]">{idx + 1}</td>
<td className="py-2 pr-3">
<pre className="font-mono text-xs whitespace-pre-wrap max-h-[60px] overflow-y-auto">{item.sql}</pre>
</td>
<td className="py-2 pr-3">
<span className={item.status === 'success' ? 'text-green-400' : 'text-red-400'}>
{item.status === 'success' ? '成功' : '失败'}
</span>
</td>
<td className="py-2 pr-3">{item.affected_rows}</td>
<td className="py-2 pr-3">{item.duration_ms}ms</td>
<td className="py-2">
{item.error && (
<span className={item.status === 'fail' ? 'text-red-400' : 'text-[var(--dg-text-muted)]'}>
{item.error.length > 80 ? item.error.substring(0, 80) + '...' : item.error}
</span>
)}
</td>
</tr>
))}
</tbody>
</table>
</div>
)}