ErrorReportModal.jsx 7.64 KB
import React from 'react'
import { Modal, Descriptions, Tag, Button, Space } from 'antd'
import { CopyOutlined, BugOutlined } from '@ant-design/icons'

/**
 * 错误类型映射
 */
function getErrorMeta(error) {
  const msg = (error || '').toLowerCase()
  if (msg.includes('syntax') || msg.includes('you have an error in your sql')) {
    return { type: 'SQL语法错误', color: 'red', level: '严重' }
  }
  if (msg.includes('timeout') || msg.includes('timed out')) {
    return { type: '查询超时', color: 'orange', level: '警告' }
  }
  if (msg.includes('lock') || msg.includes('deadlock') || msg.includes('lock wait timeout')) {
    return { type: '锁等待/死锁', color: 'red', level: '严重' }
  }
  if (msg.includes('connection') || msg.includes('econnrefused') || msg.includes('lost connection')) {
    return { type: '数据库连接异常', color: 'red', level: '严重' }
  }
  if (msg.includes('access denied') || msg.includes('permission') || msg.includes('command denied')) {
    return { type: '权限不足', color: 'volcano', level: '警告' }
  }
  if (msg.includes('unknown column') || msg.includes('unknown table') || msg.includes('doesn\'t exist')) {
    return { type: '对象不存在', color: 'orange', level: '警告' }
  }
  if (msg.includes('duplicate') || msg.includes('unique') || msg.includes('primary')) {
    return { type: '主键/唯一约束冲突', color: 'orange', level: '警告' }
  }
  if (msg.includes('data too long') || msg.includes('out of range')) {
    return { type: '数据溢出', color: 'orange', level: '警告' }
  }
  return { type: '执行异常', color: 'default', level: '一般' }
}

/**
 * 根据错误类型给出修复建议
 */
function getSuggestions(error) {
  const msg = (error || '').toLowerCase()
  const suggestions = []

  if (msg.includes('syntax') || msg.includes('you have an error in your sql')) {
    suggestions.push('检查SQL语句的语法是否正确,如引号是否闭合、关键字是否拼写错误')
    suggestions.push('注意MySQL保留字,如果使用了保留字作为字段名,需要用反引号 ` 包裹')
    suggestions.push('检查是否缺少了必要的逗号或括号')
  }
  if (msg.includes('timeout') || msg.includes('timed out')) {
    suggestions.push('查询可能涉及大量数据,建议添加 WHERE 条件缩小查询范围')
    suggestions.push('检查相关字段是否有索引,避免全表扫描')
    suggestions.push('如果使用了 JOIN,确保关联字段有索引')
    suggestions.push('考虑将复杂查询拆分为多个简单查询')
  }
  if (msg.includes('lock') || msg.includes('deadlock')) {
    suggestions.push('可能存在其他事务正在操作相同的数据,导致锁等待')
    suggestions.push('建议在非业务高峰期执行此SQL')
    suggestions.push('检查是否有长时间未提交的事务')
  }
  if (msg.includes('connection')) {
    suggestions.push('检查数据库服务是否正常运行')
    suggestions.push('检查网络连接是否正常')
    suggestions.push('检查数据库连接数是否已达上限')
  }
  if (msg.includes('access denied') || msg.includes('permission')) {
    suggestions.push('检查当前数据库用户是否有执行该操作的权限')
    suggestions.push('联系数据库管理员授予相应权限')
  }
  if (msg.includes('unknown column') || msg.includes('unknown table')) {
    suggestions.push('检查表名或字段名是否拼写正确')
    suggestions.push('检查是否选对了数据库')
    suggestions.push('注意表名和字段名的大小写,Linux下MySQL区分大小写')
  }

  if (suggestions.length === 0) {
    suggestions.push('请仔细阅读上方错误信息,定位问题原因')
    suggestions.push('如果无法自行解决,请联系数据库管理员')
  }

  return suggestions
}

