¥
立即购买

PHP数据库连接错误消息生成器

3 浏览
1 试用
0 购买
Dec 8, 2025更新

本提示词专为PHP开发场景设计,能够根据具体数据库类型和错误场景生成专业、准确且用户友好的自定义错误消息。它通过分析错误原因、数据库类型和用户群体,提供技术精准且符合用户体验的解决方案,帮助开发者快速定位和解决数据库连接问题,提升系统可靠性和用户满意度。

错误消息代码

<?php
// 场景:大促下单接口连接 MySQL 主库超时,连接池耗尽,自动重试 3 次仍失败
// 目标:对终端用户展示友好的页面提示,同时记录安全的技术日志供排障
// 注意:请在生产环境确保 display_errors=Off,敏感信息不可输出到页面

declare(strict_types=1);

// 可根据需要设置默认时区
date_default_timezone_set('Asia/Shanghai');

// 从环境变量读取数据库配置(不要在页面输出这些值)
$dbHost = getenv('DB_HOST') ?: '127.0.0.1';
$dbPort = (int)(getenv('DB_PORT') ?: 3306);
$dbName = getenv('DB_NAME') ?: 'app';
$dbUser = getenv('DB_USER') ?: 'app_user';
$dbPass = getenv('DB_PASS') ?: '';

const USER_MESSAGE_CODE = 'ORD-DB-CONN-001'; // 固定的业务错误码(对外展示)
const MAX_ATTEMPTS = 3;
const CONNECT_TIMEOUT_SEC = 3; // 连接超时(秒),视网络与数据库情况调整
const BASE_BACKOFF_MS = 160;   // 初始退避(毫秒)

/**
 * 判断是否为“可重试”的瞬时连接错误
 */
function isTransientConnectionError(?string $sqlState, ?int $driverCode, string $msg): bool
{
    $sqlState = strtoupper((string)$sqlState);

    // 常见可重试情形:网络/握手/连接数耗尽/超时
    if (in_array($driverCode, [1040, 2002, 2006, 2013], true)) {
        // 1040 Too many connections
        // 2002 Can't connect / connection timed out / refused
        // 2006 MySQL server has gone away
        // 2013 Lost connection during query
        return true;
    }

    // SQLSTATE 类别 08 开头为连接类错误;08S01 通信故障
    if (strpos($sqlState, '08') === 0 || $sqlState === '08S01') {
        return true;
    }

    // HY000 为通用错误,结合报错信息中的 timeout 关键词做兜底判断(不在页面展示)
    if ($sqlState === 'HY000' && preg_match('/(timeout|timed out|too many connections)/i', $msg)) {
        return true;
    }

    return false;
}

/**
 * 带指数退避 + 抖动的连接重试
 */
function connectWithRetry(int $maxAttempts, string $refId): PDO
{
    global $dbHost, $dbPort, $dbName, $dbUser, $dbPass;

    $dsn = sprintf('mysql:host=%s;port=%d;dbname=%s;charset=utf8mb4', $dbHost, $dbPort, $dbName);
    $options = [
        PDO::ATTR_ERRMODE            => PDO::ERRMODE_EXCEPTION,
        PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
        PDO::ATTR_TIMEOUT            => CONNECT_TIMEOUT_SEC,   // 连接超时
        // 如需持久连接(谨慎评估连接复用与资源占用),可开启:
        // PDO::ATTR_PERSISTENT         => true,
    ];

    $attempt = 0;
    $lastEx   = null;

    while ($attempt < $maxAttempts) {
        $attempt++;
        $start = microtime(true);
        try {
            $pdo = new PDO($dsn, $dbUser, $dbPass, $options);
            // 可选:快速探活,确保连接可用
            $pdo->query('SELECT 1');
            return $pdo;
        } catch (PDOException $e) {
            $elapsedMs  = (int)((microtime(true) - $start) * 1000);
            $sqlState   = $e->getCode();           // 对于 PDOException,这一般是 SQLSTATE
            $driverCode = $e->errorInfo[1] ?? null; // MySQL 驱动错误码(如 1040、2002 等)
            $msg        = $e->getMessage();

            // 记录安全的技术日志(不包含 DSN、账号等敏感信息)
            error_log(sprintf(
                'DB_CONN_RETRY ref=%s attempt=%d/%d elapsed=%dms sqlstate=%s code=%s note=%s',
                $refId,
                $attempt,
                $maxAttempts,
                $elapsedMs,
                (string)$sqlState,
                (string)$driverCode,
                // 仅记录简要错误类型关键词,避免输出环境细节
                isTransientConnectionError($sqlState, $driverCode, $msg) ? 'transient' : 'non-transient'
            ));

            $lastEx = $e;

            // 非瞬时错误直接终止(如认证失败/权限问题)
            if (!isTransientConnectionError($sqlState, $driverCode, $msg)) {
                break;
            }

            // 达到最大重试次数,结束
            if ($attempt >= $maxAttempts) {
                break;
            }

            // 指数退避 + 抖动,缓解雪崩与连接风暴
            $backoffMs = (int)(BASE_BACKOFF_MS * pow(2, $attempt - 1));
            $jitter    = random_int(0, 120);
            usleep(($backoffMs + $jitter) * 1000);
        }
    }

    // 汇总失败日志(仅服务端)
    if ($lastEx instanceof PDOException) {
        $sqlState   = $lastEx->getCode();
        $driverCode = $lastEx->errorInfo[1] ?? null;
        error_log(sprintf(
            'DB_CONN_FAIL ref=%s attempts=%d sqlstate=%s code=%s class=%s',
            $refId,
            $attempt,
            (string)$sqlState,
            (string)$driverCode,
            get_class($lastEx)
        ));
    }

    throw $lastEx ?? new RuntimeException('Database connection failed');
}

