本地端口映射
WSL搭建的服务,通过母机端口提供给局域网访问
netsh interface portproxy add v4tov4 listenport=8080 listenaddress=0.0.0.0 connectport=80 connectaddress=127.0.0.1 WSL搭建的服务,通过母机端口提供给局域网访问
netsh interface portproxy add v4tov4 listenport=8080 listenaddress=0.0.0.0 connectport=80 connectaddress=127.0.0.1 要获取标题数量对应的列名称(如 A、B、C...),可以通过标题数量计算对应的 Excel 列名。Excel 列名采用字母编号(A-Z, AA-AZ, BA-BZ...),可以编写一个转换函数来实现数字到列名的转换。
以下是结合标题数量获取对应列名称的示例代码:
// 数字转Excel列名(如1->A, 2->B, 26->Z, 27->AA)
function numberToColumn($number) {
$column = '';
while ($number > 0) {
$number--;
$column = chr(ord('A') + $number % 26) . $column;
$number = (int) ($number / 26);
}
return $column;
} control/interface.php
function formSubArray($data): array|string
{
if (is_array($data)) {
$result = [];
foreach ($data as $key => $value) {
if (is_array($value)) {
$result[$key] = formSubArray($value);
} else {
$result[$key] = formSub($value);
}
}
} else {
$result = formSub($data);
}
return $result;
}
/*表单提交数据整理和防sql注入*/
function formSub($data): string
{
$data = trim($data); //消除两边的空格
$data = htmlentities($data, ENT_QUOTES, "utf-8"); //字符转换为 HTML 实体。
//对单引号(')双引号(")反斜杠(\)NULL进行转义
return addslashes($data);
}/cmd.php
#! /usr/bin/env php
<?php
$_SERVER['HTTP_HOST'] = '';
$_SERVER['REQUEST_URI'] = '';
require_once __DIR__ . '/shell/baseCommand.php';
class CommandRunner
{
/** @var string 控制器名称 */
protected $controllerName;
/** @var string 动作名称 */
protected $actionName;
/** @var array 命令行参数 */
protected $params = [];
/** @var array 控制器映射 */
protected $controllerMap = [];
/** @var array 颜色定义 */
protected $colors = [
'reset' => "\033[0m",
'red' => "\033[31m",
'green' => "\033[32m",
'yellow' => "\033[33m",
'blue' => "\033[34m",
'purple' => "\033[35m",
'cyan' => "\033[36m",
];
/**
* 构造函数
*/
public function __construct()
{
$this->initControllerMap();
}
/**
* 初始化控制器映射
* 子类应重写此方法来定义控制器映射
*/
protected function initControllerMap()
{
// 示例控制器映射
$this->controllerMap = [];
//遍历shell目录下的所有Command结尾的类文件,加入控制器映射中
$dir = __DIR__ . '/shell';
$files = scandir($dir);
foreach ($files as $file) {
if (is_file($dir . '/' . $file) && str_ends_with($file, 'Command.php')) {
$controllerName = substr($file, 0, -11);
$this->controllerMap[$controllerName] = "shell\\{$controllerName}Command";
}
}
}
/**
* 运行命令行脚本
*/
public function run()
{
try {
$this->parseArguments();
$this->validateCommand();
$this->executeAction();
} catch (Exception $e) {
$this->error($e->getMessage());
exit(1);
}
}
/**
* 解析命令行参数
*/
protected function parseArguments()
{
global $argv;
// 至少需要 CONTROL 和 ACTION 两个参数
if (count($argv) < 3) {
throw new InvalidArgumentException("缺少 CONTROL 和 ACTION 参数");
}
// 获取 CONTROL 和 ACTION
$this->controllerName = strtolower(formSubArray($argv[1]));
$this->actionName = strtolower(formSubArray($argv[2]));
// 解析其他参数
for ($i = 3; $i < count($argv); $i++) {
$arg = $argv[$i];
if (str_starts_with($arg, '--')) {
$param = substr($arg, 2);
if (str_contains($param, '=')) {
list($key, $value) = explode('=', $param, 2);
//参数安全处理
$this->params[$key] = formSubArray($value);
} else {
// 无值参数视为布尔值 true
$this->params[$param] = true;
}
} else {
// 位置参数
$this->params[] = $arg;
}
}
}
protected function getControllerName($controllerName=null)
{
$controllerClass = $this->controllerMap[$controllerName?:$this->controllerName];
$controllerClassFile = __DIR__ . "/{$controllerClass}.php";
$controllerClassFile = str_replace(['\\','/'], DIRECTORY_SEPARATOR, $controllerClassFile);
// 检查控制器类是否存在
if (!class_exists($controllerClass)) {
if (file_exists($controllerClassFile)) require_once $controllerClassFile;
else {
throw new InvalidArgumentException("控制器文件不存在: {$controllerClassFile}");
}
if (!class_exists($controllerClass)) {
throw new InvalidArgumentException("控制器类不存在: {$controllerClass}");
}
}
return $controllerClass;
}
protected function getActionName()
{
return 'action' . ucfirst($this->actionName);
}
/**
* 验证命令是否有效
*/
protected function validateCommand()
{
// 检查控制器是否存在
if (!isset($this->controllerMap[$this->controllerName])) {
throw new InvalidArgumentException("未知的控制器: {$this->controllerName}");
}
$controllerName = $this->getControllerName();
// 检查动作方法是否存在
$controller = new $controllerName();
$actionMethod = $this->getActionName();
if (!method_exists($controller, $actionMethod)) {
throw new InvalidArgumentException("控制器 {$this->controllerName} 中不存在动作: {$this->actionName}");
}
// 检查方法是否为 public
$reflection = new ReflectionMethod($controllerName, $actionMethod);
if (!$reflection->isPublic()) {
throw new InvalidArgumentException("动作方法 {$actionMethod} 不是 public 的");
}
}
/**
* 执行控制器动作
*/
protected function executeAction()
{
$controllerClass = $this->getControllerName();
$controller = new $controllerClass();
$actionMethod = $this->getActionName();
// 获取方法参数信息
$reflection = new ReflectionMethod($controllerClass, $actionMethod);
$methodParams = $reflection->getParameters();
// 准备传递给方法的参数
$callParams = [];
foreach ($methodParams as $param) {
$paramName = $param->getName();
// 检查参数是否在命令行参数中提供
if (array_key_exists($paramName, $this->params)) {
$callParams[] = $this->params[$paramName];
} // 检查是否有默认值
elseif ($param->isOptional()) {
$callParams[] = $param->getDefaultValue();
} // 必需参数缺失
else {
throw new InvalidArgumentException("缺少必需参数: {$paramName}");
}
}
// 执行动作方法
$result = $reflection->invokeArgs($controller, $callParams);
// 输出结果(如果有)
if ($result !== null) {
$this->output($result);
}
}
/**
* 输出信息
* @param string|array $message 要输出的信息
* @param string $color 颜色名称
*/
protected function output($message, $color = 'reset')
{
if (is_array($message)) {
$message = json_encode($message, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
}
echo $this->colors[$color] . $message . $this->colors['reset'] . PHP_EOL;
}
/**
* 输出错误信息
* @param string $message 错误信息
*/
protected function error($message)
{
$this->output("错误: {$message}", 'red');
}
/**
* 显示帮助信息
*/
public function showHelp()
{
$this->output("命令行工具使用帮助", 'blue');
$this->output("用法: php cmd.php CONTROL ACTION [--参数1=值1 --参数2=值2 ...]", 'blue');
$this->output("", 'reset');
$this->output("可用控制器:", 'blue');
foreach ($this->controllerMap as $name => $class) {
$this->output(" {$name} ({$class})", 'yellow');
$this->showControllerActions($name, $class);
}
}
/**
* 显示控制器的可用动作
* @param string $controllerName 控制器名称
* @param string $controllerClass 控制器类名
*/
protected function showControllerActions($controllerName, $controllerClass)
{
$controllerClass = $this->getControllerName($controllerName);
$reflection = new ReflectionClass($controllerClass);
$methods = $reflection->getMethods(ReflectionMethod::IS_PUBLIC);
$actions = [];
foreach ($methods as $method) {
if (str_starts_with($method->getName(), 'action') && $method->getName() !== 'action') {
$actionName = substr($method->getName(), 6);
$params = [];
foreach ($method->getParameters() as $param) {
$paramInfo = '$' . $param->getName();
if ($param->isOptional()) {
$default = $param->getDefaultValue();
$defaultStr = is_string($default) ? "'{$default}'" : $default;
$paramInfo .= " = {$defaultStr}";
}
$params[] = $paramInfo;
}
$actions[] = " {$actionName}(" . implode(', ', $params) . ")";
}
}
if (!empty($actions)) {
$this->output(" 可用动作:", 'cyan');
foreach ($actions as $action) {
$this->output($action, 'purple');
}
$this->output("", 'reset');
}
}
}
// 创建并运行命令行工具
$command = new CommandRunner();
// 检查是否有 --help 或 -h 参数
if (in_array('--help', $argv) || in_array('-h', $argv)) {
$command->showHelp();
} else {
$command->run();
}
shell/baseCommand.php
<?php
namespace shell;
require_once 'control/interface.php';
use control\interfaces;
class baseCommand
{
use interfaces;
/** @var array 颜色定义 */
protected $colors = [
'reset' => "\033[0m",
'red' => "\033[31m",
'green' => "\033[32m",
'yellow' => "\033[33m",
'blue' => "\033[34m",
'purple' => "\033[35m",
'cyan' => "\033[36m",
];
public string $date;
public string $time;
public function __construct()
{
$this->date = date('Y-m-d');
$this->time = date('Y-m-d H:i:s');
$this->init();
}
/**
* 运行例子测试
* php .\cmd.php base test --name=zhangsan [--age=32]
* @param $name
* @return void
*/
public function actionTest($name, $age=23)
{
$this->output("hello world: {$name}, age: {$age}", 'green');
}
/**
* 输出信息
* @param string|array $message 要输出的信息
* @param string $color 颜色名称
*/
protected function output($message, $color = 'reset')
{
if (is_array($message)) {
$message = json_encode($message, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
}
echo $this->colors[$color] . $message . $this->colors['reset'] . PHP_EOL;
}
/**
* 输出错误信息
* @param string $message 错误信息
*/
protected function error($message)
{
$this->output("错误: {$message}", 'red');
}
/**
* 显示帮助信息
*/
public function showHelp()
{
$this->output("帮助信息", 'blue');
}
// 自动加载类
public function loadClass($className)
{
$className = str_replace('shell\\', '', $className);
if (stristr(PHP_OS, 'LINUX')) {
$className = str_replace('\\', '/', $className);
}
$paths = [
"{$className}.php",
"control/{$className}.php",
"shell/{$className}.php",
];
foreach ($paths as $fileName) {
$path = serverRoot . $fileName;
if (file_exists($path)) {
require_once $fileName;
return;
}
}
}
} mysql在使用group by子句的时候,在什么情况下 不使用 any_value()函数会报错?
在 MySQL 中,当使用 GROUP BY 子句时,如果查询的列 既不在 GROUP BY 子句中,也没有使用聚合函数(如 SUM()、COUNT()、AVG() 等),此时如果不使用 ANY_VALUE() 函数,可能会出现报错。
这一行为与 MySQL 的 ONLY_FULL_GROUP_BY 模式密切相关:
当启用 ONLY_FULL_GROUP_BY 模式(MySQL 5.7+ 版本默认启用)时:
GROUP BY 子句中GROUP BY 中,且未使用 ANY_VALUE(),则会直接报错示例(会报错):
SELECT name, age, COUNT(*)
FROM users
GROUP BY name; -- age列既不在GROUP BY中,也未使用聚合函数当禁用 ONLY_FULL_GROUP_BY 模式时:
ANY_VALUE() 函数的作用是告诉 MySQL:在 ONLY_FULL_GROUP_BY 模式下,允许从分组中随机选择一个值返回,而无需该列出现在 GROUP BY 子句中,从而避免报错。
修正后的示例:
SELECT name, ANY_VALUE(age), COUNT(*)
FROM users
GROUP BY name; -- 使用ANY_VALUE()处理非GROUP BY列当禁用 ONLY_FULL_GROUP_BY 模式时:MySQL 会从分组中随机选择一个值返回,不会报错,这不就是相当于使用 ANY_VALUE()一个效果吗,多止一举的配置项
虽然禁用 ONLY_FULL_GROUP_BY 后 MySQL 的行为看起来和使用 ANY_VALUE() 类似(都是从分组中取一个“任意”值),但两者存在本质区别,这个配置项并非多余:
ONLY_FULL_GROUP_BY 是 SQL 标准的要求,它强制查询逻辑符合“分组后只能获取分组键或聚合结果”的规范,避免因隐含的“随机取值”导致不可预期的结果。 ANY_VALUE() 能明确告知开发者:这里的取值是不确定的,是经过人工确认的合理逻辑。ONLY_FULL_GROUP_BY 会让代码隐藏潜在的逻辑风险——其他开发者可能误以为未分组的列是“确定值”,而 ANY_VALUE() 是显式的语法标记,清晰表明这是有意为之的“任意取值”,减少理解成本。ONLY_FULL_GROUP_BY 可能导致代码在升级后出现兼容性问题。而使用 ANY_VALUE() 是符合标准的写法,更利于长期维护。简单说:ONLY_FULL_GROUP_BY 是“防错机制”,ANY_VALUE() 是“明确允许不规范操作”的语法工具。前者确保代码严谨性,后者在特殊场景下提供灵活性,二者并非重复功能。
查询语句中很多列需要返回,不管是不是字段中值都相同都要使用 ANY_VALUE() ,好繁琐
在实际开发中,如果需要返回很多列且不想逐个添加 ANY_VALUE(),可以通过以下几种方式简化操作,同时兼顾代码规范和效率:
ONLY_FULL_GROUP_BY(不推荐,但应急可用)如果只是临时调试或处理历史遗留代码,可以临时关闭该模式:
-- 临时关闭(当前会话生效)
SET sql_mode = (SELECT REPLACE(@@sql_mode, 'ONLY_FULL_GROUP_BY', ''));
-- 执行查询(无需 ANY_VALUE())
SELECT col1, col2, col3, COUNT(*)
FROM table
GROUP BY group_col;
-- 用完后建议恢复(避免影响其他查询)
SET sql_mode = (SELECT CONCAT(@@sql_mode, ',ONLY_FULL_GROUP_BY'));缺点:不符合 SQL 标准,可能导致结果不可预期,长期使用有维护风险。
ANY_VALUE())如果某些列在分组内的值实际是相同的(如“用户所在城市”按“用户ID”分组时,城市值肯定相同),可以用 MAX() 或 MIN() 替代 ANY_VALUE(),效果相同且更简洁:
SELECT
group_col,
MAX(col1), -- 替代 ANY_VALUE(col1),若分组内值相同则结果一致
MAX(col2),
MAX(col3),
COUNT(*)
FROM table
GROUP BY group_col;原理:当分组内列值唯一时,MAX()/MIN() 会返回该唯一值,与 ANY_VALUE() 效果相同,但写法更简洁(少写 8 个字符)。
如果需要返回的列较多,且确实需要“分组统计 + 原始列信息”,可以拆分查询逻辑:
-- 先分组统计,再关联原始表获取其他列
SELECT t.*, stats.cnt
FROM table t
JOIN (
SELECT group_col, COUNT(*) AS cnt
FROM table
GROUP BY group_col
) stats ON t.group_col = stats.group_col;适用场景:需要展示“分组统计结果”和“该分组下的某条原始数据”(如“每个分类的第一条数据 + 该分类的总数”)。
GROUP BY 的核心是“按分组键聚合”,若需要返回大量非分组列,可能意味着:
GROUP BY 中(实际业务需要按多列分组);GROUP BY(实际需要的是 DISTINCT 去重,而非聚合)。例如,若业务需要“按用户分组,同时返回用户的所有信息 + 订单数”,更合理的写法是:
-- 按用户ID分组(用户信息天然与ID一一对应,无需 ANY_VALUE())
SELECT u.id, u.name, u.age, COUNT(o.id) AS order_cnt
FROM users u
LEFT JOIN orders o ON u.id = o.user_id
GROUP BY u.id; -- 若name、age与id一一对应,MySQL会自动识别(无需显式加入GROUP BY)注:MySQL 5.7+ 支持“功能依赖”识别,若 name、age 依赖于 id(如 id 是主键),则无需将其加入 GROUP BY 或用 ANY_VALUE()。
ANY_VALUE() 的繁琐本质是提醒开发者:分组后非聚合列的取值是不确定的。如果频繁遇到这种情况,更应该从业务逻辑或表设计层面优化(如确保分组键能唯一决定其他列),而非单纯追求语法简化。临时场景下,用 MAX() 替代或拆分查询是更规范的折中方案。
在 MySQL 数据库操作中,GROUP_CONCAT 函数是常用的字符串处理工具,它能将分组查询结果中的多个字符串拼接成一个字符串,极大简化了数据汇总场景。但在实际使用中,不少开发者会遇到拼接结果被意外截断的问题,这背后的核心原因便是 GROUP_CONCAT 存在默认长度限制。本文将系统梳理 GROUP_CONCAT 长度配置的关键知识点,帮助大家精准控制拼接结果,避免数据丢失。
GROUP_CONCAT 函数的拼接长度并非无限制,其默认最大长度由 MySQL 系统变量 group_concat_max_len 控制,默认值通常为 1024 字节。这意味着,当拼接后的字符串总长度超过 1024 字节时,超出部分会被自动截断,最终返回的结果仅包含前 1024 字节的内容。
例如,若在用户订单表中,用 GROUP_CONCAT(order_id) 拼接某用户的所有订单号,当订单号数量较多、总长度超过 1024 字节时,最终得到的订单号字符串会不完整,进而影响业务逻辑判断。
要解决 GROUP_CONCAT 长度不足的问题,需先明确当前配置,再根据需求选择合适的修改方式。
group_concat_max_len 配置通过简单的 SQL 语句,即可查询当前系统中 group_concat_max_len 的值,了解当前的拼接长度限制:
SELECT @@group_concat_max_len;执行该语句后,返回的数值即为当前 GROUP_CONCAT 允许的最大拼接长度(默认通常为 1024)。
根据业务场景的不同,group_concat_max_len 有三种修改维度,分别对应不同的生效范围和持久度:
若仅需在单次查询或当前数据库连接中调整拼接长度,可使用“会话级修改”,修改后仅对当前连接生效,关闭连接后配置自动恢复默认值。语法如下:
-- 示例:将最大长度设置为 1000000 字节(约 1MB)
SET session group_concat_max_len = 1000000;这种方式适合临时处理特殊需求(如一次性导出大量拼接数据),无需影响其他数据库连接。
若希望所有新建立的数据库连接都使用新的拼接长度,可进行“全局级修改”,但需注意:修改后需重新断开并连接数据库,配置才会对新连接生效;已存在的连接仍使用旧配置。语法如下:
-- 需拥有 SUPER 权限,示例:全局设置最大长度为 1000000 字节
SET global group_concat_max_len = 1000000;该方式适合团队内统一拼接长度标准,但需避免影响无需超长拼接的业务。
若需让配置长期生效(如服务器重启后仍保持新长度),需修改 MySQL 的核心配置文件(Linux 系统通常为 my.cnf,Windows 系统通常为 my.ini):
找到配置文件并打开,在 [mysqld] 模块下添加或修改如下内容:
[mysqld]
group_concat_max_len = 1000000 # 按需设置具体数值systemctl restart mysqld),配置即可永久生效。这种方式适合业务长期需要超长拼接的场景,但需谨慎操作,避免因配置错误导致 MySQL 启动失败。
部分开发者可能会直接将 group_concat_max_len 设置为最大值(4294967295 字节,约 4GB),但盲目增大长度会带来一系列风险,需提前规避:
GROUP_CONCAT 的拼接结果会暂存于 MySQL 内存中,若长度设置过大,当处理高并发查询或大表分组时,大量内存会被拼接结果占用,严重时可能导致 MySQL 进程内存溢出(OOM),直接影响服务稳定性。
超长字符串的拼接需要更多 CPU 计算资源和内存操作,会显著延长单个查询的执行时间;若分组查询涉及大表,还可能触发临时表或文件排序,进一步加剧性能损耗,导致整体数据库响应变慢。
拼接后的超长结果返回给客户端(如应用程序、BI 工具)时,会占用更多网络带宽,尤其在远程连接场景下,可能出现传输延迟;同时,客户端接收和解析超长字符串也需要更多内存,若客户端存在字段长度限制(如 ORM 框架的字段定义),还可能导致数据解析失败。
若将 GROUP_CONCAT 的结果存储到数据表中(如存入 VARCHAR 类型字段),若拼接长度超出字段定义的最大长度(如 VARCHAR(2000)),会导致数据插入失败或被截断,引发数据一致性问题。
合理配置 group_concat_max_len 的核心原则是“按需设置、最小够用”,具体可遵循以下建议:
SET session 进行局部修改,避免全局配置影响其他无关查询。GROUP_CONCAT 函数的长度配置是 MySQL 开发中的基础知识点,既需掌握“查-改-验”的操作流程,也需理解大值配置的潜在风险。通过按需设置合理的长度,既能满足业务对完整拼接结果的需求,又能保障数据库的性能与稳定性,避免因配置不当引发线上问题。