export default function ErrorReportModal({ open, onClose, errorInfo }) {
  if (!errorInfo) return null

  const { error, sql, phase, timestamp } = errorInfo
  const meta = getErrorMeta(error)
  const suggestions = getSuggestions(error)

  const handleCopyError = () => {
    const text = [
      `=== SQL执行错误报告 ===`,
      `时间: ${timestamp || new Date().toLocaleString()}`,
      `阶段: ${phase || '执行阶段'}`,
      `错误类型: ${meta.type}`,
      `严重程度: ${meta.level}`,
      `错误信息: ${error}`,
      ``,
      `=== SQL脚本 ===`,
      sql || '无',
      ``,
      `=== 修复建议 ===`,
      ...suggestions.map((s, i) => `${i + 1}. ${s}`),
    ].join('\n')

    navigator.clipboard.writeText(text).then(() => {
      const btn = document.activeElement
      btn && btn.click && btn.textContent && (btn.textContent = '已复制')
    })
  }

  return (
    <Modal
      title={
        <span>
          <BugOutlined style={{ color: '#ff4d4f', marginRight: 8 }} />
          SQL执行错误报告
        </span>
      }
      open={open}
      onCancel={onClose}
      width={680}
      footer={
        <Space>
          <Button onClick={handleCopyError} icon={<CopyOutlined />}>
            复制错误报告
          </Button>
          <Button type="primary" onClick={onClose}>
            我知道了
          </Button>
        </Space>
      }
    >
      {/* 基本信息 */}
      <Descriptions
        bordered
        size="small"
        column={2}
        style={{ marginBottom: 16 }}
      >
        <Descriptions.Item label="错误类型">
          <Tag color={meta.color}>{meta.type}</Tag>
        </Descriptions.Item>
        <Descriptions.Item label="严重程度">
          <Tag color={meta.level === '严重' ? 'red' : meta.level === '警告' ? 'orange' : 'blue'}>
            {meta.level}
          </Tag>
        </Descriptions.Item>
        <Descriptions.Item label="执行阶段">
          {phase || '执行阶段'}
        </Descriptions.Item>
        <Descriptions.Item label="发生时间">
          {timestamp || new Date().toLocaleString()}
        </Descriptions.Item>
      </Descriptions>

      {/* 错误详情 */}
      <div style={{ marginBottom: 16 }}>
        <h4 style={{ marginBottom: 8, color: '#ff4d4f' }}>错误详情</h4>
        <div style={{
          background: '#fff2f0',
          border: '1px solid #ffccc7',
          borderRadius: 6,
          padding: '12px 16px',
          fontSize: 13,
          fontFamily: 'Menlo, Monaco, Consolas, monospace',
          wordBreak: 'break-all',
          lineHeight: 1.8,
          color: '#cf1322',
          maxHeight: 120,
          overflow: 'auto',
        }}>
          {error || '未知错误'}
        </div>
      </div>

      {/* 出错的SQL */}
      {sql && (
        <div style={{ marginBottom: 16 }}>
          <h4 style={{ marginBottom: 8 }}>出错的SQL</h4>
          <pre style={{
            background: '#f5f5f5',
            border: '1px solid #d9d9d9',
            borderRadius: 6,
            padding: '12px 16px',
            fontSize: 12,
            fontFamily: 'Menlo, Monaco, Consolas, monospace',
            maxHeight: 150,
            overflow: 'auto',
            whiteSpace: 'pre-wrap',
            wordBreak: 'break-all',
            lineHeight: 1.6,
          }}>
            {sql}
          </pre>
        </div>
      )}

      {/* 修复建议 */}
      <div>
        <h4 style={{ marginBottom: 8, color: '#1890ff' }}>修复建议</h4>
        <div style={{
          background: '#e6f7ff',
          border: '1px solid #91d5ff',
          borderRadius: 6,
          padding: '12px 16px',
        }}>
          {suggestions.map((s, i) => (
            <div key={i} style={{ fontSize: 13, lineHeight: 2, color: '#096dd9' }}>
              <span style={{ fontWeight: 600, marginRight: 8 }}>{i + 1}.</span>
              {s}
            </div>
          ))}
        </div>
      </div>
    </Modal>
  )
}