/**
 * 生成对终端用户友好的 HTML 消息(不含敏感信息)
 */
function renderUserFriendlyError(string $refId): string
{
    $now = date('Y-m-d H:i:s');
    $safeCode = USER_MESSAGE_CODE . '-' . $refId;

    $html = <<<HTML
<div style="max-width:680px;margin:48px auto;padding:24px;border:1px solid #eee;border-radius:8px;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica,Arial,sans-serif;color:#1f2937;">
  <h2 style="margin-top:0;color:#111827;">抱歉,当前下单人数过多,系统暂时繁忙</h2>
  <p style="line-height:1.7;">
    您的订单尚未提交成功,请稍后重试。我们已为您保留购物车中的商品与优惠。
  </p>
  <ul style="line-height:1.8;margin:0 0 12px 18px;">
    <li>建议在 1–3 分钟后再次尝试</li>
    <li>如多次失败,请联系在线客服并提供参考编号</li>
  </ul>
  <div style="background:#f9fafb;border:1px dashed #d1d5db;border-radius:6px;padding:12px 14px;margin:14px 0;">
    <div style="font-size:14px;color:#374151;">参考编号:<strong>{$safeCode}</strong></div>
    <div style="font-size:12px;color:#6b7280;">时间:{$now}</div>
  </div>
  <div>
    <a href="/" style="display:inline-block;background:#111827;color:#fff;text-decoration:none;padding:10px 16px;border-radius:6px;">返回首页</a>
    <a href="javascript:location.reload()" style="display:inline-block;margin-left:8px;color:#111827;text-decoration:underline;">刷新重试</a>
  </div>
</div>
HTML;

    return $html;
}

// --- 页面控制流示例(web_page 场景) ---
$refId = strtoupper(bin2hex(random_bytes(4))); // 简短、可读的参考编号片段(不含敏感信息)

try {
    // 连接主库(写入场景不建议切换到只读库)
    $pdo = connectWithRetry(MAX_ATTEMPTS, $refId);

    // 业务逻辑(示例):此处执行创建订单等写入操作
    // $pdo->beginTransaction();
    // ... 下单相关 SQL ...
    // $pdo->commit();

    // 如果成功,这里渲染成功页面或继续后续流程
    echo "<p>订单创建成功(示例)。</p>";
} catch (Throwable $e) {
    // 统一以 503 告知页面层“暂时不可用”
    http_response_code(503);

    // 展示给终端用户的友好提示(不包含任何连接配置、主机名、端口等敏感信息)
    echo renderUserFriendlyError($refId);

    // 如需埋点或告警,可在此处调用内部告警系统(请勿把敏感信息带到外部系统)
    // alert('order-db-conn-fail', ['ref' => $refId]);
}

错误消息预览

抱歉,当前下单人数过多,系统暂时繁忙 您的订单尚未提交成功,请稍后重试。我们已为您保留购物车中的商品与优惠。

  • 建议在 1–3 分钟后再次尝试
  • 如多次失败,请联系在线客服并提供参考编号 参考编号:ORD-DB-CONN-001-AB12CD34 时间:2025-12-08 12:00:00 返回首页 | 刷新重试

(说明:参考编号与时间为示例。实际页面将显示当前时间与动态编号,不包含任何敏感信息。)

技术说明

  • 错误类型:

    • 数据库连接超时/网络抖动
    • 连接数耗尽(Too many connections / 连接池耗尽)
    • 通信链路中断(08S01)
  • 可能原因:

    • 大促高并发导致数据库或中间件(如 ProxySQL/MySQL Router)连接池打满(MySQL 1040)
    • 连接握手耗时长、DNS 解析慢、网络瞬时抖动(2002/2013)
    • 应用侧并发过高,短时间创建过多新连接;连接未及时复用或释放
    • 后端资源紧张(CPU/IO)使 MySQL 接受新连接变慢
    • PHP-FPM 进程/线程配置与数据库最大连接数不匹配,引发“惊群”效应
  • 解决建议:

    1. 容量与连接管理
      • 评估并适度提升 MySQL max_connections(同时确保 InnoDB 缓存与内存足够)
      • 使用连接中间件(ProxySQL/MySQL Router)做连接复用与速率限制,避免瞬时洪峰
      • 控制应用并发与排队:在网关/应用层设置并发上限与排队队列,避免连接风暴
    2. 应用侧优化
      • 使用重试+指数退避+抖动(代码已实现),并设置合理连接超时
      • 谨慎评估 PDO 持久连接(ATTR_PERSISTENT),配合中间件与连接生命周期管理
      • 将非关键写操作异步化或延迟执行;订单关键路径避免依赖额外外部服务
    3. 数据库与查询
      • 优化下单链路的 SQL 与索引,减少事务时间与锁等待
      • 监控 Threads_connected、Threads_running、Connection_errors_xxx、Performance Schema 事件
      • 预热连接/连接池,在大促开始前逐步升温
    4. 基础设施与观测
      • 配置与校对超时:connect_timeout、wait_timeout、net_read_timeout、net_write_timeout
      • 建立告警:连接失败率、超时率、P50/P95 连接耗时、错误码分布
      • 评估数据库主从与读写分离;写流量只落主库,避免在下单时错误回退到只读

