feat: 初始化进销存数据开放平台项目

初始化完整的前后端项目结构,包含ThinkPHP后端API服务和Next.js前端管理系统,添加基础配置、中间件、模型、路由等核心代码,以及项目文档和静态资源。
This commit is contained in:
cenzuhai
2026-07-08 17:48:00 +08:00
parent 6713e13ea6
commit 3a04eede84
99 changed files with 17002 additions and 0 deletions

399
thinkphp/采坑文档.md Normal file
View File

@@ -0,0 +1,399 @@
# 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)