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

399 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 未安装 `sqlsrv``pdo_sqlsrv` 扩展。
### 解决方案
1. 下载对应 PHP 版本的驱动:
- 下载地址:[Microsoft Drivers for PHP for SQL Server](https://learn.microsoft.com/zh-cn/sql/connect/php/download-drivers-php-sql-server?view=sql-server-ver16)
- 版本选择:需匹配 PHP 版本、NTS/TS、x86/x64
2. 将驱动文件复制到 PHP 扩展目录:
- `php_sqlsrv.dll`
- `php_pdo_sqlsrv.dll`
3.`php.ini` 中启用扩展:
```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
- 下载地址:[ODBC Driver for SQL Server](https://learn.microsoft.com/zh-cn/sql/connect/odbc/download-odbc-driver-for-sql-server)
2. 安装 Visual C++ Redistributable 2017
- 下载地址:[Visual Studio 2017 Redistributable](https://learn.microsoft.com/zh-cn/cpp/windows/latest-supported-vc-redist?view=msvc-170)
---
## 问题三SQL Server 端口配置错误
### 现象
```
用户 'xxx' 登录失败
```
### 原因
SQL Server 未使用默认端口 1433而是使用了动态端口。
### 解决方案
1. 在 SQL Server 上查询实际端口:
```sql
SELECT local_tcp_port FROM sys.dm_exec_connections WHERE session_id = @@SPID;
```
2. 修改 `.env` 配置文件:
```ini
[MSSQL]
HOSTNAME = 192.168.0.156
HOSTPORT = 50109 ; 实际端口
```
3. 使用 IP+端口方式连接,避免使用实例名。
---
## 问题四参数绑定错误HY093
### 现象
```
SQLSTATE[HY093]: Invalid parameter number: parameter was not defined
```
### 原因分析
#### 子问题 4.1SQL Server PDO 不支持表达式中的参数
**错误代码**
```php
// SQL 模板
WHERE RowID BETWEEN (:offset + 1) AND (:offset + :limit)
// 参数绑定
$params = [':offset' => 0, ':limit' => 20];
```
**问题**SQL Server 的 PDO 驱动不支持在算术表达式中使用命名参数。
**解决方案**:预先计算分页范围,传递单个参数:
```php
$startRow = ($page - 1) * $limit + 1;
$endRow = $page * $limit;
$params = [':start_row' => $startRow, ':end_row' => $endRow];
```
SQL 模板改为:
```sql
WHERE RowID BETWEEN :start_row AND :end_row
```
#### 子问题 4.2:统计查询与数据查询参数不匹配
**错误代码**
```php
$allParams = array_merge($dateParams, $isolationParams, [':start_row', ':end_row']);
Db::query($countSql, $allParams); // countSql 中没有 :start_row 和 :end_row
```
**问题**countSql统计查询和 dataSql数据查询使用了相同的参数数组但 countSql 不需要分页参数。
**解决方案**:将参数数组分开:
```php
$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 混淆
**错误代码**
```php
$logSql = self::buildLogSql($sqlTemplate, $dateCondition, $isolationSql, $allParams);
$dataSql = $logSql; // $logSql 已替换参数值
Db::query($dataSql, $allParams); // 参数绑定失败
```
**问题**`buildLogSql` 函数返回的 SQL 已经把参数值替换进去(用于日志),但代码用这个 SQL 去做参数绑定,导致 PDO 找不到占位符。
**解决方案**:让 `buildLogSql` 返回两个版本的 SQL
```php
return ['dataSql' => $dataSql, 'logSql' => $logSql];
```
---
## 问题五参数前缀错误IMSSP
### 现象
```
SQLSTATE[IMSSP]: Tried to bind parameter number 65536. SQL Server supports a maximum of 2100 parameters.
```
### 原因分析
#### 子问题 5.1:手动使用 `@` 前缀导致参数编号溢出
**错误代码**
```php
// SQL 模板
WHERE [登记时间] >= @start_date
// 参数绑定
$params = ['@start_date' => $startDate];
```
**问题**SQL Server 的 PDO 驱动在手动使用 `bindValue` 时,会把 `@start_date` 解析为参数编号 655360xFFFF导致参数编号溢出。
**尝试方案 1失败**:改用 `:` 前缀
```php
// SQL 模板
WHERE [登记时间] >= :start_date
// 参数绑定
$params = [':start_date' => $startDate];
```
**结果**:仍然报错 `HY093: Invalid parameter number`,因为 SQL Server 的 PDO 驱动在 CTEWITH 子句)中对 `:` 前缀的参数支持不佳。
#### 子问题 5.2ThinkPHP 自动添加 `:` 前缀
**根本原因**ThinkPHP 的 `PDOConnection` 类在 `bindValue` 方法中会自动添加 `:` 前缀:
```php
// 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` ✅
### 解决方案
**正确的参数绑定方式**
```php
// 1. SQL 模板中使用 : 前缀
$dateCondition = " AND [登记时间] >= :start_date";
// 2. 参数数组中使用无前缀键名
$dateParams['start_date'] = $startDate;
// 3. ThinkPHP 自动处理为 :start_date → 绑定成功!
```
**完整示例**
```php
// 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 函数修改**
```php
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 语句,难以排查问题。
### 解决方案
在查询执行前后添加详细日志:
```php
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`
```php
// 修改前
WHERE RowID BETWEEN (:offset + 1) AND (:offset + :limit)
// 修改后
WHERE RowID BETWEEN :start_row AND :end_row
```
### 2. 参数绑定修改
```php
// 修改前
$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 函数修改
```php
// 修改前
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 用 `:` 前缀 |
---
## 参考文献
- [Microsoft Drivers for PHP for SQL Server](https://learn.microsoft.com/zh-cn/sql/connect/php/download-drivers-php-sql-server)
- [ODBC Driver for SQL Server](https://learn.microsoft.com/zh-cn/sql/connect/odbc/download-odbc-driver-for-sql-server)
- [ThinkPHP 数据库操作](https://www.kancloud.cn/manual/thinkphp6_0/1037537)