使用说明

  • 集成位置:
    • 将上述代码封装到下单接口(web_page 场景的控制器/入口脚本)中。
    • 使用 connectWithRetry 获取 PDO 连接后,再进行订单事务逻辑。若抛异常,将自动展示友好页面消息。
  • 日志与排障:
    • 服务端 error_log 会记录每次尝试与最终失败摘要(包含参考编号 ref),方便在日志中快速定位同一请求。
    • 与客服/运营流程对齐:用户提供页面的参考编号(ORD-DB-CONN-001-XXXX),研发可在日志中检索 ref=XXXX。
  • 安全注意事项:
    • 不要在页面或外部告警中包含 DSN、主机名、端口、用户名或任意凭据。
    • 生产环境关闭 display_errors;仅将详细异常写入受控日志系统。
  • 定制与扩展:
    • 如需 API 返回 JSON,可在 catch 中返回标准化结构:{"code":"ORD-DB-CONN-001","message":"…","ref":"…"},并设置 HTTP 503。
    • 可将 MAX_ATTEMPTS、CONNECT_TIMEOUT_SEC、BASE_BACKOFF_MS 通过环境变量配置,以便按压测与大促阶段动态调整。
    • 若使用连接中间件,请在技术日志中记录中间件层面的错误码映射(仍需避免泄漏地址与凭据)。

错误消息代码

<?php
/**
 * PostgreSQL 连接错误处理(日志专用,安全脱敏)
 * 目标场景:内容推荐服务启动后连接只读从库被拒绝,SSL证书过期导致握手失败
 * 目标用户群体:developer
 * 显示位置:log_file
 */

declare(strict_types=1);

/**
 * 读取配置(建议通过环境变量注入)
 */
$config = [
    'service_name' => getenv('APP_SERVICE_NAME') ?: 'content-recommendation',
    'db_role'      => 'read-replica',
    'db_type'      => 'postgresql',
    'host'         => getenv('PGHOST') ?: 'replica-host',     // 不要在日志中输出
    'port'         => (int)(getenv('PGPORT') ?: 5432),        // 不要在日志中输出
    'dbname'       => getenv('PGDATABASE') ?: 'appdb',        // 不要在日志中输出
    'user'         => getenv('PGUSER') ?: 'appuser',          // 不要在日志中输出
    'password'     => getenv('PGPASSWORD') ?: '',             // 不要在日志中输出
    'sslmode'      => getenv('PGSSLMODE') ?: 'verify-full',   // prefer verify-full
    'sslrootcert'  => getenv('PGSSLROOTCERT') ?: null,
    'sslcert'      => getenv('PGSSLCERT') ?: null,
    'sslkey'       => getenv('PGSSLKEY') ?: null,
    'connect_timeout' => (int)(getenv('PG_CONNECT_TIMEOUT') ?: 5),
    'application_name' => getenv('PG_APP_NAME') ?: 'content-recommendation',
    'log_file'     => getenv('APP_DB_LOG') ?: '/var/log/app/db_errors.log',
];

/**
 * 建立 PDO 连接(不在日志中输出 DSN/凭据)
 */
function connectToPg(array $cfg): PDO {
    $dsnParts = [
        "host={$cfg['host']}",
        "port={$cfg['port']}",
        "dbname={$cfg['dbname']}",
        "sslmode={$cfg['sslmode']}",
        "connect_timeout={$cfg['connect_timeout']}",
        "application_name={$cfg['application_name']}",
    ];
    if (!empty($cfg['sslrootcert'])) $dsnParts[] = "sslrootcert={$cfg['sslrootcert']}";
    if (!empty($cfg['sslcert']))     $dsnParts[] = "sslcert={$cfg['sslcert']}";
    if (!empty($cfg['sslkey']))      $dsnParts[] = "sslkey={$cfg['sslkey']}";

    $dsn = 'pgsql:' . implode(';', $dsnParts);

    $options = [
        PDO::ATTR_ERRMODE            => PDO::ERRMODE_EXCEPTION,
        PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
        // 对 pgsql 而言,连接超时应使用 connect_timeout DSN 参数;ATTR_TIMEOUT 对部分驱动无效
    ];

    return new PDO($dsn, $cfg['user'], $cfg['password'], $options);
}

/**
 * 错误分类与建议
 */
