首页
关于小站
朋友
壁纸
留言
时光之书
笔顺字帖
LayUI手册
Search
1
【PHP】PHPoffice/PHPSpreadsheet读取和写入Excel
2,199 阅读
2
【Layui】控制页面元素展示隐藏
2,012 阅读
3
【Git】No tracked branch configured for branch master or the branch doesn't exist.
1,942 阅读
4
【PHP】PHP实现JWT生成和验证
1,901 阅读
5
【composer】composer常用命令
1,718 阅读
默认分类
PHP
ThinkPHP
Laravel
面向对象
设计模式
算法
基础
网络安全
webman
Web
HTML
CSS
JavaScript
jQuery
Layui
VUE
uni-app
Database
MySQL
Redis
RabbitMQ
Nginx
Git
Linux
Soft Ware
Windows
网赚
Go
Docker
Elasticsearch
登录
Search
标签搜索
PHP
函数
方法
类
MySQL
ThinkPHP
JavaScript
OOP
Layui
Web
Server
Nginx
Docker
PHPSpreadsheet
PHPoffice
Array
设计模式
Git
排序算法
基础
小破孩
累计撰写
261
篇文章
累计收到
13
条评论
首页
栏目
默认分类
PHP
ThinkPHP
Laravel
面向对象
设计模式
算法
基础
网络安全
webman
Web
HTML
CSS
JavaScript
jQuery
Layui
VUE
uni-app
Database
MySQL
Redis
RabbitMQ
Nginx
Git
Linux
Soft Ware
Windows
网赚
Go
Docker
Elasticsearch
页面
关于小站
朋友
壁纸
留言
时光之书
笔顺字帖
LayUI手册
搜索到
162
篇与
的结果
2026-02-06
【Nginx】UNIX Socket详解
一、核心定义UNIX Socket(UNIX域套接字/UDS):仅Linux/Unix本地的进程间通信(IPC)方式,以.sock特殊文件为通信标识,进程通过操作系统内核直接交换数据,完全不经过网络协议栈,是本地进程通信的高性能首选。与TCP/IP(127.0.0.1)核心对比(重点)对比维度TCP/IP(本地如127.0.0.1:3306)UNIX Socket(如/var/run/mysqld/mysqld.sock)通信范围本地/跨服务器(网络)仅同一Linux/Unix服务器通信标识IP+端口本地.sock文件(无实际数据,仅作通信入口)数据传输路径进程→网络协议栈→内核→网络协议栈→进程进程→内核→进程(仅内核拷贝,无网络开销)性能有封装/解包开销,延迟一般延迟极低、吞吐量高(本地首选)配置/安全需IP/端口,需开放防火墙仅需.sock路径,无端口/防火墙风险资源占用占用端口/网络连接资源仅占用磁盘文件,资源消耗可忽略通俗类比:TCP/IP是「同城快递(走网点流程)」,UNIX Socket是「邻居串门(直接沟通)」。二、核心原理(PHP开发只需掌握3点).sock文件是通信标识,非普通文件,不存传输数据,删除会直接中断通信;数据全程在内核空间拷贝,跳过网卡、TCP握手、IP封装等网络环节(性能高的根本原因);遵循C/S客户端-服务端模型,和TCP使用逻辑一致:服务端绑定.sock文件监听,客户端连接.sock文件通信,通信后文件保留(服务端停止则销毁)。三、PHP开发核心使用场景(生产环境标配)场景1:PHP连接本地MySQL/MariaDB(替代127.0.0.1:3306)步骤1:查询MySQL的.sock文件路径(3种方法)查配置文件:/etc/my.cnf//etc/mysql/my.cnf 中的socket配置项;MySQL命令行:mysql -uroot -p -e "show variables like 'socket';";系统查找:find / -name "*.sock" | grep mysql。步骤2:PHP代码写法(PDO/Mysqli/框架对比)核心变化:去掉host/port,新增unix_socket指定.sock路径。// 1. PDO(TCP vs Socket) $pdo_tcp = new PDO('mysql:host=127.0.0.1;port=3306;dbname=test;charset=utf8mb4', 'root', '123456'); $pdo_sock = new PDO('mysql:unix_socket=/var/run/mysqld/mysqld.sock;dbname=test;charset=utf8mb4', 'root', '123456'); // 2. Mysqli(TCP vs Socket) $mysqli_tcp = new mysqli('127.0.0.1', 'root', '123456', 'test', 3306); $mysqli_sock = new mysqli(null, 'root', '123456', 'test', 0, '/var/run/mysqld/mysqld.sock'); // 3. Laravel框架(.env配置,其他框架同理) # TCP配置 DB_HOST=127.0.0.1 DB_PORT=3306 # Socket配置(注释上面,新增下面) DB_UNIX_SOCKET=/var/run/mysqld/mysqld.sock场景2:Nginx通过UNIX Socket连接PHP-FPM(替代127.0.0.1:9000)步骤1:配置PHP-FPM的.sock(/etc/php-fpm.d/www.conf); 注释TCP监听 ; listen = 127.0.0.1:9000 ; 开启Socket监听,指定路径 listen = /var/run/php-fpm/www.sock ; 关键:设置权限(Nginx和PHP-FPM统一用户,如www-data) listen.owner = www-data listen.group = www-data listen.mode = 0660步骤2:配置Nginx的Socket转发(站点配置)location ~ \.php$ { # 核心:替换为PHP-FPM的.sock路径 fastcgi_pass unix:/var/run/php-fpm/www.sock; fastcgi_index index.php; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; include fastcgi_params; }步骤3:重启生效systemctl restart php-fpm && systemctl restart nginx四、核心优势(PHP生产环境必用的原因)性能提升:本地通信延迟降低20%-50%,高并发下吞吐量优势更明显;配置简单:无需配置IP/端口,避免端口占用、防火墙开放等问题;更安全:不涉及网络传输,无端口扫描、远程恶意连接风险;开发零成本:仅需替换配置/参数,使用逻辑和TCP完全一致,无需额外学习;资源高效:不占用端口/网络资源,高并发下减少系统开销。五、避坑重点(3个高频问题,必记)1. 系统限制:仅支持Linux/Unix,Windows不支持开发环境(Windows/XAMPP/WAMP):只能用TCP/IP(127.0.0.1);生产环境(Linux服务器):放心用UNIX Socket(所有服务均支持);特殊:WSL2(基于Linux内核)支持,WSL1不支持。2. 权限问题:客户端必须有.sock文件的读/写权限(最常踩坑)现象:连接时报「权限不足/连接拒绝」(如Nginx连PHP-FPM、PHP连MySQL)。解决方法(推荐生产环境方案1):统一用户/用户组:让客户端和服务端进程归属同一用户(如Nginx/PHP-FPM均为www-data,将PHP进程加入mysql用户组):usermod -aG mysql www-data # 加组 systemctl restart php-fpm mysql # 重启服务临时修改.sock权限(服务端重启后失效):chmod 660 /var/run/mysqld/mysqld.sock # 读写权限 chown mysql:www-data /var/run/mysqld/mysqld.sock # 所属用户/组3. 存储问题:.sock文件需放在本地高速目录,且磁盘不能满推荐路径:/var/run/(内存文件系统,读写快,不占磁盘空间,也是MySQL/PHP-FPM默认路径);禁止:将.sock放在网络磁盘(如NFS),内核不支持跨网络的Socket通信;注意:.sock所在磁盘分区满了,会导致进程无法创建/写入文件,直接中断通信。六、拓展场景(PHP开发可了解)除MySQL/PHP-FPM外,本地常用服务均支持UNIX Socket,配置逻辑一致:Redis:配置unixsocket /var/run/redis/redis.sock,PHP用predis/phpredis连接时指定sock路径;Memcached:本地通信可配置Socket,替代127.0.0.1:11211;本地微服务:同一服务器内的微服务进程,用Socket替代TCP提升通信性能。七、核心总结(PHP开发必记5句话)UNIX Socket是Linux本地进程通信方式,核心优势是无网络开销、高性能;本地通信优先用Socket,跨服务器通信必须用TCP/IP;PHP使用只需替换参数:去掉host/port,指定.sock文件路径;生产环境核心配置:PHP连本地MySQL、Nginx连PHP-FPM,全量替换为UNIX Socket;避坑核心:Linux专属、权限正确、存/var/run/目录。八、生产环境实操清单(一键落地)检查所有本地服务(MySQL/PHP-FPM/Redis)的.sock路径并记录;修改PHP代码/框架配置,将本地TCP连接替换为Socket连接;配置Nginx+PHP-FPM的Socket通信,统一进程用户;给所有.sock文件配置正确权限,将相关进程加入对应用户组;重启所有服务,测试连接是否正常。
2026年02月06日
3 阅读
0 评论
0 点赞
2026-02-06
【Nginx】Nginx + php-fpm 运行原理详解
深入理解 Nginx + PHP-FPM 的协同运行原理,搞清楚这两个组件各自的作用、数据如何流转,以及核心的通信和进程管理逻辑,本篇文章从组件分工、核心运行流程、通信方式、进程管理四个核心维度,用通俗易懂的方式详解,兼顾原理和实际运行细节。一、先明确核心组件的分工(为什么需要Nginx+PHP-FPM)PHP 是解释型脚本语言,本身无法直接处理 HTTP 请求、监听端口,也不擅长做静态资源解析、高并发网络通信;而 Nginx 是高性能的 HTTP 服务器/反向代理,擅长处理静态请求、高并发连接、TCP/IP 网络通信,但完全不能解析PHP脚本。PHP-FPM 是 PHP 官方推出的FastCGI 进程管理器(FastCGI Process Manager),是 FastCGI 协议的PHP实现,核心作用是管理PHP解析进程、接收并处理来自Nginx的PHP脚本解析请求、返回解析结果。简单总结分工:Nginx:前端「门卫+快递员」,负责接收所有客户端HTTP请求,过滤静态请求(html/css/js/图片等)自己直接处理,动态PHP请求转发给PHP-FPM,最后把PHP-FPM的解析结果返回给客户端。PHP-FPM:后端「脚本解析工头」,管理一群PHP解析进程,接收Nginx的转发请求,分配给子进程解析PHP脚本,生成HTML等动态内容后回传给Nginx。PHP解析进程:PHP-FPM的「工人」,实际执行PHP代码、完成脚本解析的最小单元。补充:FastCGI 是一种通信协议(属于应用层协议),作用是让「Web服务器(Nginx)」和「脚本解析器(PHP)」这两个独立进程能标准化通信,替代了早期效率极低的CGI协议(每次请求都创建新进程,高并发下性能暴跌)。二、Nginx+PHP-FPM 核心运行流程(一次PHP请求的完整流转)以用户访问 http://域名/index.php?name=test 为例,从客户端发起请求到最终收到响应的完整步骤,每一步都标注核心动作,流程清晰无冗余:客户端(浏览器/Postman) → 网络层(TCP/IP) → Nginx → FastCGI协议 → PHP-FPM → PHP解析进程 → 回传结果 → Nginx → 客户端逐步骤拆解(关键步骤标粗):客户端发起HTTP请求:用户输入URL后,客户端与Nginx建立TCP连接(默认80/443端口),发送HTTP请求(包含请求方法、URL、请求头、参数等)。Nginx接收并解析请求:Nginx监听80/443端口,接收到请求后,通过nginx.conf配置的location规则匹配请求路径:如果是静态资源(如/css/index.css),Nginx直接从本地磁盘读取文件,通过TCP连接返回给客户端,全程不涉及PHP-FPM;如果是PHP动态请求(如/index.php),Nginx触发FastCGI转发规则,准备将请求信息封装成FastCGI协议格式。Nginx封装请求并转发给PHP-FPM:Nginx根据配置的PHP-FPM地址(IP+端口/UNIX套接字),将HTTP请求的核心信息(脚本路径、请求参数、请求头、环境变量如REQUEST_METHOD/REQUEST_URI)按照FastCGI协议规范封装,发送给PHP-FPM。PHP-FPM接收请求并分配进程:PHP-FPM监听自己的端口(默认9000)或UNIX套接字,接收到FastCGI请求后,由主进程(master process) 分配给空闲的PHP子进程(worker process)(如果没有空闲子进程,会根据进程管理规则创建新进程或让请求排队)。PHP子进程解析执行脚本:空闲的PHP子进程接收请求后,执行以下操作:读取指定路径的PHP脚本文件(如/var/www/html/index.php);解释执行PHP代码,处理业务逻辑(如连接数据库、处理参数、生成动态HTML);执行完成后,将执行结果(动态HTML内容、响应状态码、响应头)按照FastCGI协议封装。PHP-FPM回传结果给Nginx:PHP子进程将封装好的解析结果返回给PHP-FPM主进程,再由主进程回传给Nginx。Nginx接收结果并返回给客户端:Nginx接收到PHP-FPM的回传数据后,将FastCGI格式的结果转换回HTTP协议格式(补充HTTP响应头、状态码),通过最初建立的TCP连接,将最终的响应内容(动态HTML)发送给客户端。释放连接/资源:客户端接收响应后,关闭TCP连接(或根据HTTP/1.1的Keep-Alive保持连接);Nginx和PHP-FPM的相关进程回到空闲状态,等待下一次请求。核心关键点:整个流程中,Nginx只做「转发和返回」,不碰PHP代码;PHP-FPM只做「接收和分配」,不碰HTTP协议;真正执行PHP代码的是PHP子进程,三者解耦,各司其职,这也是该架构高性能的核心原因。三、Nginx与PHP-FPM的两种核心通信方式Nginx和PHP-FPM之间的通信是整个架构的关键环节,配置在Nginx的fastcgi_pass指令中,主要有两种方式,各有优劣,适用于不同场景,也是实际部署中最常配置的部分:方式1:UNIX域套接字(UNIX Socket)配置格式:fastcgi_pass unix:/var/run/php-fpm/www.sock;(sock文件路径由PHP-FPM配置)通信原理:基于本地文件的进程间通信(IPC),不经过网络协议栈(无需IP/端口),直接通过内核交换数据,延迟更低、性能更高。适用场景:Nginx和PHP-FPM部署在同一台服务器(绝大多数生产场景)。注意事项:需要保证sock文件的权限正确(Nginx进程用户如www-data需要有读/写权限),否则会出现「连接拒绝」错误;sock文件存储在磁盘临时目录,需避免磁盘IO瓶颈。方式2:TCP/IP套接字(网络端口)配置格式:fastcgi_pass 127.0.0.1:9000;(如果PHP-FPM在另一台服务器,填远程IP如192.168.1.100:9000)通信原理:基于TCP/IP协议的网络通信,即使是本地127.0.0.1,也会经过内核的网络协议栈封装/解包,延迟略高,但兼容性更好。适用场景:Nginx和PHP-FPM部署在不同服务器(分布式部署场景),或需要跨服务器转发PHP请求。注意事项:PHP-FPM需要配置监听远程IP(默认只监听127.0.0.1,需改为0.0.0.0:9000),并开放服务器9000端口(防火墙/安全组);存在网络传输的安全风险,建议搭配内网访问或加密。性能对比:同一服务器下,UNIX Socket > TCP/IP(127.0.0.1),生产环境优先用UNIX Socket。四、PHP-FPM的核心:进程管理机制(高性能的关键)PHP-FPM的性能核心在于进程管理,由主进程(master)统一管理子进程(worker),避免了CGI「每次请求创建进程」的低效问题,支持三种进程管理模式,配置在PHP-FPM的配置文件(如/etc/php-fpm.d/www.conf)的pm指令中,不同模式适配不同的并发量场景。先明确两个基础配置(所有模式都需要设置):pm.max_children:PHP-FPM允许创建的最大子进程数(核心配置,决定最大并发处理能力,需根据服务器CPU/内存配置,如4核8G服务器一般设20-40);pm.start_servers:PHP-FPM启动时默认创建的空闲子进程数(无需等待请求再创建,提升首次请求响应速度)。模式1:静态模式(pm = static)核心逻辑:PHP-FPM启动后,直接创建pm.max_children个固定数量的子进程,运行期间子进程数不增不减,主进程只负责将请求分配给空闲子进程。优点:进程数固定,无进程创建/销毁的开销,性能最稳定,适合高并发、请求量稳定的场景(如电商、直播平台)。缺点:即使没有请求,所有子进程也会占用内存,内存利用率较低(如果服务器空闲,大量子进程占内存但无工作)。关键配置:仅需设置pm.max_children和pm.start_servers(start_servers ≤ max_children)。模式2:动态模式(pm = dynamic)核心逻辑:子进程数根据请求量动态调整,介于pm.min_spare_servers(最小空闲子进程数)和pm.max_children之间,主进程会根据空闲子进程数自动创建/销毁子进程:当空闲子进程数 < 最小空闲数:创建新的子进程;当空闲子进程数 > 最大空闲数(pm.max_spare_servers):销毁多余的空闲子进程。优点:内存利用率高,空闲时销毁多余进程,节省内存,适合请求量波动大的场景(如企业官网、小型应用)。缺点:请求量突增时,主进程需要创建新进程,存在短暂的进程创建开销,极端情况下可能影响响应速度。关键配置:pm.min_spare_servers(最小空闲)、pm.max_spare_servers(最大空闲)、pm.max_children(最大进程)。模式3:按需模式(pm = ondemand)核心逻辑:最极致的动态模式,PHP-FPM启动后不创建任何子进程,只有当有请求进来时,主进程才会创建子进程处理请求;请求处理完成后,子进程会在指定时间(pm.process_idle_timeout)内空闲,超时后自动销毁。优点:内存利用率极致高,空闲时几乎不占内存,适合请求量极低、偶尔有访问的场景(如个人博客、测试环境)。缺点:首次请求响应慢(需要创建子进程),请求量突增时进程创建开销大,不适合高并发场景。关键配置:pm.max_children(最大进程)、pm.process_idle_timeout(子进程空闲超时时间,默认10s)。进程管理核心原则:pm.max_children的设置是关键,不能过大也不能过小:过小:请求数超过最大进程数,后续请求会排队,导致页面加载慢、超时;过大:每个PHP子进程占用约20-50M内存,过多进程会导致服务器内存耗尽,触发OOM(内存溢出),进程被内核杀死,服务崩溃。五、补充:Nginx的FastCGI核心配置(必配项)Nginx转发PHP请求时,需要在nginx.conf或站点配置中添加FastCGI核心配置,否则PHP-FPM无法正确解析请求,核心配置如下(实际部署可直接复用):server { listen 80; server_name 你的域名; root /var/www/html; # PHP项目根目录 index index.php index.html; # 匹配PHP请求,转发给PHP-FPM location ~ \.php$ { fastcgi_pass unix:/var/run/php-fpm/www.sock; # 通信方式(UNIX Socket),替换为127.0.0.1:9000则为TCP fastcgi_index index.php; # 核心:传递PHP脚本真实路径给PHP-FPM(必须配,否则PHP-FPM找不到脚本) fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; # 加载FastCGI默认配置(包含请求头、环境变量等) include fastcgi_params; } # 禁止直接访问PHP源码目录 location ~ /\.ht { deny all; } }核心必配项:fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;,作用是将Nginx解析到的PHP脚本绝对路径传递给PHP-FPM,否则PHP-FPM会因找不到脚本返回404或502错误。六、常见异常:502 Bad Gateway(最常遇到)Nginx+PHP-FPM架构中,502错误是最常见的异常,本质是Nginx无法连接到PHP-FPM,核心原因主要有4种,排查顺序从易到难:PHP-FPM服务未启动/已崩溃:执行systemctl status php-fpm查看状态,重启systemctl restart php-fpm;通信方式配置错误:Nginx的fastcgi_pass与PHP-FPM的监听地址(listen指令)不匹配,或端口/套接字被占用;PHP-FPM进程数耗尽:pm.max_children设置过小,请求量超过最大进程数,需调大该值并重启PHP-FPM;权限问题:UNIX Socket文件的权限不足,Nginx进程用户无法读写,执行chmod 660 /var/run/php-fpm/www.sock并chown www-data:www-data /var/run/php-fpm/www.sock(匹配Nginx和PHP-FPM的运行用户)。总结(核心关键点回顾)分工解耦:Nginx处理HTTP和静态资源,PHP-FPM管理PHP进程,PHP子进程执行脚本,三者通过FastCGI协议通信,是高性能的核心;运行流程:客户端→Nginx(解析/转发)→PHP-FPM(分配进程)→PHP子进程(执行脚本)→回传Nginx→客户端,全程两次协议转换(HTTP→FastCGI→HTTP);通信方式:同一服务器优先用UNIX Socket(高性能),跨服务器用TCP/IP(兼容性);进程管理:PHP-FPM主进程管理子进程,支持静态(高并发稳定)、动态(内存高效)、按需(极低请求)三种模式,pm.max_children是核心配置;核心配置:Nginx必须配SCRIPT_FILENAME传递脚本绝对路径,否则会出现502/404错误。整个架构的设计思路是「专业的事交给专业的组件」,Nginx发挥高并发网络通信的优势,PHP-FPM解决PHP脚本的进程管理和解析效率问题,两者结合成为目前PHP Web开发的工业级标准架构,支撑绝大多数PHP生产项目(如WordPress、Laravel、ThinkPHP项目)。
2026年02月06日
4 阅读
0 评论
0 点赞
2026-02-06
【PHP】 jieba-php 中文分词快速使用手册
jieba-php 中文分词快速使用手册适用版本:PHP7.1+ / PHP8.4 核心定位:轻量无扩展、开箱即用的PHP中文分词组件,支持简繁/多语言、关键词提取、自定义词典 项目地址:https://github.com/fukuball/jieba-php目录快速安装(推荐方式)组件初始化(必做步骤)核心功能快速示例(可直接复制)高频实用技巧开发注意事项(避坑重点)1. 快速安装仅保留Composer自动安装(推荐,自动管理依赖,无需手动引入文件),执行终端命令:composer require fukuball/jieba-php2. 组件初始化所有功能执行前必须先初始化,提供2种初始化模式,按需选择,代码可直接复制到PHP脚本中。基础初始化(默认大字典,支持简繁/全功能)<?php // 引入自动加载(Composer安装后必加) require_once __DIR__ . '/vendor/autoload.php'; // 引入核心类 use Fukuball\Jieba\Jieba; use Fukuball\Jieba\Finalseg; // 初始化(处理大文本可调整内存限制,非必需) ini_set('memory_limit', '512M'); Jieba::init(); Finalseg::init();轻量初始化(小字典,加载更快,适配轻量场景)// 替换基础初始化的Jieba::init()即可 Jieba::init(['mode'=>'test', 'dict'=>'small']); Finalseg::init();3. 核心功能快速示例所有示例均基于已完成初始化,代码极简可直接运行,附带核心输出结果。3.1 基础分词(三种核心模式)jieba-php的核心能力,覆盖90%基础分词场景// 1. 精确模式(默认,无歧义切分,适合文本分析)★★★最常用 $res1 = Jieba::cut("我来到北京清华大学"); var_dump($res1); // 输出:["我","来到","北京","清华大学"] // 2. 全模式(扫描所有可成词,存在冗余,适合全词汇提取) $res2 = Jieba::cut("我来到北京清华大学", true); var_dump($res2); // 输出:["我","来","来到","北京","清华","清华大学","华大","大学"] // 3. 搜索引擎模式(长词二次切分,提升召回率,适合搜索场景) $res3 = Jieba::cutForSearch("小明硕士毕业于中国科学院计算所"); var_dump($res3); // 输出:["小明","硕士","毕业","中国","科学","学院","科学院","中国科学院","计算","计算所"]3.2 加载自定义词典(补充专业术语/新词)解决专业词汇、专属名词被错误切分的问题,词典格式必须严格遵循。步骤1:创建自定义词典文件(如user_dict.txt)格式:词语 词频 词性(空格分隔,词频/词性可选,建议填写)创新办 5 n 云计算 10 n 李小福 3 nr步骤2:代码加载并使用// 加载自定义词典(初始化后执行) Jieba::loadUserDict(__DIR__ . '/user_dict.txt'); // 分词验证 $res = Jieba::cut("李小福是创新办主任,也是云计算专家"); var_dump($res); // 输出:["李小福","是","创新办","主任","也","是","云计算","专家"]3.3 TF-IDF关键词提取(文本核心词提取)适用于文章摘要、热词分析,需额外引入分析类,支持指定提取数量。// 引入关键词提取类 use Fukuball\Jieba\JiebaAnalyse; // 初始化分析组件(初始化核心类后执行) JiebaAnalyse::init(); // 提取前5个核心关键词(第二个参数为提取数量) $content = "自然语言处理是人工智能的重要分支,中文分词是自然语言处理的基础步骤,jieba-php是优秀的PHP分词工具"; $tags = JiebaAnalyse::extractTags($content, 5); var_dump($tags); // 输出:["自然语言处理","中文分词","人工智能","jieba","php"]3.4 繁体中文分词无需额外配置,仅需基础初始化(默认大字典),直接对繁体文本分词,兼容简繁混合场景。// 繁体分词 $res1 = Jieba::cut("憐香惜玉也得要看對象啊!"); var_dump($res1); // 输出:["憐香惜玉","也","得","要","看","對象","啊"] // 简繁混合 $res2 = Jieba::cut("我喜歡吃芒果,憐香惜玉是美德"); var_dump($res2); // 输出:["我","喜歡","吃","芒果",",","憐香惜玉","是","美德"]4. 高频实用技巧4.1 清除缓存释放内存(处理大量文本/多文件必用)避免多次分词后内存溢出,在文本处理间隙执行:Jieba::clearCache(); // 清除核心缓存 // 或彻底清除所有缓存 // Fukuball\Jieba\JiebaMemory::clearAllCaches();4.2 关键词提取过滤停用词(提升准确性)创建停用词文件(如stop_words.txt,每行一个停用词:的、是、也、啊),加载后过滤无意义词汇:// 加载停用词(提取关键词前执行) JiebaAnalyse::setStopWords(__DIR__ . '/stop_words.txt'); // 再执行关键词提取,会自动过滤停用词 $tags = JiebaAnalyse::extractTags($content, 5);4.3 获取词汇在原文的位置(文本定位分析)返回词汇+起止下标,适用于文本高亮、定位场景:$res = Jieba::tokenize("永和服装饰品有限公司"); var_dump($res); // 输出示例:[["word"=>"永和","start"=>0,"end"=>2],["word"=>"服装","start"=>2,"end"=>4],...]5. 开发注意事项(避坑重点)内存配置:处理超大文本(如万字以上),需在初始化前添加ini_set('memory_limit', '1024M'),避免内存溢出;字典选择:轻量场景(如小程序/简单分词)用small字典,繁体/专业场景用默认大字典,不建议混用;Web环境优化:PHP-FPM/Web项目中,建议将初始化放在全局入口(如index.php),避免每次请求重复初始化,提升性能;无扩展依赖:纯PHP实现,无需安装任何PHP扩展(如mbstring需开启,PHP默认已开启),直接运行。
2026年02月06日
4 阅读
0 评论
0 点赞
2025-12-16
【PHP】一个流行的 PHP 日期时间处理库 nesbot/carbon
官方文档:https://carbon.nesbot.com/docs/安装方法1. 基本安装# 在项目根目录执行 composer require nesbot/carbon2. 指定版本安装# 安装特定版本 composer require nesbot/carbon:^2.0 # 安装开发版本 composer require nesbot/carbon --dev基本使用方法1. 引入 Carbonuse Carbon\Carbon; // 获取当前时间 $now = Carbon::now(); echo $now; // 2023-10-27 14:30:00 // 创建指定时间 $date = Carbon::create(2023, 12, 25, 10, 30, 0);2. 常用方法示例use Carbon\Carbon; // 格式化输出 echo Carbon::now()->format('Y-m-d H:i:s'); echo Carbon::now()->toDateString(); // 2023-10-27 echo Carbon::now()->toDateTimeString(); // 2023-10-27 14:30:00 // 日期操作 $tomorrow = Carbon::now()->addDay(); $yesterday = Carbon::now()->subDay(); $nextWeek = Carbon::now()->addWeek(); // 日期比较 $date1 = Carbon::create(2023, 10, 27); $date2 = Carbon::create(2023, 10, 28); if ($date1->lt($date2)) { // less than echo "date1 在 date2 之前"; } // 时区设置 $date = Carbon::now('Asia/Shanghai'); $date->setTimezone('America/New_York');3. 本地化(中文支持)# 安装语言包 composer require carbonphp/carbon-doctrine-typesuse Carbon\Carbon; // 设置中文 Carbon::setLocale('zh'); echo Carbon::now()->diffForHumans(); // 刚刚 echo Carbon::now()->subMinutes(5)->diffForHumans(); // 5分钟前配置文件(可选)在 Laravel 项目中,可以在 config/app.php 中配置时区:'timezone' => 'Asia/Shanghai', 'locale' => 'zh-CN',验证安装创建测试文件验证安装:<?php require 'vendor/autoload.php'; use Carbon\Carbon; echo "当前时间:" . Carbon::now() . "\n"; echo "明天:" . Carbon::now()->addDay() . "\n"; echo "一周前:" . Carbon::now()->subWeek()->diffForHumans() . "\n";常见问题解决版本兼容性问题# 查看当前PHP版本支持的Carbon版本 php -v # 如果PHP版本较旧,使用Carbon 1.x composer require nesbot/carbon:^1.39内存问题# 清理composer缓存 composer clear-cache代理设置# 如果下载慢,使用国内镜像 composer config -g repo.packagist composer https://mirrors.aliyun.com/composer/在框架中的使用LaravelCarbon 已集成在 Laravel 中,可直接使用:// 在Laravel中直接使用 $date = now(); // Carbon实例 echo $date->format('Y-m-d');其他框架// Slim, Symfony等框架 require __DIR__ . '/vendor/autoload.php'; use Carbon\Carbon; $carbon = new Carbon();这样就成功安装并可以使用 Carbon 来处理日期时间了。
2025年12月16日
6 阅读
0 评论
0 点赞
2025-12-11
PHP8.0 + 版本按数据处理类型分类的新增函数整理表格,包含函数名、对应版本、功能说明及可直接运行的用法示例
分类函数名PHP版本功能说明用法示例字符串处理str_contains()8.0检查字符串是否包含指定子串(区分大小写),返回布尔值$str = "Hello PHP8";<br>var_dump(str_contains($str, "PHP8")); // bool(true)var_dump(str_contains($str, "php8")); // bool(false)字符串处理str_starts_with()8.0检查字符串是否以指定子串开头(区分大小写),返回布尔值$str = "2025-12-11";<br>var_dump(str_starts_with($str, "2025")); // bool(true)var_dump(str_starts_with($str, "2024")); // bool(false)字符串处理str_ends_with()8.0检查字符串是否以指定子串结尾(区分大小写),返回布尔值$str = "test.txt";<br>var_dump(str_ends_with($str, ".txt")); // bool(true)var_dump(str_ends_with($str, ".php")); // bool(false)字符串处理mb_str_split()8.0多字节安全的字符串分割,支持指定分割长度(解决str_split多字节乱码)$str = "你好PHP8";<br>print_r(mb_str_split($str)); // [0]=>你 [1]=>好 [2]=>P [3]=>H [4]=>P [5]=>8print_r(mb_str_split($str, 2)); // [0]=>你好 [1]=>PH [2]=>P8字符串处理strlcpy()8.1安全的字符串拷贝(限制长度,含终止符),返回源字符串总长度$dest = ''; $src = "Hello World";$len = strlcpy($dest, $src, 6);<br>echo $dest; // Helloecho $len; // 11字符串处理mb_str_pad()8.3多字节安全的字符串填充(解决str_pad多字节长度计算错误)$str = "你好";<br>echo mb_str_pad($str, 5, "-", STR_PAD_RIGHT); // 你好---数组处理array_is_list()8.1检查数组是否为“列表”(索引从0开始、连续递增的整数索引),返回布尔值$list = [1,2,3]; $notList = [0=>1,2=>3];var_dump(array_is_list($list)); // bool(true)<br>var_dump(array_is_list($notList)); // bool(false)数组处理array_find()8.3返回数组中第一个满足回调条件的元素,无则返回null$nums = [1,2,3,4,5];<br>$even = array_find($nums, fn($n) => $n % 2 === 0);<br>echo $even; // 2数组处理array_find_key()8.3返回数组中第一个满足回调条件的键,无则返回null$users = ['a'=>18, 'b'=>22, 'c'=>19];<br>$key = array_find_key($users, fn($age) => $age > 20);<br>echo $key; // b数组处理array_any()8.3检查数组是否存在至少一个满足回调条件的元素,返回布尔值$nums = [1,3,5];<br>var_dump(array_any($nums, fn($n) => $n%2===0)); // bool(false)$nums[]=8; var_dump(array_any($nums, fn($n) => $n%2===0)); // bool(true)数组处理array_all()8.3检查数组所有元素是否都满足回调条件,返回布尔值$nums = [2,4,6];<br>var_dump(array_all($nums, fn($n) => $n%2===0)); // bool(true)$nums[]=9; var_dump(array_all($nums, fn($n) => $n%2===0)); // bool(false)数值处理fdiv()8.0浮点数除法(优雅处理除0场景,返回INF/-INF/NaN,替代普通除法)var_dump(fdiv(10,2)); // float(5)var_dump(fdiv(10,0)); // float(INF)var_dump(fdiv(0,0)); // float(NAN)数值处理enum_exists()8.1检查枚举类型是否已定义(适配8.1新增的枚举特性)enum Status { case Success; case Error; }var_dump(enum_exists('Status')); // bool(true)var_dump(enum_exists('NonExist')); // bool(false)日期时间处理DateTimeInterface::format_iso8601()8.2标准化输出ISO 8601格式日期,支持指定毫秒精度$dt = new DateTime('2025-12-11 10:00:00', new DateTimeZone('Asia/Shanghai'));<br>echo $dt->format_iso8601(); // 2025-12-11T10:00:00+08:00echo $dt->format_iso8601(3); // 2025-12-11T10:00:00.000+08:00对象/类型处理get_debug_type()8.0更精准的变量类型获取(比gettype友好,支持枚举/对象等)$obj = new stdClass(); enum E { case A; }<br>echo get_debug_type("test"); // string<br>echo get_debug_type($obj); // stdClassecho get_debug_type(E::A); // E对象/类型处理ReflectionClass::isReadOnly()8.2反射检查类是否为只读类(适配8.2新增的readonly类特性)readonly class User { public string $name; }<br>$ref = new ReflectionClass(User::class);var_dump($ref->isReadOnly()); // bool(true)JSON处理json_validate()8.3高效验证JSON字符串有效性(无需解码,性能优于json_decode)$valid = '{"name":"PHP8"}'; $invalid = '{"name":"PHP8"';var_dump(json_validate($valid)); // bool(true)<br>var_dump(json_validate($invalid)); // bool(false)正则表达式preg_last_error_msg()8.0返回正则操作的可读错误信息(替代preg_last_error的数值码)preg_match('/[a-z/', 'test'); // 语法错误echo preg_last_error_msg(); // "missing terminating ] for character class"字节处理bytes_format()8.3将字节数格式化为易读单位(B/KB/MB/GB等),支持指定小数位数echo bytes_format(1024); // 1.00 KBecho bytes_format(1048576); // 1.00 MBecho bytes_format(1500, 0); // 2 KB关键说明:版本兼容性:表格中函数均为PHP8.0及以上新增/增强,部分函数(如array_find)仅在PHP8.3+可用,使用前需确认运行环境版本;多字节支持:mb_*开头的函数解决了中文等多字节字符的处理问题,推荐优先使用;性能优化:如json_validate比json_decode验证JSON更高效(无需解析整个字符串);类型安全:get_debug_type、array_is_list等函数提升了PHP的类型判断精准度,适配8.0+的类型强化特性。
2025年12月11日
23 阅读
0 评论
0 点赞
2025-09-05
【PHP】境外微信支付生成平台证书 - wechatpay
使用方法 $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; } }
2025年09月05日
68 阅读
0 评论
0 点赞
2025-08-18
【PHP】bc系列函数(高精度数学运算)
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可以实现高精度计算,解决普通数值类型精度不足的问题。
2025年08月18日
71 阅读
0 评论
0 点赞
2025-07-30
【开发基础】nginx/postgresql/php/redis/ssdb的启动/关闭/重启命令及配置文件修改
以下是 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)。配置检查:修改配置文件后,建议先检查语法再重启服务。版本兼容性:部分命令可能因软件版本不同而略有差异。
2025年07月30日
130 阅读
0 评论
0 点赞
2025-07-30
【开发基础】macOS系统的常规使用方法
一、触控板使用技巧双指操作双指滑动:实现页面上下滚动双指开合:进行缩放操作双指左右滑动:在浏览器或桌面空间之间进行切换三指操作三指上推:显示所有打开的应用程序三指下滑:隐藏当前应用程序,显示桌面三指左右滑动:在全屏应用程序之间进行切换四指操作四指捏合:打开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 <source> <destination>:复制文件或目录mv <source> <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:退出当前应用程序
2025年07月30日
97 阅读
0 评论
0 点赞
2025-07-26
【PHP】ThinkPHP8数据库迁移示例
以下是一个包含 各种字段类型、约束条件、索引和特殊配置 的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迁移工具中绝大多数字段配置场景,实际开发中可根据需求调整。
2025年07月26日
96 阅读
0 评论
0 点赞
2025-07-26
【PHP】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. 迁移性能问题问题:大型表迁移缓慢解决方案:分批次执行数据迁移使用数据库原生工具导入大型数据避免在迁移中使用复杂查询
2025年07月26日
192 阅读
0 评论
0 点赞
2025-05-17
【ThinkPHP】创建有背景的微信小程序二维码
<?php namespace app\common\lib\wechat; use think\Exception; use think\facade\Cache; use think\facade\Request; use think\Image; use think\facade\Db; class QrCode extends Basic { // 小程序页面路径 private string $path = "pages/myCenter/invite"; // 小程序码保存路径 private string $savePath = "./storage/qrcode/"; // 二维码参数 private string $param = ""; public function __construct(string $path = "", string $field = "", string $param = "", int $width = 430) { if (!empty($path) && empty($field)) { $this->path = $path; } if (!empty($path) && !empty($field) && !empty($param)) { $this->path = "{$path}?{$field}={$param}"; } $this->param = $param; } /** * 生成小程序码并可选择合并背景图 * * @param string $backgroundPath 背景图路径 * @param int $qrWidth 二维码在背景图上的宽度位置 * @param int $qrHeight 二维码在背景图上的高度位置 * @return string 生成后的二维码URL */ public function setQrcode(string $backgroundPath = '', int $qrWidth = 160, int $qrHeight = 530): string { try { // 获取访问令牌 $accessToken = Cache::get('accesstoken'); if (empty($accessToken)) { $accessToken = (new AccessToken())->getAccesToken(); Cache::set('accesstoken', $accessToken, 7200); // 假设token有效期为2小时 } // 请求小程序码 $url = "https://api.weixin.qq.com/wxa/getwxacode?access_token={$accessToken}"; $data = [ 'path' => $this->path, 'scene' => 'type=qrcode', 'width' => 430, ]; $result = $this->curlPost($url, $data, 'POST'); if ($result === false || isset($result['errcode'])) { throw new Exception("Failed to get QR code: " . json_encode($result)); } // 保存二维码到文件 $fileName = md5($this->param); $directory = $this->savePath . date('Ymd') . '/'; if (!$this->createDirectory($directory)) { throw new Exception("目录创建失败: {$directory}"); } $filePath = "{$directory}{$fileName}.png"; if (!file_put_contents($filePath, $result)) { throw new Exception("文件写入失败: {$filePath}"); } // 生成完整URL $fullDomain = (new \app\common\lib\data\Str())->getFullDomain(); $qrcodeUrl = $fullDomain . ltrim($filePath, '.'); // 合并背景图(如果提供) if (!empty($backgroundPath)) { return $this->mergeWithBackground($backgroundPath, $filePath, $qrcodeUrl, $qrWidth, $qrHeight); } return $qrcodeUrl; } catch (Exception $e) { // 记录错误日志 error_log($e->getMessage()); return ''; } } /** * 将二维码与背景图合并 */ private function mergeWithBackground(string $backgroundPath, string $qrcodePath, string $defaultUrl, int $qrWidth, int $qrHeight): string { try { $fileName = md5(uniqid() . time()); $newImagePath = $this->savePath . date('Ymd') . "/{$fileName}.png"; $background = Image::open($backgroundPath); $background->water($qrcodePath, [$qrWidth, $qrHeight])->save($newImagePath); $imageInfo = [ 'url' => Request::domain() . substr($newImagePath, 1), 'size' => filesize($newImagePath), 'name' => $fileName, 'mime' => mime_content_type($newImagePath), 'ext' => 'png', ]; $fileData = [ 'f_uuid' => setUUID(), 'f_file' => $imageInfo['url'], 'f_location' => 0, 'f_type' => 'image', 'f_info' => serialize($imageInfo), 'f_user_uuid' => $this->request->index_user_uuid ?? '', 'f_create_time' => time(), ]; Db::name('File')->insert($fileData); return $imageInfo['url']; } catch (Exception $e) { // 记录错误日志 error_log($e->getMessage()); return $defaultUrl; } } /** * 递归创建目录 */ private function createDirectory(string $path, int $mode = 0777, bool $recursive = true): bool { if (is_dir($path)) { return true; } if (mkdir($path, $mode, $recursive)) { chmod($path, $mode); return true; } if (!is_dir(dirname($path))) { if ($this->createDirectory(dirname($path), $mode, $recursive)) { return $this->createDirectory($path, $mode, $recursive); } } return false; } /** * 发送HTTP POST请求 */ // public function curlPost($url, $data, $method = "POST") // { // $ch = curl_init(); //1.初始化 // curl_setopt($ch, CURLOPT_URL, $url); //2.请求地址 // curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method);//3.请求方式 // //4.参数如下 // curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, FALSE);//https // curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, FALSE); // curl_setopt($ch, CURLOPT_USERAGENT, 'Mozilla/5.0 (compatible; MSIE 5.01; Windows NT 5.0)');//模拟浏览器 // curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 1); // curl_setopt($ch, CURLOPT_AUTOREFERER, 1); // curl_setopt($ch, CURLOPT_HTTPHEADER, array('Accept-Encoding: gzip, deflate'));//gzip解压内容 // curl_setopt($ch, CURLOPT_ENCODING, 'gzip,deflate'); // // if ($method == "POST") {//5.post方式的时候添加数据 // $data = json_encode($data); // curl_setopt($ch, CURLOPT_POSTFIELDS, $data); // } // curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); // $tmpInfo = curl_exec($ch);//6.执行 // // if (curl_errno($ch)) {//7.如果出错 // return curl_error($ch); // } // curl_close($ch);//8.关闭 // return $tmpInfo; // } }
2025年05月17日
84 阅读
0 评论
0 点赞
2025-05-13
【PHP】PHPExcel 使用手册
PHPExcel 是一个用于操作 Microsoft Excel 格式文件的 PHP 库,它支持读取和写入多种格式(如 .xls, .xlsx, .csv 等)。不过需要注意,PHPExcel 项目已停止维护,推荐使用其继任者 PhpSpreadsheet。以下是 PHPExcel 的基本使用指南:1. 安装 PHPExcel# 使用 Composer 安装(推荐) composer require phpoffice/phpexcel2. 读取 Excel 文件require 'vendor/autoload.php'; use PHPExcel_IOFactory; // 读取文件 $inputFileName = 'example.xlsx'; $objPHPExcel = PHPExcel_IOFactory::load($inputFileName); // 获取第一个工作表 $sheet = $objPHPExcel->getActiveSheet(); // 获取最高行和最高列 $highestRow = $sheet->getHighestRow(); $highestColumn = $sheet->getHighestColumn(); // 遍历数据 for ($row = 1; $row <= $highestRow; $row++) { for ($col = 'A'; $col <= $highestColumn; $col++) { $cellValue = $sheet->getCell($col.$row)->getValue(); echo "Row $row, Column $col: $cellValue\n"; } }3. 创建新的 Excel 文件require 'vendor/autoload.php'; use PHPExcel; use PHPExcel_IOFactory; // 创建新的 PHPExcel 对象 $objPHPExcel = new PHPExcel(); // 设置文档属性 $objPHPExcel->getProperties() ->setCreator("Your Name") ->setTitle("Sample Excel File"); // 获取活动工作表 $sheet = $objPHPExcel->getActiveSheet(); // 设置单元格值 $sheet->setCellValue('A1', 'Hello'); $sheet->setCellValue('B1', 'World!'); $sheet->setCellValue('A2', 'This is a sample spreadsheet.'); // 保存为 .xlsx 文件 $objWriter = PHPExcel_IOFactory::createWriter($objPHPExcel, 'Excel2007'); $objWriter->save('new_file.xlsx');4. 设置样式// 设置字体样式 $sheet->getStyle('A1')->getFont() ->setName('Arial') ->setSize(14) ->setBold(true) ->setColor(new PHPExcel_Style_Color(PHPExcel_Style_Color::COLOR_RED)); // 设置单元格背景色 $sheet->getStyle('A1')->getFill() ->setFillType(PHPExcel_Style_Fill::FILL_SOLID) ->getStartColor()->setARGB('FFEEEEEE'); // 设置对齐方式 $sheet->getStyle('A1')->getAlignment() ->setHorizontal(PHPExcel_Style_Alignment::HORIZONTAL_CENTER) ->setVertical(PHPExcel_Style_Alignment::VERTICAL_CENTER); // 设置列宽 $sheet->getColumnDimension('A')->setWidth(20);5. 合并单元格// 合并 A1 到 B1 的单元格 $sheet->mergeCells('A1:B1'); // 取消合并 $sheet->unmergeCells('A1:B1');6. 处理日期格式// 设置日期值(PHPExcel 使用 Excel 时间戳格式) $dateTime = new DateTime(); $sheet->setCellValue('A1', $dateTime->format('Y-m-d H:i:s')); // 设置单元格格式为日期 $sheet->getStyle('A1')->getNumberFormat() ->setFormatCode('yyyy-mm-dd hh:mm:ss');7. 保存为不同格式// 保存为 .xlsx 格式 $objWriter = PHPExcel_IOFactory::createWriter($objPHPExcel, 'Excel2007'); $objWriter->save('file.xlsx'); // 保存为 .xls 格式 $objWriter = PHPExcel_IOFactory::createWriter($objPHPExcel, 'Excel5'); $objWriter->save('file.xls'); // 保存为 CSV 格式 $objWriter = PHPExcel_IOFactory::createWriter($objPHPExcel, 'CSV'); $objWriter->save('file.csv');8. 从浏览器下载文件// 设置响应头 header('Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'); header('Content-Disposition: attachment;filename="download.xlsx"'); header('Cache-Control: max-age=0'); // 输出到浏览器 $objWriter = PHPExcel_IOFactory::createWriter($objPHPExcel, 'Excel2007'); $objWriter->save('php://output'); exit;注意事项性能问题:处理大量数据时,PHPExcel 可能占用较多内存。考虑使用 PHPExcel_CachedObjectStorageFactory 进行优化。替代方案:推荐使用 PhpSpreadsheet,它是 PHPExcel 的官方继任者,修复了许多问题并提供更好的性能。内存优化:// 设置为只读模式以减少内存消耗 $objReader = PHPExcel_IOFactory::createReader('Excel2007'); $objReader->setReadDataOnly(true); $objPHPExcel = $objReader->load('large_file.xlsx');如需更详细的文档,请参考 PHPExcel 官方文档(注意:项目已停止维护)。
2025年05月13日
83 阅读
0 评论
0 点赞
2025-05-07
【PHP】PHP实现无限级树形分类 树形分类 如果子集的pid不是空,父级id不存在,则子集不展示
/** * @Author:小破孩 * @Email:3584685883@qq.com * @Time:2024/12/17 22:27 * @param $arr * @param $id * @param $pid * @return array * @Description:树形分类 如果子集的pid不是空,父级id不存在,则子集不展示 */ public function getTreeByDateDelChile($arr,$id,$pid) { $items = []; foreach($arr as $v){ $items[$v[$id]] = $v; } $tree = []; foreach($items as $k => $item){ if($item[$pid] &&!isset($items[$item[$pid]])){ unset($items[$k]); continue; } if(isset($items[$item[$pid]])){ $items[$item[$pid]]['child'][] = &$items[$k]; }else{ $tree[] = &$items[$k]; } } return $tree; } /** * @Author:小破孩 * @Email:3584685883@qq.com * @Time:2024/9/14 16:59 * @param array $arr * @param string $children * @return array * @Description:将树形数组转换成一维数组 */ public function setManyTosingle(array $arr = [], string $children = 'child'): array { $result = []; foreach ($arr as $item) { // 先将当前节点添加到结果数组中 $newItem = $item; if (isset($newItem[$children])) { unset($newItem[$children]); } $result[] = $newItem; // 递归处理子节点 if (isset($item[$children]) && is_array($item[$children])) { $result = array_merge($result, $this->setManyTosingle($item[$children], $children)); } } return $result; }
2025年05月07日
71 阅读
0 评论
0 点赞
2025-05-06
【PHP】清空MySQL数据,索引重置
<?php // 定义排除表的常量 const EXCLUDE_TABLES = [ 'web_admin_company', 'web_admin_func', 'web_admin_role', 'web_admin_user', 'web_app_config', 'web_china_city', 'web_china_city_area', 'web_china_city_backup', 'web_company_config', 'web_shop_category', 'web_tppay', // 'web_shop_goods_tmpserver', ]; class SomeClass { public function truncateTables() { try { // 开启事务 Db::startTrans(); // 获取所有表名 $tables = Db::query('SHOW TABLES'); $tableNames = array_map('current', $tables); foreach ($tableNames as $tableName) { if (!in_array($tableName, EXCLUDE_TABLES)) { // 使用参数化查询清空表并重置索引 Db::execute("TRUNCATE TABLE `$tableName`"); echo "表 {$tableName} 已清空<br>"; } } // 提交事务 Db::commit(); } catch (\Exception $e) { // 回滚事务 Db::rollback(); // 记录错误日志 error_log("发生错误: " . $e->getMessage()); echo "发生错误: " . $e->getMessage(); } } }
2025年05月06日
71 阅读
0 评论
0 点赞
2025-04-23
【PHP】管家婆各个大类的基础调用数据方法
<?php namespace app\common\lib\gjp\warehouse; use think\Exception; class Warehouse { protected $dbName; public function __construct() { $this->dbName = "*****"; // } //查询单据 public function getWarehouse($paramkey = "", $paramJson = "", $userId = "***********")// { $instanceGjpBasic = new \app\common\lib\gjp\Basic(); $gjpSignInfo = $instanceGjpBasic->getSignKey(); $instanceGjpApiUrl = new \app\common\lib\gjp\Apiurl($userId); $gjpApiUrlInfo = $instanceGjpApiUrl -> getApiUrl(); $mobile = empty($gjpApiUrlInfo['GraspCloudMobile']) ? 0 : $gjpApiUrlInfo['GraspCloudMobile']; $serviceid = empty($gjpApiUrlInfo['GraspCloudServerId']) ? 0 : $gjpApiUrlInfo['GraspCloudServerId']; $requestUrl = $gjpApiUrlInfo['ApiServerAddress']; $md5BeforeStr = "apiparam".$gjpApiUrlInfo['ApiParam']."apitype".'query'."dbname".(string)$this->dbName."interiorapi"."1"."managename"."GraspCMServerApi.dll"."mobile".$mobile."paramjson".$paramJson."paramkey".$paramkey."serviceid".$serviceid.$gjpSignInfo['SignKey']; $data = [ 'managename' => (string)"GraspCMServerApi.dll", 'dbname' => (string)$this->dbName, 'paramkey' => (string)$paramkey, 'paramjson' => (string)($paramJson), 'apiparam' => (string)$gjpApiUrlInfo['ApiParam'], 'apitype' => (string)"query", 'sign' => (string)md5($md5BeforeStr), 'mobile' => (string)$mobile, 'serviceid' => (string)$serviceid, 'interiorapi' => (integer)1, ]; $encodedParams = http_build_query($data); $result = $instanceGjpBasic->curlRequest($requestUrl,"POST",$encodedParams,true,false); if($result['code'] != 0){ print_r($result);die; } return $result; } //生产单据 public function setReceipts($vchtype = 0,$billdata = '',$userId = "*************"){ $instanceGjpBasic = new \app\common\lib\gjp\Basic(); $gjpSignInfo = $instanceGjpBasic->getSignKey(); $instanceGjpApiUrl = new \app\common\lib\gjp\Apiurl($userId); $gjpApiUrlInfo = $instanceGjpApiUrl -> getApiUrl(); $requestUrl = $gjpApiUrlInfo['ApiServerAddress']; $mobile = empty($gjpApiUrlInfo['GraspCloudMobile']) ? 0 : $gjpApiUrlInfo['GraspCloudMobile']; $serviceid = empty($gjpApiUrlInfo['GraspCloudServerId']) ? 0 : $gjpApiUrlInfo['GraspCloudServerId']; $md5BeforeStr = "apiparam".$gjpApiUrlInfo['ApiParam']."apitype".'process'."billdata". $billdata ."dbname".(string)$this->dbName."interiorapi"."1"."managename"."GraspCMServerApi.dll"."mobile".$mobile."processtype"."0"."serviceid".$serviceid."vchcode"."0"."vchtype".$vchtype.$gjpSignInfo['SignKey']; $data = [ 'managename' => (string)"GraspCMServerApi.dll", 'dbname' => (string)$this->dbName, 'processtype' => (integer)0, 'vchtype' => (integer)$vchtype, 'vchcode' => (integer)0, 'billdata' => (string)$billdata, 'apiparam' => (string)$gjpApiUrlInfo['ApiParam'], 'apitype' => (string)"process", 'sign' => (string)md5($md5BeforeStr), 'mobile' => (string)$mobile, 'serviceid' => (string)$serviceid, 'interiorapi' => (integer)1, ]; // print_r($data);die; $encodedParams = http_build_query($data); $result = $instanceGjpBasic->curlRequest($requestUrl,"POST",$encodedParams,true,false); return $result; } //新增或修改 基础资料新增 public function businessBaseInfo($basetype = "", $baseinfodata = "", $rec = "", $type = 1, $userId = "*********"){ if($type == 1){ $typeName = "add"; }else{ $typeName = "modify"; } if(empty($rec)){ (string)$rec = "0"; } $instanceGjpBasic = new \app\common\lib\gjp\Basic(); $gjpSignInfo = $instanceGjpBasic->getSignKey(); $instanceGjpApiUrl = new \app\common\lib\gjp\Apiurl($userId); $gjpApiUrlInfo = $instanceGjpApiUrl -> getApiUrl(); // print_r($gjpApiUrlInfo);die; $mobile = empty($gjpApiUrlInfo['GraspCloudMobile']) ? 0 : $gjpApiUrlInfo['GraspCloudMobile']; $serviceid = empty($gjpApiUrlInfo['GraspCloudServerId']) ? 0 : $gjpApiUrlInfo['GraspCloudServerId']; $requestUrl = $gjpApiUrlInfo['ApiServerAddress']; $md5BeforeStr ="actiontype".$typeName."apiparam".$gjpApiUrlInfo['ApiParam']."apitype".'baseinfo'."baseinfodata".$baseinfodata."basetype".$basetype."dbname".(string)$this->dbName."interiorapi"."1"."managename"."GraspCMServerApi.dll"."mobile".$mobile."rec".$rec."serviceid".$serviceid.$gjpSignInfo['SignKey']; // "actiontype".$typeName."apiparam".$gjpApiUrlInfo['ApiParam']."apitype"."baseinfo"."basetype".$basetype."baseinfodata".$baseinfodata."dbname".$this->dbName."interiorapi".1."managename"."GraspCMServerApi.dll"."mobile".$mobile."serviceid".$serviceid."sign".md5($md5BeforeStr) $data = [ 'managename' => (string)"GraspCMServerApi.dll", 'dbname' => (string)$this->dbName, 'actiontype' => (string)$typeName, 'rec' => (string)$rec, 'basetype' => (string)$basetype, 'baseinfodata' => (string)$baseinfodata, 'apiparam' => (string)$gjpApiUrlInfo['ApiParam'], 'apitype' => (string)"baseinfo", 'sign' => (string)md5($md5BeforeStr), 'mobile' => (string)$mobile, 'serviceid' => (string)$serviceid, 'interiorapi' => (integer)1, ]; $encodedParams = http_build_query($data); $result = $instanceGjpBasic->curlRequest($requestUrl,"POST",$encodedParams,true,false); if($result['code'] != 0){ print_r($result);die; } return $result; } }
2025年04月23日
78 阅读
0 评论
0 点赞
2025-04-23
【PHP】获取程序Api地址 - 管家婆基础对接 PHP版本
<?php namespace app\common\lib\gjp; class Apiurl { protected $userId; protected $appKey; protected $SercretKey; protected $currentTimestamp; protected $dbName; public function __construct($userId = '********') { $this->appKey = "*****************"; $this->SercretKey = "*******************"; $this->currentTimestamp = time(); $this->userId = $userId; $this->dbName = '*****'; } public function getApiUrl() { $url = "http://api.cmgrasp.com/CMGraspApi/GateWay"; $instanceGjpBasic = new \app\common\lib\gjp\Basic(); $instanceStr = new \app\common\lib\data\Str(); $randamStr = $instanceStr->setNonce(32,true); $md5BeforeStr = "AppKey".$this->appKey."InvalidTime".date('YmdHis',$this->currentTimestamp)."RandamStr".$randamStr."UserId".$this->userId.$this->SercretKey; $data = [ 'MethodName' => (string)"graspcm.cmapi.getcustomerapiurl", 'AppKey' => (string)$this->appKey, 'UserId' => (string)$this->userId, 'InvalidTime' => (string)date('Y-m-d H:i:s'), 'RandamStr' => (string)$randamStr, 'SignStr' => (string)md5($md5BeforeStr), 'DbName' => (string)$this->dbName ]; $jsonData = json_encode($data); $result = $instanceGjpBasic->curlRequest($url,"POST",$jsonData,true,false); if($result['RetCode'] != 0){ $this->getApiUrl(); } $resultData = json_decode($result['RetMsg'],true); return $resultData; } }
2025年04月23日
77 阅读
0 评论
0 点赞
2025-04-23
【PHP】获取接口所需的SignKey - 管家婆基础对接 PHP版本
<?php namespace app\common\lib\gjp; use think\Exception; class Basic { protected $appKey; protected $SercretKey; protected $currentTimestamp; public function __construct() { $this->appKey = "*************"; $this->SercretKey = "*************"; $this->currentTimestamp = time(); } public function getSignKey() { if(!empty( cache('gjp_sign'))) { $dataResult = cache('gjp_sign'); return $dataResult; } $signUrl = "http://api.cmgrasp.com/CMGraspApi/GateWay"; $instanceStr = new \app\common\lib\data\Str(); $randamStr = $instanceStr->setNonce(32,true); $md5BeforeStr = "AppKey".$this->appKey."InvalidTime".date('YmdHis',$this->currentTimestamp)."RandamStr".$randamStr.$this->SercretKey; $data = [ "MethodName" => (string)"graspcm.cmapi.getsignstr", 'AppKey' => (string)$this->appKey, 'InvalidTime' => (string)date('Y-m-d H:i:s',$this->currentTimestamp), 'RandamStr' => (string)$randamStr, 'SignStr' => (string)md5($md5BeforeStr), ]; $jsonData = json_encode($data); $result = $this->curlRequest($signUrl,"POST", $jsonData,true,false); if($result['RetCode'] != 0) { $this->getSignKey(); } $dataResult = json_decode($result['RetMsg'],true); if(empty( cache('gjp_sign'))) { $dataResult = json_decode($result['RetMsg'],true); cache('gjp_sign',$dataResult,60*60*20); } return $dataResult; } /** * @Author: 小破孩嫩 * @Email: 3584685883@qq.com * @Time: 2021/4/1 10:39 * @param string $url url地址 * @param string $method 请求方法,默认为 'GET',可选值为 'GET' 或 'POST' * @param mixed $data 要发送的数据,如果是 POST 请求则为数据内容,否则为 null * @param array $headers 自定义请求头信息 * @param int $timeout 超时时间,默认为 30 秒 * @param bool $verifySSL 是否验证 SSL 证书,默认为 true * @param bool $flbg 返回值是否转成数组,默认不转 * @param bool $headercontent 是否获取请求的header值内容,默认不获取 * @return array|bool|mixed|string * @Description:curl请求 */ public function curlRequest($url, $method = 'GET', $data = null, $flbg = false, $verifySSL = true, $headers = [], $headerContent = false, $timeout = 30) { // 初始化 cURL 会话 $ch = curl_init(); // 设置要请求的 URL curl_setopt($ch, CURLOPT_URL, $url); // 设置获取的信息以字符串形式返回,而不是直接输出 curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); // 设置超时时间 curl_setopt($ch, CURLOPT_TIMEOUT, $timeout); // 设置请求方法 if ($method === 'POST') { curl_setopt($ch, CURLOPT_POST, true); curl_setopt($ch, CURLOPT_POSTFIELDS, $data); } // 设置请求头 if (!empty($headers)) { curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); } // 设置是否验证 SSL 证书 curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, $verifySSL); // 执行 cURL 会话并获取响应 $response = curl_exec($ch); // 获取 HTTP 响应码 $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); // 如果 cURL 执行出错 if (curl_errno($ch)) { // 输出错误信息 echo 'Curl error: ' . curl_error($ch); // 关闭 cURL 会话并返回 false curl_close($ch); return false; } // 如果 HTTP 响应码大于等于 400(表示错误) elseif ($httpCode >= 400) { // 输出错误信息 echo "HTTP error: $httpCode"; // 关闭 cURL 会话并返回 false curl_close($ch); return false; } // 处理是否获取请求头内容 if ($headerContent && $httpCode == 200) { $headerSize = curl_getinfo($ch, CURLINFO_HEADER_SIZE); $headers = substr($response, 0, $headerSize); $body = substr($response, $headerSize); curl_close($ch); return [$headers, $body]; } // 关闭 cURL 会话 curl_close($ch); // 处理是否将响应转换为数组 if ($flbg) { $response = json_decode($response, true); } // 返回响应内容 return $response; } }
2025年04月23日
67 阅读
0 评论
0 点赞
2025-03-13
【PHP】通联支付 通企付 生产签名 PHP版本
/** * @Author:小破孩 * @Email:3584685883@qq.com * @Time:2025/2/17 15:25 * @param $array * @return string * @Description:数组以key=value&key=value 返回字符串 */ public function arrayKeyValueToString($array) { $result = ''; foreach ($array as $key => $value) { $result.= $key. '='. $value. '&'; } // 去除末尾多余的 & 符号 return rtrim($result, '&'); } /** * @Author:小破孩 * @Email:3584685883@qq.com * @Time:2025/2/14 15:32 * @return string * @Description:生产签名 */ public function setSign() { $data = [ 'mchNo' => $this->payConfig['tppay_mchid'], // 商户号 'appId' => $this->payConfig['tppay_appid'], // appid 'reqTime' => $this->currentTimestamp, // 13位时间戳 'version' => "1.0", // 固定值 'signType' => 'RSA', // 验签方式 'mchOrderNo' => $this->orderNo, // 订单号 'amount' => (string)$this->amount, // 金额 单位 分 'body' => $this->body, // 商品描述 'notifyUrl' => $this->getNotifyUrl(), // 回调通知地址 'expiredTime' => '1800', // 订单超时支付时间 单位 秒 'channelExtra' => $this->channelExtra, 'payTypeInfo' => (string)$this->payTypeInfo(), // 收银台展示的付款方式 // 'directPayType'=> (string)$this->getPayType(), // 直接支付的支付方式 ]; ksort($data); // Log::write("发起签名的参数:".var_export($data,true),"tppay"); $instanceArr = new \app\common\lib\data\Arr(); $encodedParams = $instanceArr->arrayKeyValueToString($data); Log::write("处理后的签名字符串:".PHP_EOL.var_export($encodedParams,true),"tppay"); $privateKey = "-----BEGIN PRIVATE KEY-----\n" . $this->payConfig['tppay_rsa_private_key'] . "\n-----END PRIVATE KEY-----"; $publicKey = "-----BEGIN PUBLIC KEY-----\n" . $this->payConfig['tppay_rsa_public_key'] . "\n-----END PUBLIC KEY-----"; // Log::write("发起签名的私钥:".var_export($privateKey,true),"tppay"); $instanceRsa = new \app\common\lib\pay\tppay\Rsa(null, null, $privateKey, $publicKey); $encryptedWithPrivate = $instanceRsa->sign($encodedParams); //签名使用SHA1withRSA // Log::write("签名的结果:".var_export($encryptedWithPrivate,true),"tppay"); return $encryptedWithPrivate; }
2025年03月13日
113 阅读
0 评论
0 点赞
2025-03-13
【PHP】ThinkPHP6.1 参数验证中间件
public function handle($request, \Closure $next) { try { // 获取并清理参数 $params = array_filter(array_map(function ($value) { return is_string($value) ? trim($value) : $value; }, $request->param()), function ($value) { return is_numeric($value) || !empty($value); }); unset($params['controller'], $params['function']); if (empty($params)) return $next($request); // 设置请求属性,方便后续使用 $request->checkParam = $params; // 获取应用名、控制器和操作名 $appName = app('http')->getName(); $controller = Request::instance()->controller(true); $action = Request::instance()->action(true); // 动态构建验证器路径 $controllerParts = explode('.', $controller); $validatePathParts = array_merge([$appName, 'validate'], $controllerParts); $lastKey = array_key_last($validatePathParts); $validatePathParts[$lastKey] = ucfirst((string) $validatePathParts[$lastKey]); // $validatePath = implode('\\', array_map('ucfirst', $validatePathParts)); $validatePath = 'app\\'.implode('\\', $validatePathParts); // 检查验证器是否存在及场景是否定义 if (!class_exists($validatePath) || !$this->sceneExists($validatePath, $action)) { return $next($request); } // 验证数据 $validateInstance = new $validatePath; if (!$validateInstance->scene($action)->check($params)) { throw new Exception($validateInstance->getError()); } } catch (Exception $e) { return show(100, $e->getMessage()); } return $next($request); } /** * 检查指定验证场景是否存在 * * @param string $validateClass 验证类名 * @param string $scene 场景名 * @return bool */ protected function sceneExists(string $validateClass, string $scene): bool { return (new $validateClass)->hasScene($scene); }
2025年03月13日
193 阅读
0 评论
0 点赞
1
2
...
9