技术学习从来都是枯燥的，有时候还会翻车，做一点自己感兴趣的事。跟着各路大佬，日日积累，提升技能。破而后立，晓瑜新生
MySQL 版本演进概览MySQL 的主要版本发展可以概括为：5.1 -> 5.5 -> 5.6 -> 5.7 -> 8.0。目前，MySQL 5.7 已于 2023年10月结束官方支持，MySQL 8.0 是绝对的主流和未来。新项目都应选择 8.0 或更高版本。各主要版本核心区别与特性下面我们按版本从新到旧进行梳理，重点对比 5.7 和 8.0 这两个最关键版本。MySQL 8.0 (2018年发布，当前主流)8.0 是一个里程碑式的大版本，引入了大量革命性的新特性。类别核心特性与改进说明性能与存储事务性数据字典将系统表（如 user, db）从 MyISAM 引擎迁移到 InnoDB 引擎，并存储于数据字典表中，支持原子DDL，使元数据管理更安全、可靠。 直方图为优化器提供表中数据分布的统计信息，帮助优化器在非索引字段上生成更优的执行计划。 不可见索引可以将索引设置为“不可见”，优化器会忽略它，用于测试删除某个索引是否会对性能产生负面影响，而无需真正删除。 降序索引支持按降序存储索引，对于 ORDER BY column DESC 的查询性能大幅提升。SQL功能通用表表达式支持 WITH 语句，可以创建命名的临时结果集，支持递归查询，处理树状或层次结构数据非常方便。 窗口函数支持 ROW_NUMBER(), RANK(), LAG() 等强大的分析函数，无需分组即可对行数据进行复杂计算。 资源组可以将线程绑定到特定的CPU核心上，实现更精细化的资源管理。 JSON增强引入了 JSON_TABLE(), JSON_OBJECTAGG(), JSON_ARRAYAGG() 等更多JSON函数，功能更强大。安全与管理角色引入了数据库角色，可以批量管理用户权限，极大简化了权限管理。 默认字符集默认字符集从 latin1 改为 utf8mb4，更好地支持全球化和emoji表情。 默认认证插件默认的身份认证插件改为 caching_sha2_password，比之前的 mysql_native_password 更安全，但可能导致一些旧的客户端/驱动需要升级。 持久化系统变量使用 SET PERSIST 命令设置的变量在服务器重启后依然有效。MySQL 5.7 (2015年发布，已停止支持)5.7 是一个极其成功和稳定的版本，在其生命周期内是生产环境的绝对主力。类别核心特性与改进说明性能与复制InnoDB 增强在线缓冲池调整、在线修改 VARCHAR 长度、多线程页面清理等，提升了并发处理能力和在线操作能力。 组提交实现了多线程组提交，极大改善了在高并发写场景下的性能。 多源复制一个从库可以同时从多个主库复制数据，用于数据汇总。 增强的半同步复制引入了无损复制，保证主从数据强一致性。SQL功能原生JSON支持引入了 JSON 数据类型，并提供 JSON_EXTRACT, JSON_OBJECT 等函数，但功能不如 8.0 强大。 生成列可以创建由其他列计算得出的列，分为 VIRTUAL（不存储）和 STORED（存储）两种。 Sys Schema引入了一个名为 sys 的系统库，提供了一系列视图、函数和存储过程，让性能诊断和问题排查变得非常简单。 空间数据支持增强了对 GIS 数据的支持。安全密码验证策略引入了密码过期策略和密码强度验证插件。MySQL 5.6 (2013年发布，已停止支持)5.6 版本是 InnoDB 引擎和优化器能力大幅提升的版本。核心特性与改进说明InnoDB 性能提升成为绝对的默认存储引擎，提供了全文索引、独立的 undo tablespace。优化器改进引入了索引条件下推，可以在存储引擎层利用索引过滤数据，减少回表次数。复制增强支持基于库的并行复制，提升了从库的复制性能。支持 GTID，使主从切换和故障恢复更方便。在线DDL支持了更多种类的在线 DDL 操作，减少了对业务的影响。MySQL 5.5 (2010年发布，已停止支持)核心特性与改进说明InnoDB 成为默认引擎取代 MyISAM，标志着 MySQL 向事务安全型数据库的彻底转变。半同步复制提供了比异步复制更强的数据一致性保证。性能提升引入了 InnoDB 的缓冲池预加载等特性。总结与版本选择建议版本状态建议MySQL 8.0积极维护，长期支持强烈推荐。所有新项目都应选择此版本。它提供了最好的性能、最丰富的功能和最新的安全更新。MySQL 5.7已停止官方支持不推荐。仅适用于尚未完成升级的遗留系统，应尽快制定迁移计划至 8.0。MySQL 5.6 及更早已停止官方支持绝对禁止。存在已知安全漏洞且无修复，必须立即升级。升级路径通常的升级路径是：5.5/5.6 -> 5.7 -> 8.0。从 5.7 升级到 8.0 前，务必仔细阅读官方文档，进行充分的兼容性测试，因为两者在默认配置（如认证插件、字符集）上有较大变化。希望这份整理能帮助您清晰地了解 MySQL 的版本演进和区别！
Linux 开发与运维命令终极手册封面与使用说明适用人群：PHP开发者、后端工程师、运维人员 核心价值：从日常开发到应急调试的全场景命令参考，兼顾效率与安全 使用方法：按「场景速查」快速定位，按「功能分类」系统学习，注意「安全警告」规避风险一、场景速查索引（紧急情况优先看）问题场景核心命令链功能分类网站突然卡顿/502错误top（看CPU/内存）→ ss -s（连接数）→ systemctl status php-fpm（服务状态）五、一.........二、系统基础与环境监控系统信息uname：查看系统信息uname -a # 完整信息 uname -r # 内核版本（确认Docker/Kernel兼容性）lsb_release：发行版信息lsb_release -a # 所有信息 lsb_release -c # 版本代号（适配apt/yum源）磁盘与存储df：磁盘空间df -h # 人类可读格式 df -i # inode使用情况（紧急清理时，满需删除大量小文件）du：目录大小分析du -sh * # 当前目录各项目大小 du -h --max-depth=1 /var # 一级目录分析 du -sh * \| sort -hr # 从大到小排列内存与负载free：内存监控free -h # 易读格式 free -m # MB单位 watch -n 5 "free -h" # 每5秒刷新（内存泄漏排查）uptime：系统负载uptime # 显示1/5/15分钟平均负载（负载>CPU核心数时需排查进程）高级监控iostat：磁盘IO监控iostat -x 5 # 每5秒显示IO详情（%util接近100%表示IO饱和）which/whereis：命令路径查找which php # 检查php路径 whereis mysql # 查找mysql（区分系统自带与手动安装的程序）三、文件与目录操作基本操作ls：列出内容ls -lh # 详细+大小 ls -lt # 按修改时间排序 # 现代替代：exa -l --git（带Git状态和颜色）cd：切换目录cd ~ # 家目录 cd - # 返回上一目录 # 技巧：cd .. 可简写为 ..（需配置bash）mkdir：创建目录mkdir -p app/{ctrl,model,view} # 递归创建多级目录（一次性创建项目结构）文件操作rm：删除文件 ⚠️极度危险rm file.txt # 删除文件 rm -rf dir/ # 递归强制删除目录 # ⚠️ 安全准则： # 1. 禁止执行 rm -rf / 或 rm -rf /* # 2. 删前先用 ls 确认路径：ls dir/ # 3. 替代方案：trash-put file（移到回收站，需安装trash-cli）cp：复制文件cp -a src/ dest/ # 保留权限复制 cp config.php{,.bak} # 备份（生成config.php.bak） cp *.conf{,.bak} # 批量备份（给所有conf文件加备份后缀）高级文件操作find：查找文件find ./ -name "*.php" -mtime -1 # 1天内修改的PHP文件 find ./ -name "*.log" -mtime +30 -print0 | xargs -0 rm -f # 安全批量处理（处理含空格的文件名）ln：创建链接ln -s /usr/local/php/bin/php /usr/bin/php # 创建软链接（解决命令路径问题）rsync：增量同步rsync -avz --delete ./local/ user@server:/remote/ # 镜像同步（部署代码比scp快）四、文件内容处理查看内容cat：查看文件内容cat config.php # 查看文件 # 现代替代：bat config.php（代码高亮+行号）tail：查看尾部tail -f error.log # 实时监控 tail -n 50 access.log # 最后50行 tail -F error.log # 日志轮转必备（文件重建后仍跟踪）文本处理grep：文本搜索grep -rni "数据库" ./app # 递归+行号+忽略大小写搜索 find ./ -name "*.php" | xargs grep "function getUser" # 结合find搜索sed：文本替换sed -i 's/old/new/g' file.php # 批量替换 sed -i '/^#/d' nginx.conf # 删除注释行 sed -i 's/debug=true/debug=false/g' *.php # 批量修改配置awk：按列处理awk '$9==500 {print $0}' access.log # 筛选500错误 # 统计分析黄金链： awk '{print $1}' access.log | sort | uniq -c | sort -nr | head -10 # Top 10访问IPwc：统计行数wc -l *.php # 统计PHP文件行数 grep "error" log.txt | wc -l # 错误数统计 find ./ -name "*.php" | xargs wc -l # 快速估算代码量五、进程与服务管理进程监控ps：查看进程ps aux --sort=-%cpu # 按CPU排序 ps aux | grep -v grep | grep php-fpm # 筛选特定进程top/htop：实时监控top # 基础监控 htop # 交互式增强版（支持鼠标操作） # 技巧：按P（CPU）/M（内存）排序，k键终止进程kill：终止进程kill 1234 # 正常终止 kill -9 1234 # 强制终止 pkill -f "php server.php" # 按命令名批量终止服务管理systemctl：服务管理systemctl restart nginx # 重启服务 systemctl enable mysql # 开机自启 systemctl status php-fpm -l # 查看状态及完整日志（启动失败排查）网络与调试netstat/ss：网络连接ss -lntup # 监听的端口及进程 ss -ant | grep :80 | wc -l # 统计80端口连接数lsof：文件占用lsof -i :80 # 查看80端口占用 lsof -p 1234 # 查看进程打开的文件 lsof +D /var/www # 查看目录相关进程（解决"文件被占用无法删除"）strace：系统调用跟踪strace -p 1234 # 调试卡死进程（定位程序卡住原因）六、网络操作网络诊断ping：连通性测试ping -c 4 google.com # 发送4个包 # 现代替代：mtr google.com（持续跟踪丢包和延迟）curl：HTTP请求curl -X POST -d "name=test" http://api.com # POST请求 curl -I https://example.com # 仅获取头部 curl -v https://example.com # 详细输出（显示HTTPS握手过程）文件传输wget：文件下载wget -c https://file.com/big.tar.gz # 断点续传 wget -i url_list.txt # 批量下载（从文件读URL）ssh：远程登录ssh -p 2222 user@server.com # 指定端口登录 ssh user@server "command" # 远程执行命令 ssh-copy-id user@server # 免密登录（上传公钥）scp：远程传输scp -r dir/ user@server:/path/ # 递归上传目录 # 大文件传输技巧：pv file.tar.gz | ssh user@server "cat > file.tar.gz"（显示进度）nc：网络调试nc -zv 192.168.1.1 3306 # 测试MySQL端口连通性 # 临时文件传输： # 接收端：nc -l 8080 > recv.txt # 发送端：nc server 8080 < send.txt七、MySQL数据库基本操作mysql：登录数据库mysql -u root -p # 本地登录 mysql -h 10.0.0.5 -P 3307 -u user -p dbname # 远程连接 # 安全提示：生产环境禁止用root直接登录，创建专用用户mysqldump：备份数据库mysqldump -u root -p --single-transaction blog > blog.sql # 热备份（避免锁表）管理操作mysqladmin：数据库管理mysqladmin -u root -p processlist # 查看连接进程 mysqladmin -u root -p kill 123 # 杀死进程ID 123（终止慢查询）数据导入：恢复备份mysql -u root -p blog < blog.sql # ⚠️ 警告：导入前确认目标库正确，避免覆盖生产数据日志查看：调试问题tail -f /var/log/mysql/error.log # 实时错误日志 # 技巧：开启慢查询日志（临时生效）： set global slow_query_log=1八、Docker容器管理镜像管理镜像操作：docker pull php:7.4-fpm # 拉取镜像 docker build -t myphp:v1 . # 构建镜像 docker image prune -a # 清理无用镜像容器管理容器操作：docker run -d -p 80:80 --name mynginx nginx # 后台运行容器 docker ps -a # 查看所有容器 docker exec -it myphp bash # 进入容器交互终端数据与网络数据卷：持久化存储docker volume create phpdata # 创建数据卷 docker run -v phpdata:/var/www/html php:7.4-fpm # 使用数据卷 # 备注：避免用 -v /host/path:/container/path（权限问题多），优先用数据卷网络管理：容器通信docker network create mynet # 创建网络 docker run --network mynet --name db mysql # 容器加入网络 # 技巧：自定义网络避免端口冲突，容器间用名称通信（如mysql://db:3306）九、Swoole扩展安装与验证安装Swoole：pecl install swoole # 安装最新版 pecl install swoole-4.8.12 # 安装指定版本（稳定版） echo "extension=swoole.so" >> php.ini # 启用扩展验证安装：php -m | grep swoole # 检查是否加载 php --ri swoole # 查看扩展信息（确认enable_coroutine等关键特性）服务管理服务管理：php server.php start # 启动服务 php server.php reload # 平滑重启（不中断连接，适合生产环境）进程监控：ps aux | grep server.php # 查看进程 netstat -tulpn | grep 9501 # 查看端口监听 # 备注：配置daemonize=1后台运行，通过ps确认进程数是否符合预期十、编程工具与环境开发工具php：PHP解释器php -l file.php # 语法检查 php -S 0.0.0.0:8000 -t public # 启动内置服务器 php -d display_errors=On index.php # 临时调整配置（显示错误）composer：依赖管理composer install --no-dev # 生产环境安装 composer dump-autoload -o # 优化自动加载 # 加速下载：切换国内镜像 composer config -g repo.packagist composer https://mirrors.aliyun.com/composer/git：版本控制git stash # 暂存修改 git bisect start # 二分查找bug git log -S "function_name" # 搜索历史提交（查找引入函数的提交）现代工具jq：JSON处理curl api.com | jq '.data[].name' # 提取数据 jq '.users[] | select(.age>18)' data.json # 条件筛选 cat messy.json | jq . # 格式化JSONag：代码搜索ag "function_name" app/ # 快速搜索 ag -G "*.php" "database" # 指定文件类型搜索 # 备注：比grep -r快10倍，自动忽略.git和vendor目录十一、压缩与归档压缩解压tar：打包/压缩tar -zcvf backup.tar.gz --exclude=vendor ./project # 打包并排除目录 tar -tf backup.tar.gz # 查看内容不解压zip/unzip：ZIP处理zip -r docs.zip ./docs -x "*.git*" # 压缩并排除Git文件 unzip -O GBK chinese.zip # 解压中文文件（避免乱码）gzip：GZIP压缩gzip large.log # 压缩文件（替换原文件） gzip -c file > file.gz # 压缩但保留原文件 gunzip file.gz # 解压十二、包管理与软件安装包管理命令系统更新软件源安装软件搜索软件实用技巧Debian/Ubuntuapt updateapt install <pkg>apt search <pkg>加速：替换国内镜像源CentOS/RHELyum updateyum install <pkg>yum search <pkg>批量安装：yum install php-fpm nginx mysql-server十三、安全警告与最佳实践操作安全rm -rf 安全准则永远不要在根目录/或系统目录执行递归删除删前用ls确认路径：ls /path/to/delete/*推荐用trash-cli（trash-put/trash-empty）替代rm，支持回收站恢复权限操作禁忌⚠️ 禁止执行chmod -R 777 /或chown -R root:root /home/，会导致系统崩溃正确权限：PHP文件644，目录755，Web可写目录属主设为www-data数据库操作安全生产环境执行DROP/TRUNCATE前必须备份禁止直接在生产库执行UPDATE/DELETE不带WHERE的SQL命令执行确认复杂命令先用echo测试：echo rm -rf /path/to/dir/*批量操作前先处理1个样本验证：find ./ -name "*.log" \| head -1 \| xargs rm -f附录：现代工具安装指南工具功能Ubuntu安装命令使用示例bat增强版catsudo apt install batbat config.php（代码高亮）exa增强版lssudo apt install exaexa -l --git（带Git状态）htop增强版topsudo apt install htophtop（交互式监控）mtr网络诊断sudo apt install mtrmtr google.com（路由追踪）jqJSON处理sudo apt install jqcurl api.com \ jq '.data'ag代码搜索sudo apt install silversearcher-agag "function" app/fzf模糊查找sudo apt install fzfCtrl+R搜索历史命令trash-cli回收站工具sudo apt install trash-clitrash-put file（安全删除）使用提示：遇到任何命令不清楚时，使用man <命令>查看详细手册，如man grep。
使用方法 $wechatPayPlatformCertInstance = new \addons\unidrink\library\pay\overseas\wechat\Certificate(); try { $platformCerts = $wechatPayPlatformCertInstance->getPlatformCerts(); if (!empty($platformCerts)) { // 获取最新的证书 $latestCert = $platformCerts[0]; // 返回成功信息 return json([ 'code' => 1, 'msg' => '平台证书生成成功', 'data' => [ 'serial_no' => $latestCert['serial_no'], 'effective_time' => $latestCert['effective_time'], 'expire_time' => $latestCert['expire_time'], 'cert_saved_path' => $wechatPayPlatformCertInstance->getCertPath() . $latestCert['serial_no'] . '.pem', ] ]); } else { throw new Exception('未获取到平台证书'); } } catch (Exception $e) { // 错误处理 return json([ 'code' => 0, 'msg' => '平台证书生成失败: ' . $e->getMessage(), 'data' => [] ]); } 下面是类 <?php namespace addons\unidrink\library\pay\overseas\wechat; use think\Exception; use think\Log; use WeChatPay\Crypto\Rsa; use WeChatPay\Formatter; use WeChatPay\Util\PemUtil; class Certificate { // 配置参数 private $config; // 证书保存路径 private $certPath; // 平台证书列表 private $platformCerts = []; /** * 构造函数 * @param array $config 微信支付配置 */ public function __construct($config = []) { // 默认配置 $defaultConfig = [ 'mch_id' => '', // 商户号 'serial_no' => '', // 商户API证书序列号 'private_key' => ROOT_PATH . 'public/cert/apiclient_key.pem', // 商户私钥内容或路径 'api_v3_key' => '', // APIv3密钥 'cert_path' => ROOT_PATH . 'public/cert/wechatpay/', // 证书保存目录 'ca_cert_path' => ROOT_PATH . 'public/cert/cacert.pem' // CA证书路径 ]; $this->config = array_merge($defaultConfig, $config); $this->certPath = $this->config['cert_path']; // 验证APIv3密钥长度 $this->validateApiV3Key(); // 创建证书目录并确保权限 $this->prepareDirectories(); } /** * 准备目录并确保权限 */ private function prepareDirectories() { // 创建证书目录 if (!is_dir($this->certPath)) { if (!mkdir($this->certPath, 0755, true)) { throw new Exception('无法创建证书目录: ' . $this->certPath); } } // 检查证书目录权限 if (!is_writable($this->certPath)) { throw new Exception('证书目录不可写: ' . $this->certPath); } // 确保CA证书目录可写 $caCertDir = dirname($this->config['ca_cert_path']); if (!is_dir($caCertDir)) { if (!mkdir($caCertDir, 0755, true)) { throw new Exception('无法创建CA证书目录: ' . $caCertDir); } } if (!is_writable($caCertDir)) { throw new Exception('CA证书目录不可写: ' . $caCertDir); } } /** * 验证APIv3密钥长度是否正确 * APIv3密钥必须是32位字符串 * @throws Exception */ private function validateApiV3Key() { // 移除可能存在的空格 $apiV3Key = trim($this->config['api_v3_key']); // 重新设置清理后的密钥 $this->config['api_v3_key'] = $apiV3Key; // 检查长度 $keyLength = strlen($apiV3Key); if ($keyLength !== 32) { throw new Exception("APIv3密钥长度不正确，必须是32位，当前为{$keyLength}位"); } } /** * 获取平台证书 * @return array 平台证书列表 */ public function getPlatformCerts() { try { // 如果有缓存的证书且未过期，直接返回 $cachedCerts = $this->getCachedCerts(); if (!empty($cachedCerts)) { Log::info('使用缓存的平台证书，共' . count($cachedCerts) . '个'); return $cachedCerts; } Log::info('缓存的平台证书不存在或已过期，将从API获取'); // 从微信支付API获取证书 $this->fetchPlatformCerts(); // 缓存证书 $this->cacheCerts(); return $this->platformCerts; } catch (Exception $e) { Log::error('获取微信支付平台证书失败: ' . $e->getMessage() . '，堆栈信息: ' . $e->getTraceAsString()); throw new Exception('获取平台证书失败: ' . $e->getMessage()); } } /** * 从微信支付API获取平台证书 */ private function fetchPlatformCerts() { // 构建请求参数 $timestamp = time(); $nonce = $this->generateNonce(); $method = 'GET'; // $url = '/v3/certificates'; // 中国大陆境内 $url = '/v3/global/certificates'; // 注意：全球版API路径 $body = ''; Log::info("准备请求微信支付证书API: URL={$url}, 时间戳={$timestamp}, 随机串={$nonce}"); // 生成签名 $signature = $this->generateSignature($method, $url, $timestamp, $nonce, $body); // 构建授权头 $token = sprintf( 'WECHATPAY2-SHA256-RSA2048 mchid="%s",nonce_str="%s",timestamp="%d",serial_no="%s",signature="%s"', $this->config['mch_id'], $nonce, $timestamp, $this->config['serial_no'], $signature ); // 发送请求 $headers = [ 'Accept: application/json', 'User-Agent: FastAdmin/WechatPay', 'Authorization: ' . $token, ]; // 根据商户号判断使用国内还是国际API // $apiDomain = 'https://api.mch.weixin.qq.com'; $apiDomain = 'https://apihk.mch.weixin.qq.com'; $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, $apiDomain . $url); curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true); curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2); curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 10); curl_setopt($ch, CURLOPT_TIMEOUT, 30); // 检查私钥文件是否存在 $privateKeyPath = $this->config['private_key']; if (!file_exists($privateKeyPath)) { throw new Exception('私钥文件不存在: ' . $privateKeyPath); } // 检查私钥文件权限和内容 $this->validatePrivateKey($privateKeyPath); // 设置SSL选项 curl_setopt($ch, CURLOPT_SSLCERTTYPE, 'PEM'); curl_setopt($ch, CURLOPT_SSLKEYTYPE, 'PEM'); curl_setopt($ch, CURLOPT_SSLKEY, $privateKeyPath); // 获取并设置CA证书 $caCertPath = $this->getCACertPath(); if ($caCertPath && file_exists($caCertPath)) { curl_setopt($ch, CURLOPT_CAINFO, $caCertPath); Log::info('使用CA证书: ' . $caCertPath); } else { Log::warning('无法获取CA证书，临时关闭SSL验证'); curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 0); } $response = curl_exec($ch); $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); $errorNumber = curl_errno($ch); $error = curl_error($ch); curl_close($ch); // 记录完整响应，便于调试 Log::info('微信支付平台证书API响应: HTTP状态码=' . $httpCode . ', 错误代码=' . $errorNumber . ', 错误信息=' . $error . ', 响应内容=' . $response); if ($httpCode !== 200) { // 特殊处理常见错误码 $errorMsg = "请求微信支付API失败，HTTP状态码: {$httpCode}"; switch ($httpCode) { case 401: $errorMsg .= "，可能是签名错误或商户信息不正确"; break; case 403: $errorMsg .= "，权限不足，可能是API未开通或IP白名单问题"; break; case 404: $errorMsg .= "，请求路径错误"; break; case 500: $errorMsg .= "，微信支付服务器内部错误"; break; } $errorMsg .= "，响应: {$response}"; throw new Exception($errorMsg); } if (empty($response)) { throw new Exception('微信支付API返回空响应'); } $data = json_decode($response, true); if (json_last_error() !== JSON_ERROR_NONE) { throw new Exception('解析微信支付API响应失败: ' . json_last_error_msg() . '，原始响应: ' . $response); } if (!isset($data['data']) || !is_array($data['data'])) { throw new Exception('微信支付API响应数据异常，未找到证书列表: ' . json_encode($data)); } if (empty($data['data'])) { throw new Exception('微信支付API返回空的证书列表'); } // 处理证书数据前记录原始数据 Log::info('准备处理的证书数据数量: ' . count($data['data']) . '，数据: ' . json_encode($data['data'])); // 处理证书数据 $this->processCertificates($data['data']); } /** * 验证私钥文件 */ private function validatePrivateKey($privateKeyPath) { // 检查文件权限 if (!is_readable($privateKeyPath)) { throw new Exception('私钥文件不可读: ' . $privateKeyPath); } // 检查文件大小 $fileSize = filesize($privateKeyPath); if ($fileSize < 100) { throw new Exception('私钥文件过小，可能不是有效的私钥: ' . $privateKeyPath); } // 检查私钥文件格式 $privateKeyContent = file_get_contents($privateKeyPath); if (strpos($privateKeyContent, '-----BEGIN PRIVATE KEY-----') === false || strpos($privateKeyContent, '-----END PRIVATE KEY-----') === false) { throw new Exception('私钥文件格式不正确，不是有效的PEM格式私钥'); } // 尝试加载私钥验证有效性 $key = openssl_pkey_get_private($privateKeyContent); if (!$key) { throw new Exception('私钥无效，无法加载: ' . openssl_error_string()); } // 检查密钥类型和长度 $keyDetails = openssl_pkey_get_details($key); if ($keyDetails['type'] !== OPENSSL_KEYTYPE_RSA || $keyDetails['bits'] < 2048) { throw new Exception('私钥必须是2048位或以上的RSA密钥'); } Log::info('私钥验证通过'); } /** * 获取CA证书路径 */ private function getCACertPath() { $caCertPath = $this->config['ca_cert_path']; // 如果CA证书不存在，则尝试下载 if (!file_exists($caCertPath)) { try { Log::info('CA证书不存在，尝试下载: ' . $caCertPath); $context = stream_context_create([ 'ssl' => [ 'verify_peer' => false, 'verify_peer_name' => false ], 'http' => [ 'timeout' => 10, ] ]); $cacert = file_get_contents('https://curl.se/ca/cacert.pem', false, $context); if ($cacert && strlen($cacert) > 10000) { // 确保下载到有效内容 file_put_contents($caCertPath, $cacert); Log::info('CA证书下载成功: ' . $caCertPath); return $caCertPath; } else { Log::error('下载的CA证书内容无效'); } } catch (Exception $e) { Log::error('下载CA证书失败: ' . $e->getMessage()); } } else { Log::info('使用已存在的CA证书: ' . $caCertPath); } return $caCertPath; } /** * 处理证书数据 * @param array $certificates 证书列表 */ private function processCertificates($certificates) { $successCount = 0; $errorDetails = []; foreach ($certificates as $index => $certInfo) { try { // 记录当前处理的证书信息 $serialNo = $certInfo['serial_no'] ?? '未知'; Log::info('处理第' . ($index + 1) . '个证书，序列号: ' . $serialNo); // 验证证书信息是否完整 if (!isset($certInfo['encrypt_certificate'])) { throw new Exception('缺少encrypt_certificate字段'); } $encryptCert = $certInfo['encrypt_certificate']; $requiredFields = ['ciphertext', 'nonce', 'associated_data']; foreach ($requiredFields as $field) { if (!isset($encryptCert[$field]) || empty($encryptCert[$field])) { throw new Exception("证书信息缺少{$field}字段"); } } // 解密证书 $cert = $this->decryptCertificate( $encryptCert['ciphertext'], $encryptCert['nonce'], $encryptCert['associated_data'] ); // 解析证书 $parsedCert = openssl_x509_parse($cert); if (!$parsedCert) { $error = openssl_error_string(); throw new Exception('解析证书失败: ' . $error); } // 验证证书有效期 $now = time(); $effectiveTime = strtotime($certInfo['effective_time'] ?? ''); $expireTime = strtotime($certInfo['expire_time'] ?? ''); if ($effectiveTime === false || $expireTime === false) { throw new Exception('证书有效期格式不正确'); } if ($now < $effectiveTime) { throw new Exception('证书尚未生效'); } if ($now > $expireTime) { throw new Exception('证书已过期'); } // 保存证书信息 $this->platformCerts[] = [ 'serial_no' => $serialNo, 'effective_time' => $certInfo['effective_time'], 'expire_time' => $certInfo['expire_time'], 'cert' => $cert, 'parsed_cert' => $parsedCert, ]; $successCount++; Log::info('成功处理证书，序列号: ' . $serialNo); } catch (Exception $e) { $errorMsg = '处理证书失败: ' . $e->getMessage(); $errorDetails[] = $errorMsg . '，证书信息: ' . json_encode($certInfo); Log::error($errorMsg); } } Log::info("证书处理完成，成功: {$successCount}个，失败: " . (count($certificates) - $successCount) . "个"); if (empty($this->platformCerts)) { throw new Exception('所有证书处理失败，没有可用的平台证书。详细错误: ' . implode('; ', $errorDetails)); } // 按过期时间排序，最新的在前面 usort($this->platformCerts, function ($a, $b) { return strtotime($b['expire_time']) - strtotime($a['expire_time']); }); } /** * 解密证书 * @param string $ciphertext 密文 * @param string $nonce 随机串 * @param string $associatedData 附加数据 * @return string 解密后的证书内容 */ private function decryptCertificate($ciphertext, $nonce, $associatedData) { // 记录解密参数，便于调试 Log::info("开始解密，nonce长度: " . strlen($nonce) . ", associatedData长度: " . strlen($associatedData) . ", 密文长度: " . strlen($ciphertext)); // 验证输入参数 if (empty($ciphertext)) { throw new Exception('密文为空'); } if (empty($nonce)) { throw new Exception('随机串为空'); } if (empty($associatedData)) { throw new Exception('附加数据为空'); } // 尝试解码base64 $decodedCiphertext = base64_decode($ciphertext, true); if ($decodedCiphertext === false) { throw new Exception('密文base64解码失败，可能不是有效的base64字符串'); } if (strlen($decodedCiphertext) < 10) { throw new Exception('解码后的密文长度过短，可能是无效数据'); } // 使用正确的AEAD_AES_256_GCM解密方法 // 微信支付使用的是AEAD_AES_256_GCM，需要正确处理认证标签 $ciphertext = base64_decode($ciphertext); $authTag = substr($ciphertext, -16); $ciphertext = substr($ciphertext, 0, -16); // 清除之前的OpenSSL错误 while (openssl_error_string() !== false) { } // 使用AEAD_AES_256_GCM解密 $cert = openssl_decrypt( $ciphertext, 'aes-256-gcm', $this->config['api_v3_key'], OPENSSL_RAW_DATA, $nonce, $authTag, $associatedData ); // 收集所有OpenSSL错误 $errors = []; while ($error = openssl_error_string()) { $errors[] = $error; } if ($cert === false) { throw new Exception('解密平台证书失败: ' . implode('; ', $errors) . '。检查APIv3密钥是否正确，密钥长度是否为32位'); } // 验证解密结果是否为有效的证书 if (strpos($cert, '-----BEGIN CERTIFICATE-----') === false || strpos($cert, '-----END CERTIFICATE-----') === false) { throw new Exception('解密结果不是有效的证书格式，可能是密钥错误'); } Log::info('证书解密成功，解密结果长度: ' . strlen($cert)); return $cert; } /** * 生成签名 * @param string $method 请求方法 * @param string $url 请求URL * @param int $timestamp 时间戳 * @param string $nonce 随机串 * @param string $body 请求体 * @return string 签名 */ private function generateSignature($method, $url, $timestamp, $nonce, $body) { $message = "{$method}\n{$url}\n{$timestamp}\n{$nonce}\n{$body}\n"; Log::info("生成签名的原始消息: " . base64_encode($message)); // 用base64避免特殊字符问题 // 加载私钥 $privateKey = $this->getPrivateKey(); // 生成签名 $signature = ''; $success = openssl_sign($message, $signature, $privateKey, OPENSSL_ALGO_SHA256); if (!$success) { throw new Exception('生成签名失败: ' . openssl_error_string()); } $signatureBase64 = base64_encode($signature); Log::info("生成签名成功: {$signatureBase64}"); return $signatureBase64; } /** * 获取私钥 * @return resource 私钥资源 */ private function getPrivateKey() { $privateKey = $this->config['private_key']; // 如果私钥是文件路径，读取文件内容 if (is_file($privateKey)) { $privateKey = file_get_contents($privateKey); } // 加载私钥 $key = openssl_pkey_get_private($privateKey); if (!$key) { throw new Exception('加载商户私钥失败: ' . openssl_error_string()); } return $key; } /** * 获取缓存的证书 * @return array 证书列表 */ private function getCachedCerts() { $cacheFile = $this->certPath . 'platform_certs.cache'; if (!file_exists($cacheFile)) { Log::info('平台证书缓存文件不存在: ' . $cacheFile); return []; } if (!is_readable($cacheFile)) { Log::warning('平台证书缓存文件不可读: ' . $cacheFile); return []; } $cacheData = json_decode(file_get_contents($cacheFile), true); if (json_last_error() !== JSON_ERROR_NONE) { Log::error('解析平台证书缓存失败: ' . json_last_error_msg()); return []; } if (!isset($cacheData['expire_time']) || !isset($cacheData['certs']) || !is_array($cacheData['certs'])) { Log::error('平台证书缓存格式不正确'); return []; } // 检查缓存是否过期（提前1小时刷新） $expireTime = $cacheData['expire_time']; $now = time(); if ($now >= ($expireTime - 3600)) { Log::info("平台证书缓存已过期或即将过期，当前时间: {$now}，过期时间: {$expireTime}"); return []; } return $cacheData['certs']; } /** * 缓存证书 */ private function cacheCerts() { if (empty($this->platformCerts)) { Log::warning('没有可缓存的平台证书'); return; } // 使用最早过期的时间作为缓存过期时间 $expireTime = time(); foreach ($this->platformCerts as $cert) { $certExpire = strtotime($cert['expire_time']); if ($certExpire > $expireTime) { $expireTime = $certExpire; } } $cacheData = [ 'expire_time' => $expireTime, 'certs' => $this->platformCerts, ]; $cacheFile = $this->certPath . 'platform_certs.cache'; $result = file_put_contents($cacheFile, json_encode($cacheData)); if ($result === false) { Log::error('保存平台证书缓存失败: ' . $cacheFile); } else { Log::info('平台证书缓存保存成功，有效期至: ' . date('Y-m-d H:i:s', $expireTime)); } // 保存证书文件 foreach ($this->platformCerts as $cert) { $certFile = $this->certPath . $cert['serial_no'] . '.pem'; if (file_put_contents($certFile, $cert['cert']) === false) { Log::error('保存平台证书文件失败: ' . $certFile); } else { Log::info('平台证书文件保存成功: ' . $certFile); } } } /** * 生成随机字符串 */ private function generateNonce($length = 16) { $chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'; $nonce = ''; for ($i = 0; $i < $length; $i++) { $nonce .= $chars[random_int(0, strlen($chars) - 1)]; } return $nonce; } /** * 获取最新的平台证书 * @return string 证书内容 */ public function getLatestCert() { $certs = $this->getPlatformCerts(); if (empty($certs)) { throw new Exception('没有可用的平台证书'); } return $certs[0]['cert']; } /** * 根据序列号获取平台证书 * @param string $serialNo 证书序列号 * @return string 证书内容 */ public function getCertBySerialNo($serialNo) { $certs = $this->getPlatformCerts(); foreach ($certs as $cert) { if ($cert['serial_no'] === $serialNo) { return $cert['cert']; } } throw new Exception('找不到序列号为 ' . $serialNo . ' 的平台证书'); } /** * 获取配置参数 * @param string $key 配置键名 * @return mixed 配置值 */ public function getConfig($key = null) { if ($key === null) { return $this->config; } return isset($this->config[$key]) ? $this->config[$key] : null; } /** * 获取证书保存路径 * @return string 证书保存路径 */ public function getCertPath() { return $this->certPath; } }
<?php // 配置连接的IP、端口、以及相应的数据库 $server = array( 'host' => '127.0.0.1', 'port' => 6379, 'database' => 15 ); $redis = new Client($server); // 普通set/get操作 $redis->set('library', 'predis'); $retval = $redis->get('library'); echo $retval; // 显示 'predis' // setex set一个存储时效 $redis->setex('str', 10, 'bar'); // 表示存储有效期为10秒 // setnx/msetnx相当于add操作,不会覆盖已有值 $redis->setnx('foo', 12); // true $redis->setnx('foo', 34); // false // getset操作,set的变种,结果返回替换前的值 $redis->getset('foo', 56); // 返回34 // incrby/incr/decrby/decr 对值的递增和递减 $redis->incr('foo'); // foo为57 $redis->incrby('foo', 2); // foo为59 // exists检测是否存在某值 $redis->exists('foo'); // true // del 删除 $redis->del('foo'); // true // type 类型检测,字符串返回string,列表返回 list,set表返回set/zset,hash表返回hash $redis->type('foo'); // 不存在,返回none $redis->set('str', 'test'); $redis->type('str'); // 字符串，返回string // append 连接到已存在字符串 $redis->append('str', '_123'); // 返回累加后的字符串长度8,此进str为 'test_123' // setrange 部分替换操作 $redis->setrange('str', 0, 'abc'); // 返回3,参数2为0时等同于set操作 $redis->setrange('str', 2, 'cd'); // 返回4,表示从第2个字符后替换,这时'str'为'abcd' // substr 部分获取操作 $redis->substr('str', 0, 2); // 表示从第0个起，取到第2个字符，共3个，返回'abc' // strlen 获取字符串长度 $redis->strlen('str'); // 返回4 // setbit/getbit 位存储和获取 $redis->setbit('binary', 31, 1); // 表示在第31位存入1,这边可能会有大小端问题?不过没关系,getbit 应该不会有问题 $redis->getbit('binary', 31); // 返回1 // keys 模糊查找功能,支持*号以及?号(匹配一个字符) $redis->set('foo1', 123); $redis->set('foo2', 456); $redis->keys('foo*'); // 返回foo1和foo2的array $redis->keys('f?o?'); // 同上 // randomkey 随机返回一个key $redis->randomkey(); // 可能是返回 'foo1'或者是'foo2'及其它任何一存在redis的key // rename/renamenx 对key进行改名,所不同的是renamenx不允许改成已存在的key $redis->rename('str', 'str2'); // 把原先命名为'str'的key改成了'str2' // expire 设置key-value的时效性,ttl 获取剩余有效期,persist 重新设置为永久存储 $redis->expire('foo', 1); // 设置有效期为1秒 $redis->ttl('foo'); // 返回有效期值1s $redis->expire('foo'); // 取消expire行为 // dbsize 返回redis当前数据库的记录总数 $redis->dbsize(); /* * 队列操作 */ // rpush/rpushx 有序列表操作,从队列后插入元素 // lpush/lpushx 和rpush/rpushx的区别是插入到队列的头部,同上,'x'含义是只对已存在的key进行操作 $redis->rpush('fooList', 'bar1'); // 返回一个列表的长度1 $redis->lpush('fooList', 'bar0'); // 返回一个列表的长度2 $redis->rpushx('fooList', 'bar2'); // 返回3,rpushx只对已存在的队列做添加,否则返回0 // llen返回当前列表长度 $redis->llen('fooList'); // 3 // lrange 返回队列中一个区间的元素 $redis->lrange('fooList', 0, 1); // 返回数组包含第0个至第1个共2个元素 $redis->lrange('fooList', 0, -1); // 返回第0个至倒数第一个,相当于返回所有元素,注意redis中很多时候会用到负数,下同 // lindex 返回指定顺序位置的list元素 $redis->lindex('fooList', 1); // 返回'bar1' // lset 修改队列中指定位置的value $redis->lset('fooList', 1, '123'); // 修改位置1的元素,返回true // lrem 删除队列中左起指定数量的字符 $redis->lrem('fooList', 1, '_'); // 删除队列中左起(右起使用-1)1个字符'_'(若有) // lpop/rpop 类似栈结构地弹出(并删除)最左或最右的一个元素 $redis->lpop('fooList'); // 'bar0' $redis->rpop('fooList'); // 'bar2' // ltrim 队列修改，保留左边起若干元素，其余删除 $redis->ltrim('fooList', 0, 1); // 保留左边起第0个至第1个元素 // rpoplpush 从一个队列中pop出元素并push到另一个队列 $redis->rpush('list1', 'ab0'); $redis->rpush('list1', 'ab1'); $redis->rpush('list2', 'ab2'); $redis->rpush('list2', 'ab3'); $redis->rpoplpush('list1', 'list2'); // 结果list1 => array('ab0'), list2 => array('ab1','ab2','ab3') $redis->rpoplpush('list2', 'list2'); // 也适用于同一个队列,把最后一个元素移到头部 list2 => array('ab3','ab1','ab2') // linsert 在队列的中间指定元素前或后插入元素 $redis->linsert('list2', 'before', 'ab1', '123'); // 表示在元素'ab1'之前插入'123' $redis->linsert('list2', 'after', 'ab1', '456'); // 表示在元素'ab1'之后插入'456' // blpop/brpop 阻塞并等待一个列队不为空时，再pop出最左或最右的一个元素（这个功能在php以外可以说非常好用） // brpoplpush 同样是阻塞并等待操作，结果同rpoplpush一样 $redis->blpop('list3', 10); // 如果list3为空则一直等待,直到不为空时将第一元素弹出,10秒后超时 /** * set表操作 */ // sadd 增加元素,返回true,重复返回false $redis->sadd('set1', 'ab'); $redis->sadd('set1', 'cd'); $redis->sadd('set1', 'ef'); // srem 移除指定元素 $redis->srem('set1', 'cd'); // 删除'cd'元素 // spop 弹出首元素 $redis->spop('set1'); // smove 移动当前set表的指定元素到另一个set表 $redis->sadd('set2', '123'); $redis->smove('set1', 'set2', 'ab'); // 移动'set1'中的'ab'到'set2',返回true or false // scard 返回当前set表元素个数 $redis->scard('set2'); // 2 // sismember 判断元素是否属于当前表 $redis->sismember('set2', '123'); // true or false // smembers 返回当前表的所有元素 $redis->smembers('set2'); // array('123','ab') // sinter/sunion/sdiff 返回两个表中元素的交集/并集/补集 $redis->sadd('set1', 'ab'); $redis->sinter('set2', 'set1'); // 返回array('ab') // sinterstore/sunionstore/sdiffstore 将两个表交集/并集/补集元素copy到第三个表中 $redis->set('foo', 0); $redis->sinterstore('foo', 'set1'); // 这边等同于将'set1'的内容copy到'foo'中，并将'foo'转为set表 $redis->sinterstore('foo', array('set1', 'set2')); // 将'set1'和'set2'中相同的元素copy到'foo'表中,覆盖'foo'原有内容 // srandmember 返回表中一个随机元素 $redis->srandmember('set1'); /** * 有序set表操作 */ // zadd 增加元素,并设置序号,返回true,重复返回false $redis->zadd('zset1', 1, 'ab'); $redis->zadd('zset1', 2, 'cd'); $redis->zadd('zset1', 3, 'ef'); // zincrby 对指定元素索引值的增减,改变元素排列次序 $redis->zincrby('zset1', 10, 'ab'); // 返回11 // zrem 移除指定元素 $redis->zrem('zset1', 'ef'); // true or false // zrange 按位置次序返回表中指定区间的元素 $redis->zrange('zset1', 0, 1); // 返回位置0和1之间(两个)的元素 $redis->zrange('zset1', 0, -1); // 返回位置0和倒数第一个元素之间的元素(相当于所有元素) // zrevrange 同上,返回表中指定区间的元素,按次序倒排 $redis->zrevrange('zset1', 0, -1); // 元素顺序和zrange相反 // zrangebyscore/zrevrangebyscore 按顺序/降序返回表中指定索引区间的元素 $redis->zadd('zset1', 3, 'ef'); $redis->zadd('zset1', 5, 'gh'); $redis->zrangebyscore('zset1', 2, 9); // 返回索引值2-9之间的元素 array('ef','gh') // 参数形式 $redis->zrangebyscore('zset1', 2, 9, 'withscores'); // 返回索引值2-9之间的元素并包含索引值 array(array('ef',3),array('gh',5)) $redis->zrangebyscore('zset1', 2, 9, array( 'withscores' => true, 'limit' => array(1, 2) )); // 返回索引值2-9之间的元素,'withscores'=>true表示包含索引值; 'limit'=>array(1,2),表示最多返回2条,结果为array(array('ef',3),array('gh',5)) // zunionstore/zinterstore 将多个表的并集/交集存入另一个表中 $redis->zunionstore('zset3', array('zset1', 'zset2', 'zset0')); // 将'zset1','zset2','zset0'的并集存入'zset3' // 其它参数 $redis->zunionstore('zset3', array('zset1', 'zset2'), array( 'weights' => array(5, 0) )); // weights参数表示权重，其中表示并集后值大于5的元素排在前，大于0的排在后 $redis->zunionstore('zset3', array('zset1', 'zset2'), array( 'aggregate' => 'max' )); // 'aggregate' => 'max'或'min'表示并集后相同的元素是取大值或是取小值 // zcount 统计一个索引区间的元素个数 $redis->zcount('zset1', 3, 5); // 2 $redis->zcount('zset1', '(3', 5); // '(3'表示索引值在3-5之间但不含3,同理也可以使用'(5'表示上限为5但不含5 // zcard 统计元素个数 $redis->zcard('zset1'); // 4 // zscore 查询元素的索引 $redis->zscore('zset1', 'ef'); // 3 // zremrangebyscore 删除一个索引区间的元素 $redis->zremrangebyscore('zset1', 0, 2); // 删除索引在0-2之间的元素('ab','cd'),返回删除元素个数2 // zrank/zrevrank 返回元素所在表顺序/降序的位置(不是索引) $redis->zrank('zset1', 'ef'); // 返回0,因为它是第一个元素;zrevrank则返回1(最后一个) // zremrangebyrank 删除表中指定位置区间的元素 $redis->zremrangebyrank('zset1', 0, 10); // 删除位置为0-10的元素,返回删除的元素个数2 /** * hash表操作 */ // hset/hget 存取hash表的数据 $redis->hset('hash1', 'key1', 'v1'); // 将key为'key1' value为'v1'的元素存入hash1表 $redis->hset('hash1', 'key2', 'v2'); $redis->hget('hash1', 'key1'); // 取出表'hash1'中的key 'key1'的值,返回'v1' // hexists 返回hash表中的指定key是否存在 $redis->hexists('hash1', 'key1'); // true or false // hdel 删除hash表中指定key的元素 $redis->hdel('hash1', 'key2'); // true or false // hlen 返回hash表元素个数 $redis->hlen('hash1'); // 1 // hsetnx 增加一个元素,但不能重复 $redis->hsetnx('hash1', 'key1', 'v2'); // false $redis->hsetnx('hash1', 'key2', 'v2'); // true // hmset/hmget 存取多个元素到hash表 $redis->hmset('hash1', array( 'key3' => 'v3', 'key4' => 'v4' )); $redis->hmget('hash1', array('key3', 'key4')); // 返回相应的值 array('v3','v4') // hincrby 对指定key进行累加 $redis->hincrby('hash1', 'key5', 3); // 返回3 $redis->hincrby('hash1', 'key5', 10); // 返回13 // hkeys 返回hash表中的所有key $redis->hkeys('hash1'); // 返回array('key1','key2','key3','key4','key5') // hvals 返回hash表中的所有value $redis->hvals('hash1'); // 返回array('v1','v2','v3','v4',13) // hgetall 返回整个hash表元素 $redis->hgetall('hash1'); // 返回array('key1'=>'v1','key2'=>'v2','key3'=>'v3','key4'=>'v4','key5'=>13) /** * 排序操作 */ // sort 排序 $redis->rpush('tab', 3); $redis->rpush('tab', 2); $redis->rpush('tab', 17); $redis->sort('tab'); // 返回array(2,3,17) // 使用参数,可组合使用 array('sort' => 'desc','limit' => array(1, 2)) $redis->sort('tab', array('sort' => 'desc')); // 降序排列,返回array(17,3,2) $redis->sort('tab', array('limit' => array(1, 2))); // 返回顺序位置中1的元素2个(这里的2是指个数,而不是位置),返回array(3,17) $redis->sort('tab', array('limit' => array('alpha' => true))); // 按首字符排序返回array(17,2,3),因为17的首字符是'1'所以排首位置 $redis->sort('tab', array('limit' => array('store' => 'ordered'))); // 表示永久性排序,返回元素个数 $redis->sort('tab', array('limit' => array('get' => 'pre_*'))); // 使用了通配符'*'过滤元素,表示只返回以'pre_'开头的元素 /** * redis管理操作 */ // select 指定要操作的数据库 $redis->select('mydb'); // 指定为mydb,不存在则创建 // flushdb 清空当前库 $redis->flushdb(); // move 移动当库的元素到其它库 $redis->set('foo', 'bar'); $redis->move('foo', 'mydb2'); // 若'mydb2'库存在 // info 显示服务当状态信息 $redis->info(); // slaveof 配置从服务器 $redis->slaveof('127.0.0.1', 80); // 配置127.0.0.1端口80的服务器为从服务器 $redis->slaveof(); // 清除从服务器 // 同步保存服务器数据到磁盘 $redis->save(); // 异步保存服务器数据到磁盘 $redis->bgsave(); // ?? $redis->bgrewriteaof(); // 返回最后更新磁盘的时间 $redis->lastsave(); // set/get多个key-value $mkv = array( 'usr:0001' => 'First user', 'usr:0002' => 'Second user', 'usr:0003' => 'Third user' ); $redis->mset($mkv); // 存储多个key对应的value $retval = $redis->mget(array_keys($mkv)); // 获取多个key对应的value print_r($retval); // 批量操作 $replies = $redis->pipeline(function($pipe) { $pipe->ping(); $pipe->flushdb(); $pipe->incrby('counter', 10); // 增量操作 $pipe->incrby('counter', 30); $pipe->exists('counter'); $pipe->get('counter'); $pipe->mget('does_not_exist', 'counter'); }); print_r($replies); // CAS,事务性操作 function zpop($client, $zsetKey) { $element = null; $options = array( 'cas' => true, // Initialize with support for CAS operations 'watch' => $zsetKey, // Key that needs to be WATCHed to detect changes 'retry' => 3, // Number of retries on aborted transactions, after // which the client bails out with an exception. ); $txReply = $client->multiExec($options, function($tx) use ($zsetKey, &$element) { @list($element) = $tx->zrange($zsetKey, 0, 0); if (isset($element)) { $tx->multi(); // With CAS, MULTI *must* be explicitly invoked. $tx->zrem($zsetKey, $element); } }); return $element; } $zpopped = zpop($redis, 'zset'); echo isset($zpopped) ? "ZPOPed $zpopped" : "Nothing to ZPOP!", "\n"; // 对存取的key加前缀,如: 'nrk:' $redis->getProfile()->setPreprocessor(new KeyPrefixPreprocessor('nrk:')); // 分布式存储的一些方法 $multiple_servers = array( array( 'host' => '127.0.0.1', 'port' => 6379, 'database' => 15, 'alias' => 'first', ), array( 'host' => '127.0.0.1', 'port' => 6380, 'database' => 15, 'alias' => 'second', ), ); use Predis\Distribution\IDistributionStrategy; class NaiveDistributionStrategy implements IDistributionStrategy { private $_nodes, $_nodesCount; public function __construct() { $this->_nodes = array(); $this->_nodesCount = 0; } public function add($node, $weight = null) { $this->_nodes[] = $node; $this->_nodesCount++; } public function remove($node) { $this->_nodes = array_filter($this->_nodes, function($n) use ($node) { return $n !== $node; }); $this->_nodesCount = count($this->_nodes); } public function get($key) { $count = $this->_nodesCount; if ($count === 0) { throw new RuntimeException('No connections'); } return $this->_nodes[$count > 1 ? abs(crc32($key) % $count) : 0]; } public function generateKey($value) { return crc32($value); } } // 配置键分布策略 $options = array( 'key_distribution' => new NaiveDistributionStrategy(), ); $redis = new Predis\Client($multiple_servers, $options); for ($i = 0; $i < 100; $i++) { $redis->set("key:$i", str_pad($i, 4, '0', 0)); $redis->get("key:$i"); } $server1 = $redis->getClientFor('first')->info(); $server2 = $redis->getClientFor('second')->info(); printf("Server '%s' has %d keys while server '%s' has %d keys.\n", 'first', $server1['db15']['keys'], 'second', $server2['db15']['keys'] );
PHP bc系列函数（高精度数学运算）简介：bc系列函数用于处理任意精度的十进制计算，适用于金融、科学计算等对精度要求极高的场景。需启用bcmath扩展（可通过phpinfo()检查是否启用）。特点：所有参数和返回值均为字符串类型（避免精度丢失）支持自定义小数位数（scale参数）提供基本运算、比较、幂运算等完整功能一、基础运算函数1. 加法：bcadd()// 语法：bcadd(左操作数, 右操作数, 保留小数位数) echo bcadd('123.456', '78.9', 2); // 202.36（123.456 + 78.9 = 202.356，保留2位小数） echo bcadd('100', '200', 0); // 300（整数相加）2. 减法：bcsub()// 语法：bcsub(被减数, 减数, 保留小数位数) echo bcsub('100.5', '30.25', 1); // 70.2（100.5 - 30.25 = 70.25，保留1位小数） echo bcsub('50', '75', 0); // -25（负数结果）3. 乘法：bcmul()// 语法：bcmul(乘数1, 乘数2, 保留小数位数) echo bcmul('2.5', '4.2', 2); // 10.50（2.5 × 4.2 = 10.5，保留2位小数） echo bcmul('100', '0.01', 0); // 1（整数与小数相乘）4. 除法：bcdiv()// 语法：bcdiv(被除数, 除数, 保留小数位数) echo bcdiv('10', '3', 3); // 3.333（10 ÷ 3 ≈ 3.3333...，保留3位） var_dump(bcdiv('5', '0', 0)); // false（除数为0时返回false）5. 取模（余数）：bcmod()// 语法：bcmod(被除数, 除数) echo bcmod('10', '3'); // 1（10 ÷ 3 余数为1） echo bcmod('7.5', '2.5'); // 0.0（7.5 是 2.5 的整数倍）二、比较函数bccomp()// 语法：bccomp(值1, 值2, 比较精度) // 返回值：1（值1 > 值2）、0（相等）、-1（值1 < 值2） echo bccomp('10', '10', 0); // 0（相等） echo bccomp('10.5', '10.49', 1); // 1（10.5 > 10.49，精确到1位小数） echo bccomp('5.999', '6', 2); // -1（5.999 < 6，精确到2位小数）三、幂运算与开方1. 幂运算：bcpow()// 语法：bcpow(底数, 指数, 保留小数位数) echo bcpow('2', '3', 0); // 8（2³ = 8） echo bcpow('1.2', '2', 2); // 1.44（1.2² = 1.44）2. 幂运算取模：bcpowmod()// 语法：bcpowmod(底数, 指数, 模数, 保留小数位数) // 作用：(base^exponent) % modulus，比先算幂再取模更高效 echo bcpowmod('3', '3', '5', 0); // 2（3³=27，27%5=2）3. 开平方：bcsqrt()// 语法：bcsqrt(数值, 保留小数位数) echo bcsqrt('25', 0); // 5（√25 = 5） echo bcsqrt('2', 4); // 1.4142（√2 ≈ 1.4142，保留4位） var_dump(bcsqrt('-4', 0)); // false（负数无法开平方）四、全局配置函数bcscale()// 功能：设置所有bc函数的默认小数位数（全局生效） bcscale(2); // 全局默认保留2位小数 echo bcadd('1', '2'); // 3.00（使用全局配置） echo bcsub('5', '3.5'); // 1.50（自动补全2位小数） echo bcadd('1', '2', 0); // 3（局部参数优先级高于全局）关键注意事项参数类型：必须传入字符串（如'123.45'），而非数字（避免自动转换导致精度丢失）。返回值：始终为字符串，需转换类型时用(float)或(int)。扩展依赖：需在php.ini中启用extension=bcmath（重启服务生效）。精度优先级：函数的scale参数 > bcscale()全局配置。通过这些函数，PHP可以实现高精度计算，解决普通数值类型精度不足的问题。
以下是 Nginx、PostgreSQL、PHP、Redis 和 SSDB 在 Linux 系统下的常用管理命令及配置文件位置总结：1. Nginx启动/关闭/重启# 使用 systemd（CentOS 7+/Ubuntu 16.04+） systemctl start nginx # 启动 systemctl stop nginx # 停止 systemctl restart nginx # 重启 systemctl reload nginx # 重新加载配置 systemctl status nginx # 查看状态 # 使用 service（旧版系统） service nginx start|stop|restart|reload # 直接调用 nginx 命令（需 root 权限） nginx # 启动 nginx -s stop # 快速停止 nginx -s quit # 优雅停止 nginx -s reload # 重新加载配置配置文件主配置文件：/etc/nginx/nginx.conf站点配置目录：/etc/nginx/conf.d/ 或 /etc/nginx/sites-enabled/修改后检查配置并重启：nginx -t # 检查配置语法 systemctl reload nginx # 重新加载配置2. PostgreSQL启动/关闭/重启# 使用 systemd systemctl start postgresql-<版本号> # 启动（如：postgresql-14） systemctl stop postgresql-<版本号> # 停止 systemctl restart postgresql-<版本号> # 重启 systemctl status postgresql-<版本号> # 查看状态 # Ubuntu/Debian 通用命令 systemctl start|stop|restart postgresql # 手动控制（需切换到 postgres 用户） sudo -u postgres pg_ctl -D /var/lib/pgsql/<版本号>/data start|stop|restart配置文件主配置文件：/var/lib/pgsql/<版本号>/data/postgresql.conf认证配置：/var/lib/pgsql/<版本号>/data/pg_hba.conf修改后重启服务：systemctl restart postgresql-<版本号>3. PHP-FPM启动/关闭/重启# 使用 systemd（PHP-FPM 通常作为 FastCGI 进程管理器） systemctl start php-fpm # 启动（CentOS/RHEL） systemctl start php<版本号>-fpm # 启动（Ubuntu/Debian，如：php8.1-fpm） systemctl stop|restart|status php-fpm # 直接调用 php-fpm 命令 php-fpm -D # 后台启动 php-fpm -t # 检查配置语法 kill -USR2 `cat /run/php-fpm.pid` # 优雅重启配置文件主配置文件：/etc/php-fpm.conf 或 /etc/php/<版本号>/fpm/php-fpm.conf池配置目录：/etc/php-fpm.d/ 或 /etc/php/<版本号>/fpm/pool.d/PHP .ini 文件：/etc/php.ini 或 /etc/php/<版本号>/fpm/php.ini修改后重启服务：systemctl restart php-fpm4. Redis启动/关闭/重启# 使用 systemd systemctl start redis # 启动 systemctl stop redis # 停止 systemctl restart redis # 重启 systemctl status redis # 查看状态 # 手动启动（需指定配置文件） redis-server /etc/redis.conf # 前台启动 redis-server /etc/redis.conf & # 后台启动 # 关闭 Redis（通过客户端） redis-cli shutdown配置文件主配置文件：/etc/redis.conf修改后重启服务：systemctl restart redis5. SSDB启动/关闭/重启# 使用 systemd（需手动创建 service 文件） systemctl start ssdb # 启动 systemctl stop ssdb # 停止 systemctl restart ssdb # 重启 # 手动控制（通过 ssdb-server 命令） /opt/ssdb/ssdb-server /opt/ssdb/ssdb.conf -d # 后台启动 /opt/ssdb/ssdb-cli -h 127.0.0.1 -p 8888 shutdown # 关闭配置文件主配置文件：/opt/ssdb/ssdb.conf修改后重启服务：systemctl restart ssdb注意事项路径差异：不同 Linux 发行版或安装方式（源码/包管理器）可能导致配置文件路径不同，建议通过 whereis 或 find 命令查找。权限问题：操作服务时需确保用户有足够权限（如使用 sudo）。配置检查：修改配置文件后，建议先检查语法再重启服务。版本兼容性：部分命令可能因软件版本不同而略有差异。
一、触控板使用技巧双指操作双指滑动：实现页面上下滚动双指开合：进行缩放操作双指左右滑动：在浏览器或桌面空间之间进行切换三指操作三指上推：显示所有打开的应用程序三指下滑：隐藏当前应用程序，显示桌面三指左右滑动：在全屏应用程序之间进行切换四指操作四指捏合：打开Launchpad（应用程序启动器）四指展开：显示应用程序坞二、Homebrew包管理器Homebrew是macOS系统上常用的包管理器，可用于安装各种软件。安装Homebrew打开终端，执行以下命令：/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"常用brew命令brew install <package>：安装软件包brew uninstall <package>：卸载软件包brew upgrade <package>：升级软件包brew list：查看已安装的软件包brew search <package>：搜索软件包brew update：更新Homebrew自身brew doctor：检查Homebrew的状态三、macOS系统目录结构主要目录/Applications：这里存放的是用户安装的应用程序/System：包含系统文件，一般情况下不要对其进行修改/Users：用户主目录，例如/Users/your_username/Library：系统级别的资源库/usr：包含Unix工具和开发文件/tmp：临时文件目录，系统重启后该目录下的文件会被清除用户目录~/Documents：用于存放文档~/Downloads：下载文件的存放位置~/Desktop：桌面文件的存放处~/Pictures：图片文件的存放目录~/Movies：视频文件的存放目录~/Music：音乐文件的存放目录四、常用Linux命令（macOS基于Unix）文件操作ls：列出目录内容ls -l：以长格式显示文件和目录信息ls -a：显示包括隐藏文件在内的所有文件cd <directory>：切换到指定目录cd ..：返回上一级目录mkdir <directory>：创建新目录rm <file>：删除文件rm -r <directory>：递归删除目录及其内容cp <destination>：复制文件或目录mv <destination>：移动或重命名文件文件查看cat <file>：查看文件内容less <file>：分页查看文件内容head <file>：显示文件的开头部分tail <file>：显示文件的结尾部分权限管理chmod <permissions> <file>：修改文件权限chown <owner> <file>：修改文件所有者搜索grep <pattern> <file>：在文件中搜索指定模式find <directory> -name <pattern>：在目录中查找文件系统信息uname -a：显示系统信息df -h：查看磁盘使用情况free -h：查看内存使用情况top：显示正在运行的进程htop：一个更高级的进程查看器（需要先使用brew安装）网络ping <host>：测试与主机的连接ifconfig：显示网络接口信息curl <url>：下载URL内容wget <url>：下载文件（需要先使用brew安装）五、终端快捷键光标移动Ctrl + A：将光标移动到行首Ctrl + E：将光标移动到行尾Ctrl + F：光标向前移动一个字符Ctrl + B：光标向后移动一个字符历史命令Up Arrow：查看上一条命令Down Arrow：查看下一条命令Ctrl + R：搜索历史命令其他Ctrl + L：清屏Ctrl + C：中断当前命令Ctrl + D：退出终端会话六、其他实用技巧截图Command + Shift + 3：截取整个屏幕Command + Shift + 4：截取屏幕的一部分Command + Shift + 5：打开截图工具（可录制屏幕）快速操作Command + Tab：在应用程序之间进行切换Command + Space：打开Spotlight搜索Option + Click：在应用程序坞中查看应用程序的所有窗口Command + H：隐藏当前应用程序Command + Q：退出当前应用程序
以下是一个包含 各种字段类型、约束条件、索引和特殊配置 的ThinkPHP8数据库迁移示例（以products产品表为例），覆盖常见场景：迁移文件示例（database/migrations/20250726153000_create_products_table.php）<?php use think\migration\Migrator; use think\migration\db\Column; class CreateProductsTable extends Migrator { /** * 执行迁移（创建表结构） */ public function change() { // 创建products表，配置表级参数 $table = $this->table('products', [ 'engine' => 'InnoDB', // 数据库引擎 'charset' => 'utf8mb4', // 字符集 'collation' => 'utf8mb4_unicode_ci', // 排序规则 'comment' => '产品表（包含多种字段类型示例）', // 表注释 'auto_increment' => 1000, // 自增ID起始值（MySQL支持） ]); // 字段定义（覆盖各种场景） $table // 1. 自增主键（整数型，无符号，自增） ->addColumn('id', 'integer', [ 'identity' => true, // 自增 'unsigned' => true, // 无符号（只存正数） 'comment' => '产品ID（主键）' ]) // 2. 字符串（有限长度，非空，唯一索引） ->addColumn('sku', 'string', [ 'limit' => 50, // 长度限制 'null' => false, // 非空（默认） 'comment' => '产品SKU（唯一标识）', 'unique' => true // 唯一约束 ]) // 3. 字符串（长文本，可空，带默认值） ->addColumn('name', 'string', [ 'limit' => 255, 'null' => false, 'default' => '', // 默认空字符串 'comment' => '产品名称' ]) // 4. 文本类型（无长度限制，可空） ->addColumn('description', 'text', [ 'null' => true, // 允许为空 'comment' => '产品详细描述' ]) // 5. 小数类型（高精度，带默认值） ->addColumn('price', 'decimal', [ 'precision' => 10, // 总位数 'scale' => 2, // 小数位数 'default' => 0.00, // 默认0.00 'comment' => '产品售价' ]) // 6. 整数类型（无符号，默认值，索引） ->addColumn('stock', 'integer', [ 'unsigned' => true, // 无符号（只存正数） 'default' => 0, // 默认库存0 'comment' => '库存数量', 'index' => true // 普通索引 ]) // 7. 枚举类型（固定可选值） ->addColumn('status', 'enum', [ 'values' => ['draft', 'active', 'disabled'], // 可选值 'default' => 'draft', // 默认草稿状态 'comment' => '产品状态（draft:草稿, active:上架, disabled:下架）' ]) // 8. 布尔类型（tinyint，默认true） ->addColumn('is_recommend', 'boolean', [ 'default' => true, // 默认true（1） 'comment' => '是否推荐（1:是, 0:否）' ]) // 9. 外键关联（整数，无符号，级联操作） ->addColumn('category_id', 'integer', [ 'unsigned' => true, 'null' => false, 'comment' => '所属分类ID', 'index' => true // 普通索引 ]) // 10. JSON类型（存储复杂结构，可空） ->addColumn('tags', 'json', [ 'null' => true, 'comment' => '产品标签（JSON格式，如["新品","热卖"]）' ]) // 11. 时间戳类型（自动维护） ->addTimestamps() // 自动添加 create_time 和 update_time（datetime类型） // 12. 软删除字段（自动维护删除时间） ->addSoftDelete() // 添加 delete_time 字段（软删除标识） // 13. 整数类型（排序权重，默认0） ->addColumn('sort', 'integer', [ 'default' => 0, 'comment' => '排序权重（值越大越靠前）' ]) // 添加外键约束（关联分类表） ->addForeignKey('category_id', 'categories', 'id', [ 'delete' => 'CASCADE', // 分类删除时，关联产品也删除 'update' => 'CASCADE' // 分类ID更新时，产品关联ID同步更新 ]) // 添加联合索引（名称+状态） ->addIndex(['name', 'status'], [ 'name' => 'idx_name_status' // 自定义索引名 ]) // 执行创建表 ->create(); } /** * 回滚迁移（删除表） */ public function down() { // 先删除外键约束（避免删除表失败） $this->table('products')->dropForeignKey('category_id'); // 再删除表 $this->dropTable('products'); } }关键字段配置说明字段名类型/配置核心特点id自增整数、主键无符号，自增起始值1000sku字符串、唯一约束唯一标识，非空，长度50name字符串、默认值非空，默认空字符串，用于产品名称description文本类型、可空无长度限制，允许为空（适合长文本描述）price小数(10,2)高精度价格，默认0.00stock无符号整数、索引库存数量，非负，普通索引提升查询效率status枚举类型固定可选值，默认草稿状态is_recommend布尔类型本质tinyint，默认true（1）category_id外键、级联操作关联分类表，删除/更新时级联处理tagsJSON类型存储数组/对象结构，适合非结构化数据create_time时间戳（自动维护）由addTimestamps()生成，记录创建时间delete_time软删除字段由addSoftDelete()生成，非NULL表示已删除执行迁移# 创建表 php think migrate:run # 回滚（删除表） php think migrate:rollback注意事项外键依赖：示例中category_id关联categories表，需确保categories表已存在（可先创建分类表迁移）。索引优化：根据查询场景添加索引，避免过度索引影响写入性能。软删除：addSoftDelete()会自动添加delete_time字段（datetime类型，默认NULL），适合逻辑删除。枚举值：修改枚举值需谨慎，生产环境可能需要先添加新值再迁移数据。字段注释：每个字段添加清晰注释，便于后期维护（尤其团队协作场景）。通过这个示例，可以覆盖ThinkPHP8迁移工具中绝大多数字段配置场景，实际开发中可根据需求调整。
ThinkPHP8 数据库迁移与数据填充完全指南（官方文档补充版）一、安装与配置1. 安装迁移扩展composer require topthink/think-migration2. 配置文件说明配置文件位于 config/database.php默认迁移表名：think_migration支持多数据库连接3. 环境要求PHP >= 8.1ThinkPHP >= 8.0建议使用 InnoDB 引擎（支持事务）二、迁移文件操作1. 创建迁移文件# 创建基础迁移文件 php think migrate:create CreateUsersTable # 创建带表名的迁移文件（自动生成基础结构） php think migrate:create AddEmailToUsersTable --table=users # 指定数据库连接 php think migrate:create CreateLogsTable --connection=log2. 迁移文件结构生成的迁移文件位于 database/migrations，示例：<?php use think\migration\Migrator; use think\migration\db\Column; class CreateUsersTable extends Migrator { /** * 执行迁移（向上操作） */ public function up() { $table = $this->table('users'); $table->addColumn('name', 'string', ['limit' => 50]) ->addColumn('email', 'string', ['limit' => 100, 'unique' => true]) ->addColumn('password', 'string', ['limit' => 100]) ->addTimestamps() // 自动添加 create_time 和 update_time 字段 ->create(); } /** * 回滚迁移（向下操作） */ public function down() { $this->dropTable('users'); } }三、常用字段类型字段类型描述示例用法integer整数类型->addColumn('age', 'integer')biginteger大整数类型->addColumn('user_id', 'biginteger')string字符串类型->addColumn('name', 'string', ['limit' => 50])text长文本类型->addColumn('content', 'text')datetime日期时间类型->addColumn('created_at', 'datetime')timestamp时间戳类型->addColumn('updated_at', 'timestamp')boolean布尔类型->addColumn('status', 'boolean', ['default' => 0])decimal高精度小数->addColumn('price', 'decimal', ['precision' => 10, 'scale' => 2])enum枚举类型->addColumn('gender', 'enum', ['values' => ['male', 'female', 'other']])四、表结构操作1. 创建表public function up() { $table = $this->table('users', [ 'engine' => 'InnoDB', 'charset' => 'utf8mb4', 'comment' => '用户表', 'collation' => 'utf8mb4_unicode_ci' ]); $table->addColumn('id', 'integer', ['identity' => true]) // 自增ID（默认主键） ->addColumn('username', 'string', ['limit' => 30]) ->addColumn('email', 'string', ['limit' => 100, 'unique' => true]) ->addColumn('password', 'string', ['limit' => 100]) ->addColumn('status', 'boolean', ['default' => 1]) ->addIndex(['username']) // 普通索引 ->addUniqueIndex(['email']) // 唯一索引 ->addTimestamps() // 自动添加 create_time 和 update_time ->addSoftDelete() // 添加 delete_time 软删除字段 ->create(); }2. 修改表添加字段：public function up() { $this->table('users') ->addColumn('phone', 'string', ['limit' => 20, 'after' => 'email']) ->update(); }修改字段：public function up() { $this->table('users') ->changeColumn('phone', 'string', ['limit' => 11, 'default' => '']) ->update(); }删除字段：public function up() { $this->table('users') ->removeColumn('phone') ->update(); }3. 添加外键约束public function up() { $this->table('posts') ->addColumn('user_id', 'integer') ->addColumn('title', 'string') ->addColumn('content', 'text') ->addForeignKey('user_id', 'users', 'id', ['delete' => 'CASCADE', 'update' => 'CASCADE']) ->update(); }五、执行迁移命令1. 执行所有未迁移的文件php think migrate:run2. 回滚上一次迁移php think migrate:rollback3. 回滚到指定版本php think migrate:rollback --target=20250726101500 # 指定时间戳4. 重置所有迁移（先回滚再执行）php think migrate:reset5. 刷新数据库（重置并重新执行所有迁移）php think migrate:refresh6. 查看迁移状态php think migrate:status7. 指定数据库连接php think migrate:run --connection=db_log # 指定日志数据库六、数据填充操作1. 创建数据填充文件php think seed:create Users # 创建Users表的数据填充器2. 编写数据填充逻辑<?php use think\migration\Seeder; use think\facade\Db; class Users extends Seeder { /** * 填充数据 */ public function run() { $data = []; for ($i = 1; $i <= 10; $i++) { $data[] = [ 'username' => 'user' . $i, 'email' => 'user' . $i . '@example.com', 'password' => password_hash('123456', PASSWORD_DEFAULT), 'create_time' => date('Y-m-d H:i:s'), 'update_time' => date('Y-m-d H:i:s') ]; } // 使用批量插入提高性能 Db::name('users')->insertAll($data); } }3. 执行数据填充php think seed:run # 执行所有填充器 php think seed:run --seeder Users # 执行指定填充器七、高级技巧1. 使用 change() 方法（简化双向操作）public function change() { $table = $this->table('users'); // 创建表 $table->addColumn('username', 'string') ->addColumn('email', 'string') ->create(); // 修改表 $table->addColumn('phone', 'string') ->update(); }2. 使用事务public function up() { $this->getAdapter()->beginTransaction(); try { $this->table('table1')->addColumn(...)->create(); $this->table('table2')->addColumn(...)->create(); $this->getAdapter()->commit(); } catch (\Exception $e) { $this->getAdapter()->rollBack(); throw $e; } }3. 创建数据库快照php think migrate:snapshot # 创建当前数据库结构快照4. 指定迁移文件路径php think migrate:run --path=database/migrations/custom # 指定自定义路径八、最佳实践1. 命名规范迁移文件：YYYYMMDDHHMMSS_表名_操作.php（自动生成）表名：使用小写字母和下划线（如 user_info）字段名：使用小写字母和下划线（如 create_time）2. 避免复杂SQL单个迁移文件只做单一变更避免在迁移中执行数据迁移操作3. 测试迁移在开发环境充分测试使用测试数据库验证回滚功能4. 生产环境注意事项迁移前备份数据库使用 --pretend 参数预览变更避免在高峰期执行大型迁移5. 团队协作迁移文件提交到版本控制系统避免多人同时修改同一迁移文件拉取代码后先执行 php think migrate:run九、常见问题与解决方案1. 迁移文件冲突问题：多个迁移文件时间戳相近导致执行顺序异常解决方案：使用 --timestamp 参数手动指定时间戳php think migrate:create NewTable --timestamp=202507261530002. 外键约束错误问题：删除表时外键约束阻止操作解决方案：在 down() 方法中先删除外键public function down() { $this->table('posts') ->dropForeignKey('user_id') ->update(); $this->dropTable('posts'); }3. 数据填充重复问题问题：多次执行填充器导致数据重复解决方案：在填充前清空表public function run() { Db::name('users')->delete(true); // 清空表 // 填充新数据 }4. 迁移性能问题问题：大型表迁移缓慢解决方案：分批次执行数据迁移使用数据库原生工具导入大型数据避免在迁移中使用复杂查询
【Nginx】Nginx需具体配置示例（涵盖了复杂场景下的高级应用）
以下是 Nginx 常见场景的具体配置示例，配置文件可存放在 /etc/nginx/conf.d/ 目录下（Linux）或直接修改 nginx.conf（Windows）：一、基础配置：静态网站托管配置文件（/etc/nginx/conf.d/mysite.conf）server { listen 80; server_name example.com www.example.com; # 替换为你的域名 # 网站根目录 root /var/www/html; index index.html index.htm; # 日志配置 access_log /var/log/nginx/access.log; error_log /var/log/nginx/error.log; # 静态文件缓存 location ~* \.(jpg|jpeg|png|gif|ico|css|js)$ { expires 30d; # 静态资源缓存30天 } # 错误页面 error_page 404 /404.html; error_page 500 502 503 504 /50x.html; }操作步骤创建网站目录：sudo mkdir -p /var/www/html添加测试文件：echo "Hello, Nginx!" > /var/www/html/index.html验证配置：nginx -t重载配置：nginx -s reload二、反向代理：转发到后端服务场景1：代理到本地 Node.js 应用（端口3000）server { listen 80; server_name api.example.com; location / { proxy_pass http://localhost:3000; # 后端服务地址 proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; # 超时设置 proxy_connect_timeout 60s; proxy_send_timeout 60s; proxy_read_timeout 60s; } }场景2：代理到多个后端服务器（负载均衡）# 定义后端服务器组 upstream backend_servers { server backend1.example.com weight=5; # 权重5 server backend2.example.com weight=3; # 权重3 server backend3.example.com backup; # 备份服务器 } server { listen 80; server_name example.com; location / { proxy_pass http://backend_servers; # 其他 proxy_set_header 配置同上 } }三、HTTPS 配置（使用 Let's Encrypt 证书）前提条件已安装 Certbot：sudo apt install certbot python3-certbot-nginx域名已解析到服务器 IP自动配置（推荐）sudo certbot --nginx -d example.com -d www.example.comCertbot 会自动修改 Nginx 配置并设置证书自动更新。手动配置示例server { listen 80; server_name example.com www.example.com; return 301 https://$host$request_uri; # 强制 HTTPS } server { listen 443 ssl http2; server_name example.com www.example.com; # 证书路径（Let's Encrypt 默认路径） ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem; # SSL 优化配置 ssl_protocols TLSv1.2 TLSv1.3; ssl_prefer_server_ciphers on; ssl_ciphers ECDHE-RSA-AES256-GCM-SHA512:DHE-RSA-AES256-GCM-SHA512; ssl_session_cache shared:SSL:10m; ssl_session_timeout 10m; # 其他配置（如 root、location 等）同 HTTP 配置 }四、URL 重写与跳转示例1：www 到非 www 跳转server { listen 80; server_name www.example.com; return 301 https://example.com$request_uri; }示例2：路径重写location /old-path/ { rewrite ^/old-path/(.*)$ /new-path/$1 permanent; # 301 永久重定向 }示例3：基于参数跳转if ($args ~* "id=([0-9]+)") { return 301 https://example.com/item/$1; }五、限流配置（防爬/防刷）限制单个 IP 的请求频率# http 块中定义限流规则 http { limit_req_zone $binary_remote_addr zone=mylimit:10m rate=10r/s; server { listen 80; server_name example.com; location /api/ { limit_req zone=mylimit burst=20 nodelay; # 每秒最多10个请求，突发最多20个，超出的请求直接返回503 } } }六、WebSocket 代理支持配置示例server { listen 80; server_name ws.example.com; location /ws/ { proxy_pass http://backend_websocket_server; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "Upgrade"; proxy_set_header Host $host; # 防止 WebSocket 连接超时 proxy_read_timeout 86400; } }七、多站点配置（虚拟主机）在同一服务器托管多个域名# 站点1 server { listen 80; server_name site1.com www.site1.com; root /var/www/site1; } # 站点2 server { listen 80; server_name site2.com www.site2.com; root /var/www/site2; }八、静态文件优化配置配置 Gzip 压缩http { gzip on; gzip_vary on; gzip_proxied any; gzip_comp_level 6; gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript; gzip_buffers 16 8k; gzip_http_version 1.1; }九、常见问题处理配置1. 解决跨域请求（CORS）location /api/ { add_header 'Access-Control-Allow-Origin' '*' always; add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS' always; add_header 'Access-Control-Allow-Headers' 'DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization' always; # OPTIONS 请求直接返回 204 if ($request_method = 'OPTIONS') { return 204; } }2. 隐藏 Nginx 版本信息http { server_tokens off; # 隐藏响应头中的 Nginx 版本 }配置验证与重载每次修改配置后，务必执行：nginx -t # 验证配置语法 nginx -s reload # 重载配置（不中断服务）以上配置可根据实际需求组合使用，例如同时配置 HTTPS、反向代理和限流。如需更复杂的场景（如 A/B 测试、灰度发布），可进一步扩展配置。以下是 Nginx 更高级的扩展配置示例，适用于复杂场景：十、A/B 测试与灰度发布基于 Cookie 的流量分发http { # 定义变量存储分流规则 map $cookie_ab_test $backend { default backend_a; "b" backend_b; } upstream backend_a { server backend1.example.com; } upstream backend_b { server backend2.example.com; } server { listen 80; server_name example.com; # 首次访问设置 Cookie if ($cookie_ab_test = "") { set $ab_test $random % 2; # 随机分为两组 if ($ab_test = 0) { add_header Set-Cookie "ab_test=a; path=/; domain=.example.com"; } if ($ab_test = 1) { add_header Set-Cookie "ab_test=b; path=/; domain=.example.com"; } } location / { proxy_pass http://$backend; } } }十一、基于用户区域的流量路由根据 IP 地理位置分发请求# 需要安装 ngx_http_geoip2_module 模块 http { geoip2 /path/to/GeoLite2-Country.mmdb { $geoip2_data_country_code country iso_code; } map $geoip2_data_country_code $backend { default backend_global; "CN" backend_china; "US" backend_usa; } upstream backend_global { server global.example.com; } upstream backend_china { server china.example.com; } upstream backend_usa { server usa.example.com; } server { listen 80; server_name example.com; location / { proxy_pass http://$backend; } } }十二、内容缓存配置配置代理缓存（如缓存 API 响应）http { # 定义缓存参数 proxy_cache_path /var/cache/nginx levels=1:2 keys_zone=my_cache:10m max_size=10g inactive=60m use_temp_path=off; server { listen 80; server_name api.example.com; location /static-api/ { proxy_pass http://backend_api; proxy_cache my_cache; proxy_cache_key "$scheme$request_method$host$request_uri"; proxy_cache_valid 200 302 12h; # 成功响应缓存12小时 proxy_cache_valid 404 1m; # 404缓存1分钟 # 缓存命中时添加响应头，便于调试 add_header X-Cache $upstream_cache_status; } } }十三、健康检查与自动剔除故障节点配置主动健康检查（需 nginx-plus 或 lua 模块）http { upstream backend_servers { server backend1.example.com max_fails=3 fail_timeout=30s; server backend2.example.com max_fails=3 fail_timeout=30s; # 健康检查（Nginx Plus 功能） zone backend 64k; health_check interval=5s fails=3 passes=2 uri=/health; } server { listen 80; server_name example.com; location / { proxy_pass http://backend_servers; } } }使用 Lua 模块实现健康检查（开源版）# 需要安装 ngx_lua 模块 http { lua_shared_dict healthcheck 1m; init_by_lua_block { local hc = require "resty.healthcheck" local checker = hc.new({ name = "backend_checker", shm_name = "healthcheck", type = "http", http_req = "GET /health HTTP/1.0\r\nHost: backend\r\n\r\n", interval = 2000, # 2秒检查一次 timeout = 1000, # 超时时间1秒 fall = 3, # 连续3次失败标记为不可用 rise = 2, # 连续2次成功标记为可用 }) -- 添加后端服务器 checker:add_target("backend1.example.com", nil, 80, true) checker:add_target("backend2.example.com", nil, 80, true) -- 启动后台检查 local ok, err = checker:start() if not ok then ngx.log(ngx.ERR, "failed to start health checker: ", err) return end } upstream backend_servers { server backend1.example.com; server backend2.example.com; # 使用 Lua 脚本判断服务器是否健康 server 0.0.0.1; # 防止 upstream 为空 balancer_by_lua_block { local hc = require "resty.healthcheck" local checker = hc.new({ name = "backend_checker", shm_name = "healthcheck", type = "http", }) local ok, err = checker:get_reachable() if not ok then ngx.log(ngx.ERR, "no reachable backend server: ", err) return ngx.exit(500) end -- 设置要代理的服务器 local host, port = ok.host, ok.port local ok, err = ngx.balancer.set_current_peer(host, port) if not ok then ngx.log(ngx.ERR, "failed to set current peer: ", err) return ngx.exit(500) end } } }十四、请求限流与熔断基于漏桶算法的限流http { # 定义漏桶限流规则 limit_req_zone $binary_remote_addr zone=api_limit:10m rate=5r/s; # 每秒5个请求 server { listen 80; server_name api.example.com; location /api/ { limit_req zone=api_limit burst=10 nodelay; # 突发10个请求 # 超过限流时返回自定义错误页面 error_page 503 @api_blocked; } location @api_blocked { default_type application/json; return 503 '{"error":"Too many requests","code":429}'; } } }熔断机制（当后端服务不可用时）upstream backend_servers { server backend1.example.com; server backend2.example.com; # 当后端服务器返回500、502、503、504时，将请求转发到其他服务器 proxy_next_upstream error timeout invalid_header http_500 http_502 http_503 http_504; }十五、WebSocket 集群与会话保持基于 IP Hash 的会话保持upstream websocket_backend { ip_hash; # 相同IP的请求始终转发到同一后端服务器 server backend1.example.com:8080; server backend2.example.com:8080; } server { listen 80; server_name ws.example.com; location /ws/ { proxy_pass http://websocket_backend; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "Upgrade"; proxy_set_header Host $host; # 保持长连接 proxy_read_timeout 3600s; } }十六、自定义日志格式与分析JSON 格式日志http { log_format json_log '{ "timestamp": "$time_iso8601", "remote_addr": "$remote_addr", "host": "$host", "request": "$request", "status": $status, "body_bytes_sent": $body_bytes_sent, "request_time": $request_time, "upstream_response_time": "$upstream_response_time", "http_referer": "$http_referer", "http_user_agent": "$http_user_agent" }'; server { listen 80; server_name example.com; access_log /var/log/nginx/access_json.log json_log; } }十七、HTTP/2 与 TLS 优化配置HTTP/2 与 OCSP 装订server { listen 443 ssl http2; server_name example.com; # 证书配置 ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem; # 启用 OCSP 装订 ssl_stapling on; ssl_stapling_verify on; resolver 8.8.8.8 8.8.4.4 valid=300s; resolver_timeout 5s; # TLS 优化 ssl_protocols TLSv1.3 TLSv1.2; ssl_prefer_server_ciphers on; ssl_ciphers "ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384"; # HTTP/2 优化 http2_push_preload on; # 启用 HTTP/2 预推送 }十八、自定义错误页面配置全局错误页面http { # 定义错误页面路径 error_page 400 /errors/400.html; error_page 401 /errors/401.html; error_page 403 /errors/403.html; error_page 404 /errors/404.html; error_page 500 502 503 504 /errors/50x.html; server { listen 80; server_name example.com; location /errors/ { internal; # 只允许内部重定向访问 root /var/www/html; } } }十九、Nginx 与 Lua 脚本集成示例：请求签名验证# 需要安装 ngx_lua 模块 server { listen 80; server_name api.example.com; location /api/ { # 验证请求签名 access_by_lua_block { local secret_key = "your_secret_key" local request_time = ngx.var.arg_timestamp local signature = ngx.var.arg_signature # 检查时间戳是否过期（防止重放攻击） local current_time = os.time() if not request_time or math.abs(current_time - tonumber(request_time)) > 300 then ngx.status = ngx.HTTP_UNAUTHORIZED ngx.say('{"error":"Invalid timestamp"}') ngx.exit(ngx.HTTP_UNAUTHORIZED) end # 验证签名 local args = ngx.req.get_uri_args() local sorted_keys = {} for k, _ in pairs(args) do if k ~= "sign" then table.insert(sorted_keys, k) end end table.sort(sorted_keys) local sign_str = "" for _, k in ipairs(sorted_keys) do sign_str = sign_str .. k .. "=" .. args[k] .. "&" end sign_str = sign_str .. "key=" .. secret_key local resty_sha1 = require "resty.sha1" local str = require "resty.string" local sha1 = resty_sha1:new() sha1:update(sign_str) local digest = sha1:final() local calculated_sign = str.to_hex(digest) if calculated_sign ~= signature then ngx.status = ngx.HTTP_UNAUTHORIZED ngx.say('{"error":"Invalid signature"}') ngx.exit(ngx.HTTP_UNAUTHORIZED) end } proxy_pass http://backend_api; } }二十、基于 JWT 的认证JWT 验证示例# 需要安装 lua-resty-jwt 模块 server { listen 80; server_name auth.example.com; location /api/ { access_by_lua_block { local jwt = require "resty.jwt" local secret = "your_jwt_secret" -- 从请求头中获取 JWT local auth_header = ngx.var.http_Authorization if not auth_header then ngx.status = ngx.HTTP_UNAUTHORIZED ngx.say('{"error":"Authorization header missing"}') ngx.exit(ngx.HTTP_UNAUTHORIZED) end -- 提取 token local _, _, token = string.find(auth_header, "Bearer%s+(.+)") if not token then ngx.status = ngx.HTTP_UNAUTHORIZED ngx.say('{"error":"Invalid token format"}') ngx.exit(ngx.HTTP_UNAUTHORIZED) end -- 验证 JWT local jwt_obj = jwt:verify(secret, token) if not jwt_obj.verified then ngx.status = ngx.HTTP_UNAUTHORIZED ngx.say('{"error":"' .. jwt_obj.reason .. '"}') ngx.exit(ngx.HTTP_UNAUTHORIZED) end -- 将用户信息传递给后端 ngx.req.set_header("X-User-ID", jwt_obj.payload.user_id) ngx.req.set_header("X-User-Role", jwt_obj.payload.role) } proxy_pass http://backend_api; } }配置验证与重载每次修改配置后，务必执行：nginx -t # 验证配置语法 nginx -s reload # 重载配置（不中断服务）以上配置涵盖了复杂场景下的高级应用，可根据实际需求组合使用。对于生产环境，建议先在测试环境验证配置，确保稳定性。