Files
docs/thinkphp/采坑文档.md
cenzuhai 3a04eede84 feat: 初始化进销存数据开放平台项目
初始化完整的前后端项目结构,包含ThinkPHP后端API服务和Next.js前端管理系统,添加基础配置、中间件、模型、路由等核心代码,以及项目文档和静态资源。
2026-07-08 17:48:00 +08:00

11 KiB
Raw Blame History

SQL Server 连接与查询采坑文档

问题概述

本项目在 PHP + ThinkPHP 框架下连接 SQL Server 数据库时,遇到了一系列连接和查询问题,现整理如下。


问题一PHP 缺少 SQL Server 驱动

现象

SQLSTATE[IMSSP]: This extension requires the Microsoft ODBC Driver for SQL Server to communicate with SQL Server

原因

PHP 未安装 sqlsrvpdo_sqlsrv 扩展。

解决方案

  1. 下载对应 PHP 版本的驱动:

  2. 将驱动文件复制到 PHP 扩展目录:

    • php_sqlsrv.dll
    • php_pdo_sqlsrv.dll
  3. php.ini 中启用扩展:

    extension=php_sqlsrv.dll
    extension=php_pdo_sqlsrv.dll
    
  4. 重启 PHP 服务。

注意事项

  • Windows 7 兼容性SQLSRV 5.13.1 支持 Windows 7但需要安装 Visual C++ Redistributable 2017。
  • 避免重复加载:检查 php.ini 中是否有重复的扩展配置。

问题二:缺少 ODBC Driver 依赖

现象

This extension requires the Microsoft ODBC Driver for SQL Server

原因

SQLSRV 扩展依赖 Microsoft ODBC Driver for SQL Server。

解决方案

  1. 安装 ODBC Driver 17 for SQL Server

  2. 安装 Visual C++ Redistributable 2017


问题三SQL Server 端口配置错误

现象

用户 'xxx' 登录失败

原因

SQL Server 未使用默认端口 1433而是使用了动态端口。

解决方案

  1. 在 SQL Server 上查询实际端口:

    SELECT local_tcp_port FROM sys.dm_exec_connections WHERE session_id = @@SPID;
    
  2. 修改 .env 配置文件:

    [MSSQL]
    HOSTNAME = 192.168.0.156
    HOSTPORT = 50109  ; 实际端口
    
  3. 使用 IP+端口方式连接,避免使用实例名。


问题四参数绑定错误HY093

现象

SQLSTATE[HY093]: Invalid parameter number: parameter was not defined

原因分析

子问题 4.1SQL Server PDO 不支持表达式中的参数

错误代码

// SQL 模板
WHERE RowID BETWEEN (:offset + 1) AND (:offset + :limit)

// 参数绑定
$params = [':offset' => 0, ':limit' => 20];

问题SQL Server 的 PDO 驱动不支持在算术表达式中使用命名参数。

解决方案:预先计算分页范围,传递单个参数:

$startRow = ($page - 1) * $limit + 1;
$endRow = $page * $limit;
$params = [':start_row' => $startRow, ':end_row' => $endRow];

SQL 模板改为:

WHERE RowID BETWEEN :start_row AND :end_row

子问题 4.2:统计查询与数据查询参数不匹配

错误代码

$allParams = array_merge($dateParams, $isolationParams, [':start_row', ':end_row']);
Db::query($countSql, $allParams);  // countSql 中没有 :start_row 和 :end_row

问题countSql统计查询和 dataSql数据查询使用了相同的参数数组但 countSql 不需要分页参数。

解决方案:将参数数组分开:

$countParams = array_merge($dateParams, $isolationParams);
$dataParams = array_merge($countParams, [':start_row', ':end_row']);

Db::query($countSql, $countParams);
Db::query($dataSql, $dataParams);

子问题 4.3:日志 SQL 与执行 SQL 混淆

错误代码

$logSql = self::buildLogSql($sqlTemplate, $dateCondition, $isolationSql, $allParams);
$dataSql = $logSql;  // $logSql 已替换参数值
Db::query($dataSql, $allParams);  // 参数绑定失败

问题buildLogSql 函数返回的 SQL 已经把参数值替换进去(用于日志),但代码用这个 SQL 去做参数绑定,导致 PDO 找不到占位符。

解决方案:让 buildLogSql 返回两个版本的 SQL

return ['dataSql' => $dataSql, 'logSql' => $logSql];

问题五参数前缀错误IMSSP

现象

SQLSTATE[IMSSP]: Tried to bind parameter number 65536. SQL Server supports a maximum of 2100 parameters.

原因分析

子问题 5.1:手动使用 @ 前缀导致参数编号溢出

错误代码

// SQL 模板
WHERE [登记时间] >= @start_date

// 参数绑定
$params = ['@start_date' => $startDate];

问题SQL Server 的 PDO 驱动在手动使用 bindValue 时,会把 @start_date 解析为参数编号 655360xFFFF导致参数编号溢出。

尝试方案 1失败:改用 : 前缀

// SQL 模板
WHERE [登记时间] >= :start_date

// 参数绑定
$params = [':start_date' => $startDate];

结果:仍然报错 HY093: Invalid parameter number,因为 SQL Server 的 PDO 驱动在 CTEWITH 子句)中对 : 前缀的参数支持不佳。

子问题 5.2ThinkPHP 自动添加 : 前缀