function categorizePgConnectionError(PDOException $e): array {
    $msg = $e->getMessage();
    $code = $e->getCode() ?: null; // 对连接阶段 SQLSTATE 可能为空

    $classification = 'CONNECTION_FAILURE';
    $retryable = true;
    $suggestions = ['check_replica_status', 'verify_host_port', 'firewall_rules', 'pg_hba_conf'];

    $m = strtolower($msg);
    if ((str_contains($m, 'ssl') && (str_contains($m, 'expired') || str_contains($m, 'certificate verify failed') || str_contains($m, 'handshake') || str_contains($m, 'unknown ca'))) {
        $classification = 'SSL_CERT_EXPIRED';
        $retryable = false; // 证书过期不应盲目重试
        $suggestions = ['renew_certificate', 'update_ca_bundle', 'sync_system_time', 'restart_db_and_app'];
    } elseif (str_contains($m, 'connection refused') || str_contains($m, 'econnrefused') || str_contains($m, 'could not connect to server')) {
        $classification = 'CONNECTION_REFUSED';
        $retryable = true;
        $suggestions = ['check_replica_status', 'verify_host_port', 'firewall_rules', 'pg_hba_conf'];
    } elseif (str_contains($m, 'no pg_hba.conf entry')) {
        $classification = 'ACCESS_DENIED';
        $retryable = false;
        $suggestions = ['pg_hba_conf', 'server_reload'];
    } elseif (str_contains($m, 'timeout') || str_contains($m, 'timed out')) {
        $classification = 'CONNECT_TIMEOUT';
        $retryable = true;
        $suggestions = ['network_latency', 'increase_timeout', 'route_health'];
    }

    return [
        'classification' => $classification,
        'retryable' => $retryable,
        'suggestions' => $suggestions,
        'sqlstate' => $code,
    ];
}

/**
 * 日志工具(JSON 行,敏感信息脱敏)
 */
function ensureLogDir(string $logFile): void {
    $dir = dirname($logFile);
    if (!is_dir($dir)) {
        @mkdir($dir, 0750, true);
    }
}

function generateEventId(): string {
    return bin2hex(random_bytes(8)); // 16 hex chars
}

function safeHash(string $val): string {
    return 'sha256:' . substr(hash('sha256', $val), 0, 12);
}

function sanitizeMessage(string $msg): string {
    $patterns = [
        '/password\s*=\s*[^;\s]*/i',
        '/user\s*=\s*[^;\s]*/i',
        '/dbname\s*=\s*[^;\s]*/i',
        '/host\s*=\s*[^;\s]*/i',
        '/sslkey\s*=\s*[^;\s]*/i',
        '/sslcert\s*=\s*[^;\s]*/i',
        '/sslrootcert\s*=\s*[^;\s]*/i',
        '/port\s*=\s*\d+/i',
        '/(postgres:\/\/)[^\s"]+/i',
    ];
    $msg = preg_replace($patterns, '$1[redacted]', $msg);
    // 压缩多余空白
    return trim(preg_replace('/\s+/', ' ', $msg));
}

function safeLog(string $logFile, array $payload): void {
    ensureLogDir($logFile);
    $line = json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
    // message_type=3 表示写入文件
    @error_log($line . PHP_EOL, 3, $logFile);
}

/**
 * 尝试连接并在失败时记录安全日志
 */
try {
    // 生产环境:在服务启动或首次数据拉取前进行连接握手
    $pdo = connectToPg($config);
    // 可选:验证只读从库(避免误连主库)
    // $isReplica = $pdo->query("SELECT pg_is_in_recovery() AS rep")->fetchColumn();
    // if ((int)$isReplica !== 1) { /* 处理误连 */ }

} catch (PDOException $e) {
    $cat  = categorizePgConnectionError($e);
    $now  = (new DateTimeImmutable('now', new DateTimeZone('UTC')))->format('c');
    $raw  = $e->getMessage();
    $safe = sanitizeMessage($raw);

    // 为了可观测性而不泄露敏感信息:仅记录指纹
    $hostFingerprint = isset($config['host']) ? safeHash((string)$config['host']) : null;

    $payload = [
        'level'         => 'error',
        'event'         => 'DB_CONNECT_FAILED',
        'event_id'      => generateEventId(),
        'timestamp'     => $now,
        'service'       => $config['service_name'],
        'db'            => [
            'type' => $config['db_type'],
            'role' => $config['db_role'],
        ],
        'classification'=> $cat['classification'],
        'summary'       => match ($cat['classification']) {
            'SSL_CERT_EXPIRED' => '连接到只读从库失败:SSL证书已过期,握手失败',
            'CONNECTION_REFUSED'=> '连接到只读从库失败:连接被拒绝',
            'ACCESS_DENIED'    => '连接到只读从库失败:访问被拒绝(pg_hba.conf)',
            'CONNECT_TIMEOUT'  => '连接到只读从库失败:连接超时',
            default            => '连接到只读从库失败:未知连接错误',
        },
        'sqlstate'      => $cat['sqlstate'],
        'retryable'     => $cat['retryable'],
        'hints'         => $cat['suggestions'],
        'safe_excerpt'  => $safe,
        'raw_message_digest' => safeHash($raw),
        'host_fingerprint'   => $hostFingerprint,
        'php_version'   => PHP_VERSION,
    ];

    safeLog($config['log_file'], $payload);

    // 对上层调用者抛出通用异常(避免在界面或其他通道暴露细节)
    throw new RuntimeException('Database connection failed. See log for details.');
}

错误消息预览

{"level":"error","event":"DB_CONNECT_FAILED","event_id":"9f3c12ab8d4e7c21","timestamp":"2025-12-08T10:35:12+00:00","service":"content-recommendation","db":{"type":"postgresql","role":"read-replica"},"classification":"SSL_CERT_EXPIRED","summary":"连接到只读从库失败:SSL证书已过期,握手失败","sqlstate":null,"retryable":false,"hints":["renew_certificate","update_ca_bundle","sync_system_time","restart_db_and_app"],"safe_excerpt":"SSL error: handshake failure (certificate expired).","raw_message_digest":"sha256:3a91e5c0f2d1","host_fingerprint":"sha256:7c4e1a0b2f33","php_version":"8.2.12"}

技术说明

  • 错误类型:SSL_CERT_EXPIRED(SSL证书过期导致握手失败;连接“被拒绝”的直接诱因是握手阶段失败)
  • 可能原因:
    • 从库服务器端证书已过期,或中间证书链不完整
    • 客户端 CA 根证书过期/缺失,或 sslmode=verify-full 校验证书失败
    • 服务器/客户端系统时间漂移导致证书在当前时间窗内被视为无效
    • 证书续期后未重载数据库服务,仍在使用旧证书
  • 解决建议:
    1. 立即续期与部署证书
      • 在从库服务器上更新 server.crt/server.key,确保证书链完整(含中间 CA)
      • PostgreSQL 配置检查:postgresql.conf 中 ssl=on,ssl_cert_file,ssl_key_file 指向新证书
      • 执行 SELECT pg_reload_conf(); 或重启实例以生效
    2. 校验证书有效性与链路
      • openssl x509 -in server.crt -noout -dates 验证 Not Before/Not After
      • openssl verify -CAfile rootCA.pem -untrusted intermediate.pem server.crt 验证信任链
    3. 客户端校验与 CA 更新
      • 在应用侧提供最新 CA(PGSSLROOTCERT),并使用 sslmode=verify-full
      • 确保连接的主机名与证书 CN/SAN 匹配
    4. 校时与安全基线
      • 同步 NTP,确保系统时间准确
      • 证书权限正确(key 600),避免被拒
    5. 如仍出现“连接被拒绝/超时”等伴随问题
      • 检查从库服务状态与监听端口(5432)
      • 防火墙或安全组放行
      • pg_hba.conf 存在 hostssl 条目并允许客户端来源
      • 确认该节点处于只读(SELECT pg_is_in_recovery(); 应返回 true)

使用说明

  • 集成方式:
    1. 将上述代码置于应用的引导阶段(如 bootstrap 或 DB 连接工厂);
    2. 通过环境变量提供连接信息与证书路径(PGHOST/PGPORT/PGDATABASE/PGUSER/PGPASSWORD/PGSSLMODE/PGSSLROOTCERT/PGSSLCERT/PGSSLKEY);
    3. 设置 APP_DB_LOG 指向应用可写的日志文件(默认 /var/log/app/db_errors.log)。
  • 日志规范:
    • 采用一行一条 JSON 的结构化日志,便于聚合与检索;
    • 自动脱敏,绝不输出 DSN、用户名、密码、证书路径等敏感信息;
    • 提供 event_id/raw_message_digest/host_fingerprint 以便跨系统关联问题而不泄密。
  • 权限与运维:
    • 确保日志目录权限为 0750,日志文件 0640,配置日志轮转;
    • 生产环境关闭 display_errors,仅通过日志定位问题;
    • 证书续期后需 reload/restart 数据库与应用进程,确保新证书生效。
  • 测试建议:
    • 可在测试环境临时使用过期证书或错误 CA,验证“SSL_CERT_EXPIRED”路径与日志输出;
    • 暂时阻断 5432 端口验证“CONNECTION_REFUSED”分类是否准确。

错误消息代码

<?php
/**
 * 安全的 SQL Server 连接与 DNS 延迟诊断(CLI 专用)
 * - 目标:在 DNS 解析偶发变慢导致驱动超时时,向管理员输出友好且可操作的诊断信息
 * - 注意:不会在控制台输出连接字符串或密码等敏感信息
 */

if (PHP_SAPI !== 'cli') {
    fwrite(STDERR, "This script is intended for CLI only.\n");
    exit(1);
}

/*========================
  可调参数(通过环境变量覆盖)
=========================*/
$cfg = [
    'host'                  => getenv('DB_HOST') ?: '',
    'port'                  => getenv('DB_PORT') ?: '1433',
    'db'                    => getenv('DB_NAME') ?: '',
    'user'                  => getenv('DB_USER') ?: '',
    'pass'                  => getenv('DB_PASSWORD') ?: '',
    'loginTimeout'          => (int)(getenv('DB_LOGIN_TIMEOUT') ?: 5),     // 初始登录超时秒
    'maxLoginTimeout'       => (int)(getenv('DB_MAX_LOGIN_TIMEOUT') ?: 12),// 重试时的上限
    'dnsSlowThresholdMs'    => (int)(getenv('DB_DNS_SLOW_THRESHOLD_MS') ?: 150),
    'retries'               => (int)(getenv('DB_CONN_RETRIES') ?: 2),      // 总尝试次数(含主机名与IP回退)
    'logFile'               => getenv('APP_LOG') ?: '/var/log/app/db_connect.log',
    'encrypt'               => (getenv('DB_ENCRYPT') ?: 'yes') === 'yes',  // 加密连接
    'trustServerCert'       => (getenv('DB_TRUST_SERVER_CERT') ?: 'no') === 'yes', // 是否信任服务器证书(生产环境建议no)
];

if (!$cfg['host'] || !$cfg['db'] || !$cfg['user']) {
    fwrite(STDERR, "配置不完整:请设置 DB_HOST / DB_NAME / DB_USER(密码通过 DB_PASSWORD 提供)。\n");
    exit(2);
}

/*========================
  工具函数
=========================*/
function log_safe($file, $line) {
    $dir = dirname($file);
    if (!is_dir($dir)) {
        @mkdir($dir, 0770, true);
    }
    $ts = date('c');
    @file_put_contents($file, "[$ts] $line\n", FILE_APPEND);
}

function mask_value($value, $keepStart = 3, $keepEnd = 3) {
    $len = mb_strlen($value);
    if ($len <= ($keepStart + $keepEnd)) return str_repeat('*', $len);
    return mb_substr($value, 0, $keepStart) . str_repeat('*', $len - $keepStart - $keepEnd) . mb_substr($value, -$keepEnd);
}

function mask_host_for_display($host) {
    // 保留首段与末段顶级域名,其他以***遮蔽
    $parts = explode('.', $host);
    if (count($parts) <= 2) {
        return mask_value($host, 2, 2);
    }
    $first = $parts[0];
    $lastTwo = array_slice($parts, -2);
    return $first . '.***.' . implode('.', $lastTwo);
}

function dns_resolve_with_metrics($host) {
    $start = microtime(true);
    $ips = @gethostbynamel($host);
    $durationMs = (int)round((microtime(true) - $start) * 1000);
    return [
        'ok'         => is_array($ips) && count($ips) > 0,
        'ips'        => $ips ?: [],
        'durationMs' => $durationMs,
    ];
}

function sqlsrv_connect_try($server, array $cfg, &$driverName = null, &$driverErrs = []) {
    $driverErrs = [];

    // 优先使用 sqlsrv 扩展
    if (function_exists('sqlsrv_connect')) {
        $driverName = 'sqlsrv';
        $connectionInfo = [
            'Database'                   => $cfg['db'],
            'UID'                        => $cfg['user'],
            'PWD'                        => $cfg['pass'],
            'LoginTimeout'               => (int)$cfg['loginTimeout'],
            'MultipleActiveResultSets'   => true,
            'ConnectionPooling'          => true,
            'Encrypt'                    => $cfg['encrypt'],
            'TrustServerCertificate'     => $cfg['trustServerCert'],
            // 跨子网连接优化(AlwaysOn 场景)
            'MultiSubnetFailover'        => true,
        ];

        $conn = @sqlsrv_connect($server, $connectionInfo);
        if (!$conn) {
            $errs = sqlsrv_errors(SQLSRV_ERR_ALL);
            $driverErrs = is_array($errs) ? $errs : [];
        }
        return $conn;
    }

    // 退回 PDO_SQLSRV
    if (extension_loaded('pdo_sqlsrv')) {
        $driverName = 'pdo_sqlsrv';

        $dsn = sprintf(
            'sqlsrv:Server=%s;Database=%s;LoginTimeout=%d;Encrypt=%d;TrustServerCertificate=%d;MultiSubnetFailover=1',
            $server,
            $cfg['db'],
            (int)$cfg['loginTimeout'],
            $cfg['encrypt'] ? 1 : 0,
            $cfg['trustServerCert'] ? 1 : 0
        );

        try {
            $pdo = new PDO($dsn, $cfg['user'], $cfg['pass'], [
                PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
                PDO::ATTR_TIMEOUT => (int)$cfg['loginTimeout'],
            ]);
            return $pdo;
        } catch (PDOException $e) {
            $driverErrs = [[
                'SQLSTATE' => $e->getCode(),
                'code'     => 0,
                'message'  => $e->getMessage(),
            ]];
            return false;
        }
    }

    $driverName = 'none';
    $driverErrs = [[
        'SQLSTATE' => null,
        'code'     => null,
        'message'  => 'Neither sqlsrv nor pdo_sqlsrv extension is available.',
    ]];
    return false;
}

function classify_sql_errors(array $errs) {
    // 简要分类:是否为超时/网络/DNS类问题
    $codes = [];
    $msgs  = [];
    foreach ($errs as $e) {
        $codes[] = strtoupper((string)($e['SQLSTATE'] ?? ''));
        $msgs[]  = strtolower((string)($e['message'] ?? ''));
    }
    $all = implode(' | ', $msgs);
    $isTimeout = (stripos($all, 'timeout') !== false) || in_array('HYT00', $codes, true) || in_array('S1T00', $codes, true);
    $isDns     = (stripos($all, 'dns') !== false) || (stripos($all, '11001') !== false) || (stripos($all, 'name or service not known') !== false);
    $isNetwork = (stripos($all, '08001') !== false) || (stripos($all, '10060') !== false) || (stripos($all, '10061') !== false);
    return [$isTimeout, $isDns, $isNetwork];
}

/*========================
  主流程
=========================*/
$maskedHost = mask_host_for_display($cfg['host']);
$serverWithPort = "tcp:{$cfg['host']},{$cfg['port']}";

$dns = dns_resolve_with_metrics($cfg['host']);
$dnsSlow = $dns['durationMs'] >= $cfg['dnsSlowThresholdMs'];

$attempts = [];
$connected = false;
$conn = null;
$driver = null;
$driverErrs = [];

// 第一次:使用主机名连接
$attempts[] = [
    'label' => 'hostname',
    'server' => $serverWithPort,
    'timeout' => $cfg['loginTimeout'],
];

$resolvedIp = $dns['ok'] ? $dns['ips'][0] : null;
if ($resolvedIp) {
    $attempts[] = [
        'label' => 'ip-fallback',
        'server' => "tcp:{$resolvedIp},{$cfg['port']}",
        'timeout' => min($cfg['maxLoginTimeout'], $cfg['loginTimeout'] + 4),
    ];
}

// 控制最大尝试次数
$attempts = array_slice($attempts, 0, max(1, $cfg['retries']));

$errorsCollected = [];
foreach ($attempts as $i => $a) {
    $cfg['loginTimeout'] = (int)$a['timeout'];
    $conn = sqlsrv_connect_try($a['server'], $cfg, $driver, $driverErrs);
    if ($conn) {
        $connected = true;
        break;
    }
    $errorsCollected[] = [
        'attempt'   => $a['label'],
        'server'    => $a['server'], // 日志用,不在CLI输出
        'timeout'   => $a['timeout'],
        'driver'    => $driver,
        'errors'    => $driverErrs,
    ];

    // 指数回退短暂等待(避免立即拥塞)
    usleep(200000 * ($i + 1)); // 200ms, 400ms...
}

// 成功则输出简短通过信息(管理员可见,不含敏感)
if ($connected) {
    echo "数据库连接检查通过:SQL Server 已连接。DNS解析耗时 {$dns['durationMs']} ms。驱动={$driver}。\n";
    exit(0);
}

/*========================
  失败:输出面向管理员的友好诊断
=========================*/
[$isTimeout, $isDns, $isNetwork] = classify_sql_errors(array_merge(...array_map(fn($e) => $e['errors'], $errorsCollected)));

$errorId = 'DB_CONN_DNS_TIMEOUT';
$lines = [];
$lines[] = "错误:登录模块无法连接数据库(SQL Server)";
$lines[] = "错误编号:{$errorId}";
$lines[] = "影响范围:用户登录验证暂时不可用";
$lines[] = "";
$lines[] = "关键信息:";
$lines[] = "- 目标主机(遮蔽):{$maskedHost}:{$cfg['port']}";
$lines[] = "- 目标数据库:".mask_value($cfg['db'], 1, 1);
$lines[] = "- DNS 解析:".($dns['ok'] ? "成功,用时 {$dns['durationMs']} ms" : "失败");
$lines[] = "- 连接尝试:".count($attempts)." 次(含 IP 回退:".($resolvedIp ? "是" : "否").")";
$lines[] = "- 触发类型:".(
    $isDns ? "DNS 解析/路由延迟" :
    ($isTimeout ? "连接超时" :
    ($isNetwork ? "网络不可达/防火墙" : "未知"))
);
$lines[] = "";

$lines[] = "驱动返回(节选):";
if (!empty($errorsCollected)) {
    $firstErrs = $errorsCollected[0]['errors'] ?: [];
    $maxShow = 2;
    $count = 0;
    foreach ($firstErrs as $e) {
        $count++;
        if ($count > $maxShow) break;
        $sqlstate = $e['SQLSTATE'] ?? '';
        $code     = $e['code'] ?? '';
        $msg      = trim(preg_replace('/\s+/', ' ', (string)($e['message'] ?? '')));
        $msg      = mb_strimwidth($msg, 0, 220, '...');
        $lines[]  = "- SQLSTATE={$sqlstate}; CODE={$code}; MSG={$msg}";
    }
} else {
    $lines[] = "- 无可用的驱动错误详情";
}
$lines[] = "";

$lines[] = "可能原因:";
$lines[] = "- 跨区域 DNS 解析偶发变慢或缓存未命中,导致登录超时(HYT00 等)。";
$lines[] = "- 网络抖动/丢包,或防火墙对 1433 端口限速。";
$lines[] = "- 多子网/异地容灾场景中,连接路径切换导致初次建连耗时。";
$lines[] = "";

$lines[] = "建议操作:";
$lines[] = "1) 立即缓解:";
$lines[] = "   - 提高登录超时至 10~15 秒(DB_LOGIN_TIMEOUT),并开启 IP 回退(本脚本已内置)。";
$lines[] = "   - 在应用侧启用连接重试,或短期在本机配置 hosts 静态解析(仅应急,变更需记录)。";
$lines[] = "2) 排查验证:";
$lines[] = "   - 测试 DNS 时延:nslookup {$cfg['host']} 或 dig {$cfg['host']} 并记录耗时/TTL。";
$lines[] = "   - 连通性检查:telnet {$maskedHost} 1433 或 Test-NetConnection(Windows)。";
$lines[] = "   - 查看防火墙与安全组是否对跨区域流量限速/丢弃。";
$lines[] = "3) 长期优化:";
$lines[] = "   - 启用/确认 MultiSubnetFailover=Yes(已在连接参数中开启)。";
$lines[] = "   - 引入就近 DNS/缓存(如 systemd-resolved、nscd)并监控解析时延与命中率。";
$lines[] = "   - 与 DBA/网络团队确认跨区域链路与 SQL 监听端点策略,必要时提供就近只读/写入端点。";
$lines[] = "";
$lines[] = "提示:为安全起见,已隐藏连接字符串和密码。详细技术信息请查看应用日志。";
$finalMessage = implode("\n", $lines);

echo $finalMessage . "\n";

/*========================
  安全日志(包含更详细上下文,但仍不记录密码)
=========================*/
$logCtx = [
    'driver'       => $driver,
    'host'         => $cfg['host'],
    'port'         => $cfg['port'],
    'db'           => $cfg['db'],
    'user_masked'  => mask_value($cfg['user'], 1, 1),
    'dns_ok'       => $dns['ok'],
    'dns_ms'       => $dns['durationMs'],
    'attempts'     => array_map(function($a){ return ['label'=>$a['label'],'timeout'=>$a['timeout']]; }, $attempts),
    'errors'       => $errorsCollected,
];
log_safe($cfg['logFile'], 'DB_CONNECT_FAIL '.json_encode($logCtx, JSON_UNESCAPED_UNICODE));

exit(70); // EX_SOFTWARE

错误消息预览

错误:登录模块无法连接数据库(SQL Server) 错误编号:DB_CONN_DNS_TIMEOUT 影响范围:用户登录验证暂时不可用

关键信息:

  • 目标主机(遮蔽):db-login.***.example.com:1433
  • 目标数据库:p***d
  • DNS 解析:成功,用时 312 ms
  • 连接尝试:2 次(含 IP 回退:是)
  • 触发类型:DNS 解析/路由延迟

驱动返回(节选):

  • SQLSTATE=HYT00; CODE=0; MSG=Login timeout expired
  • SQLSTATE=08001; CODE=10060; MSG=A network-related or instance-specific error occurred...

可能原因:

  • 跨区域 DNS 解析偶发变慢或缓存未命中,导致登录超时(HYT00 等)。
  • 网络抖动/丢包,或防火墙对 1433 端口限速。
  • 多子网/异地容灾场景中,连接路径切换导致初次建连耗时。

建议操作:

  1. 立即缓解:
    • 提高登录超时至 10~15 秒(DB_LOGIN_TIMEOUT),并开启 IP 回退(本脚本已内置)。
    • 在应用侧启用连接重试,或短期在本机配置 hosts 静态解析(仅应急,变更需记录)。
  2. 排查验证:
    • 测试 DNS 时延:nslookup <真实主机名> 或 dig <真实主机名> 并记录耗时/TTL。
    • 连通性检查:telnet <目标> 1433 或 Test-NetConnection(Windows)。
    • 查看防火墙与安全组是否对跨区域流量限速/丢弃。
  3. 长期优化:
    • 启用/确认 MultiSubnetFailover=Yes(已在连接参数中开启)。
    • 引入就近 DNS/缓存(如 systemd-resolved、nscd)并监控解析时延与命中率。
    • 与 DBA/网络团队确认跨区域链路与 SQL 监听端点策略,必要时提供就近只读/写入端点。

提示:为安全起见,已隐藏连接字符串和密码。详细技术信息请查看应用日志。

技术说明

  • 错误类型:SQL Server 连接失败(DNS 解析延迟引发的驱动登录超时 HYT00/08001 等)
  • 可能原因:
    • 跨区域 DNS 解析延迟或缓存未命中导致初次连接耗时偏高
    • 网络链路抖动、丢包或防火墙策略导致 TCP 1433 建连超时
    • 多子网/异地容灾(AlwaysOn Listener)在故障转移或路径切换时连接建立变慢
  • 解决建议:
    • 提高 LoginTimeout 至 10~15 秒并启用有限重试,降低瞬时抖动对业务的影响
    • 保持加密连接 Encrypt=Yes,必要时与证书策略一致后再考虑 TrustServerCertificate
    • 在驱动参数中启用 MultiSubnetFailover=Yes(已开启)以优化多子网探测
    • 部署/启用本地 DNS 缓存或边缘 DNS,加大热点记录 TTL 并监控解析时延
    • 用 nslookup/dig/Resolve-DnsName 测试解析时间,配合 ping/telnet/Test-NetConnection 验证端口可达
    • 与网络/DBA 校验安全组、ACL、跨区域链路质量与带宽;必要时提供就近访问的专用读/写端点

使用说明

  • 集成方式:

    1. 确保安装 sqlsrv 或 pdo_sqlsrv(优先使用 sqlsrv)
    2. 设置环境变量:
      • DB_HOST, DB_PORT, DB_NAME, DB_USER, DB_PASSWORD
      • 可选:DB_LOGIN_TIMEOUT(默认5秒)、DB_MAX_LOGIN_TIMEOUT(默认12秒)
      • 可选:DB_DNS_SLOW_THRESHOLD_MS(默认150ms)、DB_CONN_RETRIES(默认2)
      • 可选:DB_ENCRYPT=yes/no(默认yes)、DB_TRUST_SERVER_CERT=yes/no(默认no)
      • 可选:APP_LOG(默认 /var/log/app/db_connect.log)
    3. 在命令行运行此脚本,失败时会输出面向管理员的诊断信息,并记录安全日志用于深入排查。
  • 注意事项:

    • 控制台输出不会包含连接字符串或密码;日志同样不记录密码。
    • 若在生产环境短期启用 hosts 静态解析,请记录变更并在根因修复后撤销。
    • 如使用加密连接且出现证书相关错误,请与安全与DBA团队确认证书链与主机名一致性,不建议长期启用 TrustServerCertificate。
    • 返回码 0 表示连接成功;70 表示软件类错误(连接失败)。

示例详情

解决的问题

以一条可复用的专业提示词,自动生成“会说人话”的数据库连接错误消息与可直接落地的PHP处理代码,帮助研发与产品团队:

  • 在电商下单、内容加载、用户登录等关键环节,快速识别连接故障的核心原因;
  • 按不同受众(终端用户、客服、开发、运维)自动分层输出合适的话术与行动指引;
  • 严控信息暴露,避免主机、账号、库名等敏感信息泄露;
  • 统一团队错误提示风格与排障流程,沉淀成企业级标准库;
  • 明显缩短排障与恢复时间,减少投诉与流失,提升转化与口碑。 试用即上手:选择数据库类型→描述错误场景→指定受众与展示位置,立即得到可复制使用的消息与代码片段。

适用用户

PHP后端工程师

在开发或排障时,一键生成可用的错误处理代码与友好提示;快速锁定原因、给出排查步骤,统一团队消息规范。

技术支持与运维

将复杂报错转化为易懂说明,用于状态页、告警或工单回复;提供替代操作与自助引导,减少重复沟通。

中小企业站长与自由开发者

无需深厚经验,复制即用;为电商下单、CMS读取、用户登录等场景配置可靠提示,降低流失与投诉。

特征总结

自动分析数据库类型与场景,一键生成贴合业务的错误提示与修复建议
按目标用户分层输出文案,技术与普通用户各得其所,降低沟通成本
自动生成可直接嵌入的PHP处理代码,复制即用,快速上线不改架构
内置安全校验,避免泄露连接信息与服务器细节,守住合规与信任感
提供分步排查清单与备用方案,遇到高并发或网络波动也能稳住体验
支持电商、CMS、登录等高频场景,自动匹配语气与展示位置的差异
可参数化配置品牌语调与风格,保持跨项目一致性,塑造专业形象力
自动优化表述与结构,突出可执行步骤,减少无效信息与用户焦虑感
生成中文与英文双语版本,方便对外支持与全球团队协作和知识库沉淀

如何使用购买的提示词模板

1. 直接在外部 Chat 应用中使用

将模板生成的提示词复制粘贴到您常用的 Chat 应用(如 ChatGPT、Claude 等),即可直接对话使用,无需额外开发。适合个人快速体验和轻量使用场景。

2. 发布为 API 接口调用

把提示词模板转化为 API,您的程序可任意修改模板参数,通过接口直接调用,轻松实现自动化与批量处理。适合开发者集成与业务系统嵌入。

3. 在 MCP Client 中配置使用

在 MCP client 中配置对应的 server 地址,让您的 AI 应用自动调用提示词模板。适合高级用户和团队协作,让提示词在不同 AI 工具间无缝衔接。

AI 提示词价格
¥20.00元
先用后买,用好了再付款,超安全!

您购买后可以获得什么

获得完整提示词模板
- 共 573 tokens
- 4 个可调节参数
{ 数据库类型 } { 错误场景描述 } { 目标用户群体 } { 显示位置 }
获得社区贡献内容的使用权
- 精选社区优质案例,助您快速上手提示词
使用提示词兑换券,低至 ¥ 9.9
了解兑换券 →
限时半价

不要错过!

半价获取高级提示词-优惠即将到期

17
:
23
小时
:
59
分钟
:
59