根本原因ThinkPHP 的 PDOConnection 类在 bindValue 方法中会自动添加 : 前缀:

// vendor/topthink/think-orm/src/db/PDOConnection.php:1358
protected function bindValue(array $bind = []): void
{
    foreach ($bind as $key => $val) {
        // 占位符
        $param = is_numeric($key) ? $key + 1 : ':' . $key;  // 自动添加 : 前缀
        // ...
    }
}

这意味着:

  • 参数 @start_date → 变成 :@start_date
  • 参数 :start_date → 变成 ::start_date
  • 参数 start_date → 变成 :start_date

解决方案

正确的参数绑定方式

// 1. SQL 模板中使用 : 前缀
$dateCondition = " AND [登记时间] >= :start_date";

// 2. 参数数组中使用无前缀键名
$dateParams['start_date'] = $startDate;

// 3. ThinkPHP 自动处理为 :start_date → 绑定成功!

完整示例

// SQL 模板
WHERE [登记时间] >= :start_date AND [登记时间] <= :end_date
  AND [销方全称] = :iso_0

// 参数数组
$params = [
    'start_date' => '2024-02-02',
    'end_date' => '2026-06-22',
    'iso_0' => '喀喇沁旗中医蒙医医院'
];

// 执行查询
Db::connect('mssql')->query($sql, $params);

buildLogSql 函数修改

private static function buildLogSql($sqlTemplate, $dateCondition, $isolationSql, $allParams)
{
    $dataSql = str_replace(['{date_condition}', '{isolation_condition}'], [$dateCondition, $isolationSql], $sqlTemplate);
    $logSql = $dataSql;

    foreach ($allParams as $key => $value) {
        $placeholder = ':' . $key;  // 替换时自动添加 : 前缀
        if (is_string($value)) {
            $logSql = str_replace($placeholder, "'" . $value . "'", $logSql);
        } else {
            $logSql = str_replace($placeholder, (string)$value, $logSql);
        }
    }

    return ['dataSql' => $dataSql, 'logSql' => $logSql];
}

修改的文件

  1. MssqlQuery.php

    • queryChuku 方法:参数键名从 :start_date 改为 start_date
    • queryChukuReal 方法:参数键名从 @start_row 改为 start_row
    • queryRuku 方法:参数键名从 :start_date 改为 start_date
    • queryRukuReal 方法:参数键名从 @start_row 改为 start_row
    • SQL 模板:保持 : 前缀占位符
    • buildLogSql 方法:替换时自动添加 : 前缀
  2. DataIsolation.php

    • buildCustomFilter 方法:参数键名从 @iso_0 改为 iso_0
    • buildAutoFilter 方法:参数键名从 @iso_0 改为 iso_0

总结

方案 SQL 占位符 参数键名 ThinkPHP 处理后 结果
错误 1 @start_date @start_date :@start_date IMSSP 错误
错误 2 :start_date :start_date ::start_date HY093 错误
正确 :start_date start_date :start_date 成功

问题六:日志记录缺失

现象

无法看到生成的 SQL 语句,难以排查问题。

解决方案

在查询执行前后添加详细日志:

Log::debug("出库查询 - 统计SQL: " . $countSql);
Log::debug("出库查询 - 统计参数: " . json_encode($countParams));
Log::debug("出库查询 - 数据SQL: " . $dataSql);
Log::debug("出库查询 - 数据参数: " . json_encode($dataParams));

排查流程总结

连接失败
    ↓
检查 PHP 扩展是否安装
    ↓
检查 ODBC Driver 是否安装
    ↓
检查 SQL Server 端口配置
    ↓
连接成功但查询失败
    ↓
检查 SQL 语法是否正确
    ↓
检查参数绑定是否匹配
    ↓
添加日志记录调试
    ↓
定位具体问题并修复

关键代码修改

1. SQL 模板修改

文件app/service/MssqlQuery.php

// 修改前
WHERE RowID BETWEEN (:offset + 1) AND (:offset + :limit)

// 修改后
WHERE RowID BETWEEN :start_row AND :end_row

2. 参数绑定修改

// 修改前
$offset = ($page - 1) * $limit;
$allParams = array_merge($dateParams, $isolationParams, [
    ':offset' => $offset,
    ':limit' => $limit
]);

// 修改后
$startRow = ($page - 1) * $limit + 1;
$endRow = $page * $limit;
$countParams = array_merge($dateParams, $isolationParams);
$dataParams = array_merge($countParams, [
    ':start_row' => $startRow,
    ':end_row' => $endRow
]);

3. buildLogSql 函数修改

// 修改前
private static function buildLogSql(...)
{
    // ...
    return $logSql;
}

// 修改后
private static function buildLogSql(...)
{
    // ...
    return ['dataSql' => $dataSql, 'logSql' => $logSql];
}

总结

问题 根本原因 解决方案
驱动缺失 PHP 未安装 sqlsrv 扩展 安装对应版本驱动
ODBC 依赖 缺少 ODBC Driver 安装 ODBC Driver 17
端口错误 SQL Server 使用动态端口 查询实际端口并配置
参数表达式 SQL Server PDO 不支持 预先计算参数值
参数不匹配 统计/数据查询共用参数 分开参数数组
日志混淆 执行 SQL 使用了日志版本 返回分离的 SQL
参数前缀错误 ThinkPHP 自动添加 : 前缀 参数键名用无前缀SQL 用 : 前缀

参考文献