直接进入主题这里直接在默认的Index控制器中编写的测试代码<?php /** * Created by PhpStorm. * Author: Dcr163 * Date: 2025/10/10 * Time: 15:49 * paypal服务类 */ namespace app\customer\services; use Exception; use think\App; use think\facade\Log; class PaypalService { //paypal 沙盒环境 const PAYPAL_URL = 'https://api-m.sandbox.paypal.com'; //paypal 正式环境 // const PAYPAL_URL = 'https://api-m.paypal.com'; //获取Token 链接 沙盒环境 const GET_TOKEN_URL = self::PAYPAL_URL . '/v1/oauth2/token'; //订单创建URL const CREATE_ORDER_URL = self::PAYPAL_URL . '/v2/checkout/orders'; //get:显示订单详情 patch:更新状态为“已创建”或“已批准”的订单。无法更新状态为“已完成”的订单 /v2/checkout/orders/{id} const SHOW_ORDER_URL = self::PAYPAL_URL . '/v2/checkout/orders/'; //API KEY 这里面对应https://developer.paypal.com/dashboard/applications/sandbox private $clientId; //API秘钥 这里面对应https://developer.paypal.com/dashboard/applications/sandbox private $clientSecret; //paypal通信token public $paypalToken = ''; //paypal auth token缓存key const TOKEN_KEY = 'paypal:auth:token'; //webhook值 https://developer.paypal.com/dashboard/applications app管理底部webhook板块 private $webhookId; //webhook 验证证书 private $certCache = []; //webhook验证证书保存地址 private $cacheDir = ''; public function __construct(array $config) { $this->clientId = $config['client_id'] ?? ''; $this->clientSecret = $config['client_secret'] ?? ''; } /** * 获取paypal通信 Token * @param bool $refresh 是否强制刷新token * @return void * @throws Exception */ public function getToken(bool $refresh = false) { //先读取缓存 $token = redisUser(14)->get(self::TOKEN_KEY); if (!$token || $refresh) { $res = $this->request(self::GET_TOKEN_URL, 'POST', http_build_query(['grant_type' => 'client_credentials']), '', 10, '', [$this->clientId, $this->clientSecret]); $data = json_decode($res['content'], true); if (isset($data['error']) || !isset($data['access_token'])) { throw new Exception($data); } //把token给缓存下来，设置过期时间早与paypal过期时间 redisUser(14)->setex(self::TOKEN_KEY, $data['expires_in'] - 10, $data['access_token']); $token = $data['access_token']; } $this->paypalToken = $token; } /** * 向paypal下单 * @param array $order 订单信息 * @param string $returnUrl 支付成功跳转地址 * @param string $cancelUrl 支付取消跳转地址 * @return array * @throws Exception */ public function orderCreate(array $order, string $returnUrl = '', string $cancelUrl = '') { $this->getToken(); //paypal接受的下单 $orderData = [ 'purchase_units' => [ [ //API 调用者提供的购买单位外部 ID 最长256字符 'reference_id' => $order['order_sn'], //购买描述 不超过3000字符 'description' => $order['order_sn'], //API 调用者提供的外部 ID 'custom_id' => $order['order_sn'], 'amount' => [ //订单所属货币：CNY、USD 这种格式 'currency_code' => $order['currency_name'], //订单总付款金额 'value' => $order['customer_paid_amount'], ], ] ], 'intent' => 'CAPTURE', //即时到账 'payment_source' => [ 'paypal' => [ 'experience_context' => [ 'payment_method_preference' => 'IMMEDIATE_PAYMENT_REQUIRED', //LOGIN:当客户点击 PayPal Checkout 时，客户将被重定向到一个页面以登录 PayPal 并批准付款 ; GUEST_CHECKOUT:当客户点击 PayPal 结账时，系统会将客户重定向至一个页面，要求输入信用卡或借记卡信息以及其他完成购买所需的相关账单信息。此选项之前也称为“账单”;NO_PREFERENCE:当客户点击 PayPal Checkout 时，客户将被重定向到登录 PayPal 并批准付款的页面，或者输入信用卡或借记卡以及完成购买所需的其他相关账单信息的页面，具体取决于他们之前与 PayPal 的互动。 'landing_page' => 'NO_PREFERENCE', //GET_FROM_FILE:在 PayPal 网站上获取客户提供的送货地址; NO_SHIPPING:从 API 响应和 Paypal 网站中删除收货地址信息; SET_PROVIDED_ADDRESS:获取商家提供的地址。客户无法在 PayPal 网站上更改此地址。如果商家未提供地址，客户可以在 PayPal 页面上选择地址 "shipping_preference" => "NO_SHIPPING", "user_action" => "PAY_NOW", "return_url" => $returnUrl, //批准付款后跳转的商家地址 "cancel_url" => $cancelUrl //取消付款后跳转的商家地址 ], ] ], ]; //请求需要的格式 $header = [ 'Authorization:Bearer ' . $this->paypalToken, 'Content-Type:application/json', ]; $res = $this->request(self::CREATE_ORDER_URL, 'POST', json_encode($orderData), $header); $content = json_decode($res['content'], true); if (!in_array($res['http_code'], [200, 201])) { $this->logError($res); throw new Exception(json_encode($res)); } //前端需要跳转的支付地址 $redirectUrl = ''; //获取操作支付的地址 foreach ($content['links'] as $link) { //payer-action 获取支付的URL if ($link['rel'] == 'payer-action') { $redirectUrl = $link['href']; break; } } //返回前端 return [ 'id' => $content['id'], 'paypal_id' => $content['id'], 'status' => $content['status'], 'redirect_url' => $redirectUrl, ]; } /** * 查询paypal订单详情 * @param string $paypalId * @return mixed * @throws Exception */ public function orderShow(string $paypalId) { $this->getToken(); $res = $this->request(self::SHOW_ORDER_URL . $paypalId, 'get', [], ['Authorization:Bearer ' . $this->paypalToken]); $content = json_decode($res['content'], true); if (!in_array($res['http_code'], [200, 201])) { $this->logError($res); throw new Exception(json_encode($res)); } return $content; } /** * 捕获支付订单，这里可以修改订单状态，同步capture_id到订单表 * @param string $paypalId * @return mixed * @throws Exception */ public function orderCapture(string $paypalId) { $this->getToken(); $header = [ 'Authorization:Bearer ' . $this->paypalToken, 'Content-Type:application/json', ]; $res = $this->request(self::SHOW_ORDER_URL . $paypalId . '/capture', 'post', [], $header); $content = json_decode($res['content'], true); if (!in_array($res['http_code'], [200, 201])) { $this->logError($res); throw new Exception(json_encode($res)); } return $content; } /** * 支付订单退款 * @param string $captureId 捕获订单的ID * @param string $currencyCode 退款的货币：CNY、USD 这种格式 * @param string $money 退款的金额 * @return mixed * @throws Exception */ public function orderRefund(string $captureId, string $currencyCode, string $money) { $this->getToken(); $refundUrl = self::PAYPAL_URL . '/v2/payments/captures/' . $captureId . '/refund'; $header = [ 'Authorization:Bearer ' . $this->paypalToken, 'Content-Type:application/json', ]; $params = [ 'amount' => [ 'currency_code' => $currencyCode, 'value' => $money, ] ]; $res = $this->request($refundUrl, 'post', json_encode($params), $header); $content = json_decode($res['content'], true); if (!in_array($res['http_code'], [200, 201])) { $this->logError($res); throw new Exception(json_encode($res)); } return $content; } /** * 发起CURL请求 * @param string $url * @param string $method * @param $data * @param $header * @param int $timeout * @param $cookie * @param array $authInfo * @return array */ private function request(string $url, string $method = 'get', $data = [], $header = false, int $timeout = 10, $cookie = '', array $authInfo = []) { $curl = curl_init($url); $method = strtoupper($method); //请求方式 curl_setopt($curl, CURLOPT_CUSTOMREQUEST, $method); //携带参数 if (in_array($method, ['POST', 'PATCH'])) { curl_setopt($curl, CURLOPT_POSTFIELDS, $data); } elseif ($method == 'GET' && count($data)) { $url .= '?' . http_build_query($data); curl_setopt($curl, CURLOPT_URL, $url); } //超时时间 curl_setopt($curl, CURLOPT_TIMEOUT, $timeout); //设置header头 if ($header) curl_setopt($curl, CURLOPT_HTTPHEADER, $header); curl_setopt($curl, CURLOPT_FAILONERROR, false); //返回抓取数据 curl_setopt($curl, CURLOPT_RETURNTRANSFER, true); //输出header头信息 curl_setopt($curl, CURLOPT_HEADER, true); //TRUE 时追踪句柄的请求字符串，从 PHP 5.1.3 开始可用。这个很关键，就是允许你查看请求header curl_setopt($curl, CURLINFO_HEADER_OUT, true); //https请求 if (1 == strpos("$" . $url, "https://")) { curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, false); curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, false); } //设置cookie if ($cookie != '') curl_setopt($curl, CURLOPT_COOKIE, $cookie); //设置认证信息 if (!empty($authInfo)) curl_setopt($curl, CURLOPT_USERPWD, implode(':', $authInfo)); $curlError = curl_error($curl); list($content, $status) = [curl_exec($curl), curl_getinfo($curl), curl_close($curl)]; $content = trim(substr($content, $status['header_size'])); return [ 'error' => $curlError, 'http_code' => $status['http_code'], 'content' => $content, ]; } ##===============下面是 webhook 异步回调用的函数===============## /** * 初始化webhook * @param string $webhookId * @return void */ public function initWebhook(string $webhookId) { $this->webhookId = $webhookId; $this->cacheDir = app()->getRuntimePath() . 'log/paypal/'; // 确保缓存目录存在 if (!is_dir($this->cacheDir)) { mkdir($this->cacheDir, 0755, true); } } /** * 验证 PayPal Webhook 签名 */ public function verifySignature($rawRequestBody, $headers) { try { // 获取必要的头信息 $transmissionId = $this->getHeader($headers, 'Paypal-Transmission-Id'); $transmissionTime = $this->getHeader($headers, 'Paypal-Transmission-Time'); $transmissionSig = $this->getHeader($headers, 'Paypal-Transmission-Sig'); $certUrl = $this->getHeader($headers, 'Paypal-Cert-Url'); $authAlgo = $this->getHeader($headers, 'Paypal-Auth-Algo', 'SHA256withRSA'); // 验证必要的头信息存在 if (!$transmissionId || !$transmissionTime || !$transmissionSig || !$certUrl) { $this->log("Missing required PayPal webhook headers"); } // 计算 CRC32（使用 crc32b，PHP 默认） $crc32 = hash('crc32b', $rawRequestBody); // 转为十进制整数 $crc32_decimal = hexdec($crc32); // 构建验证消息 $message = sprintf('%s|%s|%s|%s', $transmissionId, $transmissionTime, $this->webhookId, $crc32_decimal ); $this->logError("Verification message: {$message}"); // 获取证书 $certificate = $this->downloadAndCacheCertificate($certUrl); // 解码签名 $signature = base64_decode($transmissionSig); // 验证签名 return $this->verifyRSASignature($message, $signature, $certificate, $authAlgo); } catch (Exception $e) { $this->logError("PayPal webhook verification failed: " . $e->getMessage()); return false; } } /** * 验证 RSA 签名 */ private function verifyRSASignature($message, $signature, $certificate, $authAlgo) { // 从证书中提取公钥 $publicKey = openssl_pkey_get_public($certificate); if ($publicKey === false) { $this->log('Failed to extract public key from certificate: ' . openssl_error_string()); } // 根据算法确定 OpenSSL 算法常量 $algorithm = $this->getOpenSSLAlgorithm($authAlgo); // 验证签名 $result = openssl_verify($message, $signature, $publicKey, $algorithm); // 释放资源 openssl_free_key($publicKey); if ($result === 1) { return true; } elseif ($result === 0) { $this->logError('Signature verification failed: Invalid signature'); } else { $this->logError('Signature verification error: ' . openssl_error_string()); } } /** * 将 PayPal 算法名称转换为 OpenSSL 常量 */ private function getOpenSSLAlgorithm($authAlgo) { $mapping = [ 'SHA256withRSA' => OPENSSL_ALGO_SHA256, 'SHA1withRSA' => OPENSSL_ALGO_SHA1, 'SHA384withRSA' => OPENSSL_ALGO_SHA384, 'SHA512withRSA' => OPENSSL_ALGO_SHA512, 'MD5withRSA' => OPENSSL_ALGO_MD5, ]; return $mapping[$authAlgo] ?? OPENSSL_ALGO_SHA256; } /** * 下载并缓存证书 */ private function downloadAndCacheCertificate($certUrl) { // 检查缓存 $cacheKey = md5($certUrl); $cacheFile = $this->cacheDir . '/' . $cacheKey . '.pem'; if (isset($this->certCache[$cacheKey])) { return $this->certCache[$cacheKey]; } if (file_exists($cacheFile) && time() - filemtime($cacheFile) < 3600) { // 使用缓存的证书（1小时内有效） $certificate = file_get_contents($cacheFile); $this->certCache[$cacheKey] = $certificate; return $certificate; } // 下载新证书 $context = stream_context_create([ 'ssl' => [ 'verify_peer' => true, 'verify_peer_name' => true, 'allow_self_signed' => false, ], 'http' => [ 'timeout' => 10, 'user_agent' => 'PayPal-Webhook-Verifier/1.0' ] ]); $certificate = file_get_contents($certUrl, false, $context); if ($certificate === false) { $this->logError("Failed to download certificate from: {$certUrl}"); } // 验证证书格式 if (!openssl_x509_read($certificate)) { $this->logError('Invalid certificate format: ' . openssl_error_string()); } // 缓存证书 file_put_contents($cacheFile, $certificate); $this->certCache[$cacheKey] = $certificate; return $certificate; } /** * 安全获取头信息 */ private function getHeader($headers, $key, $default = null) { if (isset($headers[$key])) { return $headers[$key]; } $key = 'HTTP_' . $key; if (isset($headers[$key])) { return $headers[$key]; } return $default; } /** * 清理所有证书缓存 */ public function clearCache() { $files = glob($this->cacheDir . '*.pem'); foreach ($files as $file) { if (is_file($file)) { unlink($file); } } $this->certCache = []; } /** * 记录日志 * @param $data * @return void */ public function log($data) { Log::channel('paypal')->info($data); } /** * 记录错误信息 * @param $data * @return void */ public function logError($data) { Log::channel('paypal')->error($data); } }整理了一个简易的流程图需要注意： 新台币TWD、日圆JPY、匈牙利福林HUF 的货币，是不支持小数点的，他们必须取整，否则下不了单！
前提是已经安装好Rabbitmq的服务和安装好Rabbitmq消息延迟队列插件新建数据表使用的orders表结构CREATE TABLE `orders` ( `id` int(11) unsigned NOT NULL AUTO_INCREMENT, `orderno` varchar(60) NOT NULL DEFAULT '' COMMENT '订单号', `price` decimal(10,2) NOT NULL DEFAULT '0.00' COMMENT '价格', `create_time` datetime DEFAULT NULL, `cancel_time` datetime DEFAULT NULL, `status` tinyint(3) DEFAULT '0', PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=75 DEFAULT CHARSET=utf8mb4 COMMENT '测试订单表';添加控制器在app模块新增一个控制器/app/controller/Order.php,内容如下：<?php declare (strict_types=1); namespace app\controller; use app\service\RabbitmqService; use Exception; use think\Response; use think\response\Json; class Order { /** * * 模拟客户下单 * @return Response */ public function index() { $db = db('orders'); $getConnection = $db->getConnection(); $timeOut = 1800; $id = $getConnection->transaction(function () use ($db, $timeOut) { $time = time(); $orderData = [ 'orderno' => uniqid('hm'), 'price' => mt_rand(10, 999), 'create_time' => date('Y-m-d H:i:s', $time), 'status' => 0, ]; $id = $db->insertGetId($orderData); $rabbitmqService = app()->make(RabbitmqService::class); $rabbitmqService->sendDelayedMsg([ 'id' => $id, ], $timeOut); return $id; }); if ($id) { $msg = sprintf('订单插入成功：【%d】;在 %f 分钟内未支付，将自动取消订单~', $id, $timeOut/60); return json(['msg' => $msg]); } return json(['msg' => 'fail']); } /** * 模拟异步发送邮件 * @return Json * @throws Exception */ public function email() { $mailData = [ 'email' => mt_rand(10000000, 999999999) . '@qq.com', ]; $rabbitmqService = app()->make(RabbitmqService::class); $res = $rabbitmqService->sendMsg($mailData); return $res ? json(['msg' => $mailData['email']]) : json(['msg' => 'fail']); } }添加路由修改位置：/route/app.php,新增2个访问的地址Route::group('order',function(){ Route::get('index', Order::class.'::index'); Route::get('email', Order::class.'::email'); });添加订单取消和发送邮件的操作脚本主要是用在php think xx 命令行中执行新建订单取消命令行脚本新建文件：/app/command/OrderCancel.php,文件内容如下：<?php declare (strict_types=1); namespace app\command; use app\service\RabbitmqService; use PhpAmqpLib\Message\AMQPMessage; use PhpAmqpLib\Wire\AMQPTable; use think\console\Command; use think\console\Input; use think\console\Output; use think\facade\Db; use think\facade\Log; class OrderCancel extends Command { protected function configure() { // 指令配置 $this->setName('orderCancel') ->setDescription('订单超时未支付自动取消订单'); } protected function execute(Input $input, Output $output) { // 指令输出 $output->writeln('orderCancel'); $rabbitmqService = app()->make(RabbitmqService::class); //声明交换机 $params = new AMQPTable([ 'x-delayed-type' => 'direct', ]); $rabbitmqService->channel->exchange_declare(config('rabbitmq.exchange'), 'x-delayed-message', false, true, false, false, false, $params); //声明队列 $rabbitmqService->channel->queue_declare(config('rabbitmq.queue'), false, true, false, false); //绑定队列和交换机 $rabbitmqService->channel->queue_bind(config('rabbitmq.queue'), config('rabbitmq.exchange'), config('rabbitmq.routing_key')); $callback = function (AMQPMessage $msg) use ($rabbitmqService, $output) { $output->writeln('In callBack'); $data = json_decode($msg->getBody(), true); $output->writeln($msg->getBody()); try { $res = $this->cancelOrder($data['id']); if ($res) { $output->writeln('订单【' . $data['id'] . '】处理完毕'); $msg->ack(); return; } $msg->nack(); } catch (\Exception $exception) { Log::error($exception->getMessage()); $msg->nack(); } }; // 设置 QoS，防止消费者一次性获取过多消息 $rabbitmqService->channel->basic_qos(null, 1, null); $rabbitmqService->channel->basic_consume(config('rabbitmq.queue'), '', false, false, false, false, $callback); try { $rabbitmqService->channel->consume(); } catch (\Exception $exception) { $rabbitmqService->channel->close(); $rabbitmqService->connection->close(); Log::info("监听异常：" . $exception->getMessage()); } } /** * 取消订单 * @param int $orderId * @return bool * @throws \think\db\exception\DataNotFoundException * @throws \think\db\exception\DbException * @throws \think\db\exception\ModelNotFoundException */ public function cancelOrder(int $orderId) { try { // 尝试执行一个简单的查询来检查连接状态 Db::query('SELECT 1'); } catch (\Exception $e) { Log::error('数据库连接断开: ' . $e->getMessage()); // 关闭当前连接 Db::close(); // 重新连接 Db::connect(); } $db = db('orders'); $order = $db->where('id', $orderId)->find(); if ($order['status'] != 0) return false; $res = $db->where('id', $order['id'])->update([ 'status' => -1, 'cancel_time' => date('Y-m-d H:i:s'), ]); return (bool)$res; } }新建发送邮件命令行脚本新建文件：/app/command/SendEmail.php,文件内容如下：<?php declare (strict_types=1); namespace app\command; use app\service\RabbitmqService; use PhpAmqpLib\Message\AMQPMessage; use think\console\Command; use think\console\Input; use think\console\Output; use think\Exception; use think\facade\Db; use think\facade\Log; class SendEmail extends Command { protected function configure() { // 指令配置 $this->setName('sendEmail') ->setDescription('发送邮件'); } protected function execute(Input $input, Output $output) { // 指令输出 $output->writeln('sendEmail'); $rabbitmqService = app()->make(RabbitmqService::class); $rabbitmqService->channel->exchange_declare('send_email.exchange', 'direct', false, true, false); $rabbitmqService->channel->queue_declare('send_mail.queue', false, true, false, false); $rabbitmqService->channel->queue_bind('send_mail.queue', 'send_email.exchange'); $callback = function (AMQPMessage $msg) use ($rabbitmqService, $output) { try { $data = json_decode($msg->getBody(), true); $output->writeln($msg->getBody()); Log::info($msg->getBody()); $output->writeln('邮件【' . $data['email'] . '】处理完毕'); $msg->ack(); } catch (\Exception $exception) { $output->writeln($exception->getMessage()); $msg->nack(); } }; // 设置 QoS，防止消费者一次性获取过多消息 $rabbitmqService->channel->basic_qos(null, 1, null); $rabbitmqService->channel->basic_consume('send_mail.queue', '', false, false, false, false, $callback); try { $rabbitmqService->channel->consume(); } catch (\Exception $exception) { $rabbitmqService->channel->close(); $rabbitmqService->connection->close(); Log::info("监听异常：" . $exception->getMessage()); } } }注册到think命令新建好命令行执行脚本后，还需要注册到think中，否则使用php think xx会找不到命令修改文件：/config/console.php,文件内容如下：<?php return [ // 指令定义 'commands' => [ 'orderCancel' => \app\command\OrderCancel::class, //注册 可以用 php think orderCancel 执行 'sendEmail' => \app\command\SendEmail::class, //注册 可以用 php think sendEmail 执行 ], ];这时候，可以去到项目根目录，执行php think，就能查看到自己定义的命令了监听未支付订单取消直接去到项目的根目录执行 php think orderCancel，不要关闭窗口监听邮件发送新建一个窗口，去到项目的根目录执行php think sendEmail不要关闭窗口测试直接修改文件/app/controller/Order.php对应的方法，修改成自己想要的结果，在浏览器访问：http://xxx/order/index模拟订单取消http://xxx/order/email模拟邮件发送这时候再去控制台查看监听进程，正常情况会输出对应的打印信息了。
直接上代码：$data = [ '一队' => [ 'counts' => 0, 'totals' => 0, 'team' => [ ['realname' => '张三', 'counts' => 10, 'totals' => 600], ['realname' => '张三2', 'counts' => 17, 'totals' => 478], ['realname' => '张三3', 'counts' => 190, 'totals' => 10000], ['realname' => '张三4', 'counts' => 8, 'totals' => 500], ] ], '二队' => [ 'counts' => 0, 'totals' => 0, 'team' => [ ['realname' => '李四', 'counts' => 10, 'totals' => 600], ['realname' => '李四2', 'counts' => 17, 'totals' => 478], ['realname' => '李四3', 'counts' => 190, 'totals' => 10000], ['realname' => '李四4', 'counts' => 8, 'totals' => 500], ] ], '三队' => [ 'counts' => 0, 'totals' => 0, 'team' => [ ['realname' => '王五', 'counts' => 10, 'totals' => 600], ['realname' => '王五2', 'counts' => 17, 'totals' => 478], ['realname' => '王五3', 'counts' => 190, 'totals' => 10000], ['realname' => '王五4', 'counts' => 8, 'totals' => 500], ['realname' => '王五5', 'counts' => 11, 'totals' => 600], ] ], ]; //数组根据指定key排序 foreach ($data as $key => &$val) { usort($val['team'], function ($a, $b) { //根据totals从高到低排序 if ($a['totals'] != $b['totals']) { if ($a['totals'] > $b['totals']) { // 如果是 < 就是从低到高排序 return -1; } elseif ($a['totals'] == $b['totals']) { return 0; } else { return 1; } //php7+可以这么写,使用航天飞船符 <=> 更简洁，逻辑清晰 //return $b['totals'] <=> $a['totals']; } //根据counts从高到底排序 if ($a['counts'] > $b['counts']) { // 如果是 < 就是从低到高排序 return -1; } elseif ($a['counts'] == $b['counts']) { return 0; } else { return 1; } //php7+可以这么写,使用航天飞船符 <=> 更简洁，逻辑清晰 //return $b['counts'] <=> $a['counts']; }); } unset($val); echo ' '; print_r($data); 看看最终输出的结果：Array ( [一队] => Array ( [counts] => 0 [totals] => 0 [team] => Array ( [0] => Array ( [realname] => 张三3 [counts] => 190 [totals] => 10000 ) [1] => Array ( [realname] => 张三 [counts] => 10 [totals] => 600 ) [2] => Array ( [realname] => 张三4 [counts] => 8 [totals] => 500 ) [3] => Array ( [realname] => 张三2 [counts] => 17 [totals] => 478 ) ) ) [二队] => Array ( [counts] => 0 [totals] => 0 [team] => Array ( [0] => Array ( [realname] => 李四3 [counts] => 190 [totals] => 10000 ) [1] => Array ( [realname] => 李四 [counts] => 10 [totals] => 600 ) [2] => Array ( [realname] => 李四4 [counts] => 8 [totals] => 500 ) [3] => Array ( [realname] => 李四2 [counts] => 17 [totals] => 478 ) ) ) [三队] => Array ( [counts] => 0 [totals] => 0 [team] => Array ( [0] => Array ( [realname] => 王五3 [counts] => 190 [totals] => 10000 ) [1] => Array ( [realname] => 王五5 [counts] => 11 [totals] => 600 ) [2] => Array ( [realname] => 王五 [counts] => 10 [totals] => 600 ) [3] => Array ( [realname] => 王五4 [counts] => 8 [totals] => 500 ) [4] => Array ( [realname] => 王五2 [counts] => 17 [totals] => 478 ) ) ) )
环境Centos8PHP8.4.8Swoole6.0.2下载Swoole6.0.2使用的Github地址下载（比较新），下载地址：https://github.com/swoole/swoole-src可以下载zip或源码都行，我这边下载好了.zip解压:unzip swoole-src-master.zip进入到解压后的目录可以看到解压出来的是没有configure这个执行文件的，我们需要用php8的phpize命令生成下，我这边php8安装目录在/usr/local/php8/直接在当前目录执行命令：/usr/local/php8/bin/phpize执行完毕后就发现生成了这个文件了：configure开始执行configure执行命令：./configure --with-php-config=/usr/local/php8/bin/php-config这个选项--with-php-config一定要匹配到对应的目录编译并且安装执行命令：make && make install如果不出意外的话，就编译好了swoole.so这个文件到php8的扩展目录下在php.ini中启用swoole扩展编辑配置文件：vim /usr/local/php8/etc/php.ini追加这个拓展：extension=swoole保存后，使用php -m验证是否成功加载swoole以上就是PHP8安装swoole拓展的步骤了。
先新建一个软件目录，我们全部放在lnmp这个目录中mkdir lnmp && cd lnmp下载Nginx先去官方网站下载http://nginx.org/en/download.html，这里我们选择最新版下载 http://nginx.org/download/nginx-1.23.4.tar.gz[root@centos8_1 lnmp]# wget http://nginx.org/download/nginx-1.23.4.tar.gz下载Mysql，这里我们使用Mysql8.0.33下载地址：https://cdn.mysql.com/Downloads/MySQL-8.0/mysql-8.0.33-linux-glibc2.12-x86_64.tar.xz[root@centos8_1 lnmp]# wget https://cdn.mysql.com/Downloads/MySQL-8.0/mysql-8.0.33-linux-glibc2.12-x86_64.tar.xz下载PHP7.4.33下载地址：https://www.php.net/distributions/php-7.4.33.tar.gz[root@centos8_1 lnmp]# wget https://www.php.net/distributions/php-7.4.33.tar.gz顺便提供下其他版本的PHP下载地址：PHP7.0.33 https://www.php.net/distributions/php-7.0.33.tar.gzPHP7.2.34 https://www.php.net/distributions/php-7.2.34.tar.gzPHP7.4.33 https://www.php.net/distributions/php-7.4.33.tar.gz下载rpcsvc-proto[root@centos8_1 lnmp]# wget https://www.linuxprobe.com/Software/rpcsvc-proto-1.4.tar.gz系统要有具备编译程序源码的环境，把常用的软件包都给安装上[root@centos8_1 lnmp]# dnf -y install apr* autoconf automake numactl bison bzip2-devel cpp curl-devel fontconfig-devel freetype-devel gcc gcc-c++ gd-devel gettext-devel kernel-headers keyutils-libs-devel krb5-devel libcom_err-devel libpng-devel libjpeg* libsepol-devel libselinux-devel libstdc++-devel libtool* libxml2-devel libXpm* libxml* libXaw-devel libXmu-devel libtiff* make openssl-devel patch pcre-devel perl php-common php-gd telnet zlib-devel libtirpc-devel gtk* ntpstat na* bison* lrzsz cmake ncurses-devel libzip-devel libxslt-devel gdbm-devel readline-devel gmp-devel安装配置Nginx服务创建用于管理网站服务的系统账户，新建账户时应使用-M参数不创建对应的家目录，-s参数指定登录后的Shell解释器为/sbin/nologin，确保任何人都不能通过这个账号登录主机。[root@centos8_1 lnmp]# useradd nginx -M -s /sbin/nologin编译安装Nginx网站服务程序。为了能够让网站服务支持更多的功能，需要在编译过程中添加额外的参数，其中较为重要的是使用prefix参数指定服务将被安装到哪个目录，方便后面找到和调用它。其次，考虑到HTTPS协议的使用越来越广泛，所以这里用with-http_ssl_module参数来开启Nginx服务的SSL加密模块，以便日后开启HTTPS协议功能：[root@centos8_1 lnmp]# tar -zxvf nginx-1.24.0.tar.gz && cd nginx-1.24.0编译[root@centos8_1 lnmp]# ./configure --prefix=/usr/local/nginx --with-http_ssl_module 编译后安装[root@centos8_1 lnmp]# make && make install安装完毕后进入最终配置阶段，既然在编译环境中使用prefix参数指定了安装路径，在/usr/local/nginx目录 我们总共要进行3处修改，首先是把第2行的注释符（#）删除，然后在后面写上负责运行网站服务程序的账户名称和用户组名称。这里假设由nginx用户及nginx用户组负责管理网站服务。[root@centos8_1 lnmp]# vim /usr/local/nginx/conf/nginx.conf #下面是打开文件后的内容 1 2 user nginx;其次是修改第45行的首页文件名称，在里面添加index.php的名字。这个文件也是让用户浏览网站时第一眼看到的文件，也叫首页文件。 43 location / { 44 root html; 45 index index.php index.html index.htm; 46 } 最后再删除第65～71行前面的注释符（#）来启用虚拟主机功能，然后将第69行后面对应的网站根目录修改为$document_root，其中的fastcgi_script_name参数用于指代脚本名称，也就是用户请求的URL。只有信息填写正确了，才能使Nginx服务正确解析用户请求，否则访问的页面会提示“404 Not Found”的错误。 65 location ~ \.php$ { 66 root html; 67 fastcgi_pass 127.0.0.1:9000; 68 fastcgi_index index.php; 69 fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; 70 include fastcgi_params; 71 }通过编译源码方式安装的服务默认不能被systemctl命令所管理，而要使用Nginx服务本身的管理工具进行操作，相应命令所在的目录是/usr/local/nginx/sbin。由于使用绝对路径的形式输入命令未免会太麻烦，建议将/usr/local/nginx/sbin路径加入到PATH变量中，让Bash解释器在后续执行命令时自动搜索到它。然后在source命令后加载配置文件，让参数立即生效。下次就只需要输入nginx命令即可启动网站服务了。[root@centos8_1 lnmp]# vim ~/.bash_profile # .bash_profile # Get the aliases and functions if [ -f ~/.bashrc ]; then . ~/.bashrc fi # User specific environment and startup programs PATH=$PATH:$HOME/bin:/usr/local/nginx/sbin export PATH [root@centos8_1 lnmp]# source ~/.bash_profile [root@centos8_1 lnmp]# nginx 查看Nignx服务器是否启动，下面可以看到是成功启动了服务root@centos8_1 lnmp]# ps aux|grep nginx root 5238 0.0 0.1 42344 840 ? Ss 23:04 0:00 nginx: master process nginx nginx 5239 0.0 0.6 74464 4988 ? S 23:04 0:00 nginx: worker process root 5456 0.0 0.1 12348 1080 pts/0 S+ 23:16 0:00 grep --color=auto nginx并在浏览器中输入本机的IP地址，即可访问到Nginx网站服务程序的默认界面，如果访问不了，可能是防火墙打开了，关闭防火墙再试试[root@centos8_1 lnmp]# systemctl stop firewalld安装配置Mysql服务添加mysql用户，对于MySQL数据库来说，我们需要在系统中创建一个名为mysql的用户，专门用于负责运行MySQL数据库。请记得要把这类账户的Bash终端设置成nologin解释器，避免黑客通过该用户登录到服务器中，从而提高系统安全性。[root@centos8_1 lnmp]# useradd mysql -M -s /sbin/nologin解压MySQL安装软件包。将解压出的程序目录改名并移动到/usr/local目录下，对其进行初始化操作后便可使用。需要注意的是，以.tar.xz结尾的压缩包软件，不应用z参数进行解压。[root@centos8_1 lnmp]# tar xvf mysql-8.0.33-linux-glibc2.12-x86_64.tar.xz [root@centos8_1 lnmp]# mv mysql-8.0.33-linux-glibc2.12-x86_64 /usr/local/mysql在生产环境中管理MySQL数据库时，有两个比较常用的目录。一个是/usr/local/mysql目录，这是用于保存MySQL数据库程序文件的路径。还有一个是/usr/local/mysql/data目录，它用于存储数据库的具体内容，每个数据库的内容会被单独存放到一个目录内。对于存放实际数据库文件的data目录，用户需要先手动创建出来：[root@centos8_1 lnmp]# cd /usr/local/mysql [root@centos8_1 mysql]# mkdir data初始化MySQL服务程序，对目录进行授权更改为mysql用户所属，保证数据能够被mysql系统用户读取。在初始化阶段，应使用mysqld命令确认管理MySQL数据库服务的用户名称、数据保存目录及编码信息。在信息确认无误后开始进行初始化。在初始化的最后阶段，系统会给用户分配一个初始化的临时密码，大家一定要保存好，例如下面示例中分配的密码是 PhwfpNq:u8ot[root@centos8_1 mysql]# chown -R mysql:mysql /usr/local/mysql/ [root@centos8_1 mysql]# cd bin [root@centos8_1 bin]# ./mysqld --initialize --user=mysql --basedir=/usr/local/mysql --datadir=/usr/local/mysql/data与Nginx服务相似，MySQL数据库的二进制可执行命令也单独存放在自身的程序目录/usr/local/mysql/bin中。若每次在执行命令之前都要先切换到这个目录，则着实有些麻烦，这里我们也把它添加到环境变量中[root@centos8_1 bin]# vim ~/.bash_profile # .bash_profile # Get the aliases and functions if [ -f ~/.bashrc ]; then . ~/.bashrc fi # User specific environment and startup programs PATH=$PATH:$HOME/bin:/usr/local/nginx/sbin:/usr/local/mysql/bin export PATH [root@centos8_1 bin]# source ~/.bash_profile在这样设置后，即便返回到源码目录，也可以继续执行MySQL数据库的管理命令。不过先别着急！既然是手动安装服务，那么让文件“归位”的重任就只得亲力亲为了，将启动脚本mysql.server放入到/etc/init.d目录中，让服务器每次重启后都能自动启动数据库，并给予可执行权限。 libtinfo.so.5文件是MySQL数据库在8.0版本后新添加的重要的函数库文件，但默认不存在，需要将libtinfo.so.6.1文件复制过来或者作为链接文件才能正常启动：[root@centos8_1 bin]# cd /usr/local/mysql/ [root@centos8_1 mysql]# cp -a support-files/mysql.server /etc/init.d/ [root@centos8_1 mysql]# chmod a+x /etc/init.d/mysql.server [root@centos8_1 mysql]# ln -s /usr/lib64/libtinfo.so.6.1 /usr/lib64/libtinfo.so.5执行MySQL数据库服务启动文件，并进行初始化工作。为了安全着想，MySQL自8.0版本起不再允许用户使用临时密码来管理数据库内容，也不能进行远程控制，用户必须修改初始化密码后才能使用MySQL数据库。数据库作为系统重要的组成服务，密码位数不建议少于20位。例如，下面将密码修改为“PObejCBeDzTRCncXwgBy”。下面的连接密码，就是上面mysql初始化的原始密码。[root@centos8_1 mysql]# /etc/init.d/mysql.server start Starting MySQL.Logging to '/usr/local/mysql/data/centos8_1.err'. .. SUCCESS! [root@centos8_1 mysql]# mysql -u root -p Enter password: Welcome to the MySQL monitor. Commands end with ; or \g. Your MySQL connection id is 8 Server version: 8.0.33 Copyright (c) 2000, 2023, Oracle and/or its affiliates. Oracle is a registered trademark of Oracle Corporation and/or its affiliates. Other names may be trademarks of their respective owners. Type 'help;' or '\h' for help. Type '\c' to clear the current input statement. mysql> ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY 'PObejCBeDzTRCncXwgBy';安装配置PHP服务添加www用户，对于php来说，我们需要在系统中创建一个名为www的用户，专门用于负责运行php。请记得要把这类账户的Bash终端设置成nologin解释器，避免黑客通过该用户登录到服务器中，从而提高系统安全性。[root@centos8_1 php-7.4.33]# useradd www -M -s /sbin/nologin编译php7的时候需要先建立一个lib库，否则编译的时候会报错 configure: error: Cannot find ldap libraries in /usr/lib.[root@centos8_1 php-7.4.33]# cp -frp /usr/lib64/libldap* /usr/lib/解压php安装包软件并编译安装。在编译期间，需要使用prefix参数指定安装路径，使用--with-mysqli等参数开启对数据库支持的模块列表[root@centos8_1 mysql]# cd /lnmp [root@centos8_1 lnmp]# tar zxvf php-7.4.33.tar.gz [root@centos8_1 lnmp]# cd php-7.4.33 [root@centos8_1 php-7.4.33]# ./configure --prefix=/usr/local/php7.4 --with-config-file-path=/usr/local/php7.4/etc --enable-fpm --with-fpm-user=www --with-fpm-group=www --enable-mysqlnd --with-mysqli=mysqlnd --with-pdo-mysql=mysqlnd --enable-mysqlnd-compression-support --with-iconv-dir --with-zlib --enable-xml --disable-rpath --enable-bcmath --enable-shmop --enable-sysvsem --enable-inline-optimization --with-curl --enable-mbregex --enable-mbstring --enable-intl --enable-ftp --with-gd --with-openssl --with-mhash --enable-pcntl --enable-sockets --with-xmlrpc --enable-soap --with-gettext --disable-fileinfo --enable-opcache --with-pear --enable-maintainer-zts --with-ldap=shared --without-gdbm编译时发现有个软件包报错 这里我们安装下这个sqlite-devel[root@centos8_1 php-7.4.33]# yum install sqlite-devel编译过程中又发现一个报错 *Package 'oniguruma', required by 'virtual:world', not found 先去github下载，然后解压安装[root@centos8_1 php-7.4.33]# cd /lnmp/ [root@centos8_1 lnmp]# wget https://github.com/kkos/oniguruma/archive/v6.9.4.tar.gz -O oniguruma-6.9.4.tar.gz [root@centos8_1 lnmp]# tar zxvf oniguruma-6.9.4.tar.gz [root@centos8_1 lnmp]# cd oniguruma-6.9.4/ [root@centos8_1 oniguruma-6.9.4]# ./autogen.sh && ./configure --prefix=/usr [root@centos8_1 oniguruma-6.9.4]# make && make install再试试编译php[root@centos8_1 oniguruma-6.9.4]# cd /lnmp/php-7.4.33/ [root@centos8_1 php-7.4.33]# ./configure --prefix=/usr/local/php7.4 --with-config-file-path=/usr/local/php7.4/etc --enable-fpm --with-fpm-user=www --with-fpm-group=www --enable-mysqlnd --with-mysqli=mysqlnd --with-pdo-mysql=mysqlnd --enable-mysqlnd-compression-support --with-iconv-dir --with-zlib --enable-xml --disable-rpath --enable-bcmath --enable-shmop --enable-sysvsem --enable-inline-optimization --with-curl --enable-mbregex --enable-mbstring --enable-intl --enable-ftp --with-gd --with-openssl --with-mhash --enable-pcntl --enable-sockets --with-xmlrpc --enable-soap --with-gettext --disable-fileinfo --enable-opcache --with-pear --enable-maintainer-zts --with-ldap=shared --without-gdbm这次发现没有报错了 直接执行，使用下述命令生成二进制文件并进行安装，时间大约为10～20分钟，耐心等待即可：[root@centos8_1 php-7.4.33]# make && make install将生成的php服务配置文件复制到安装目录中（/usr/local/php7.4/），让其生效。[root@centos8_1 php-7.4.33]# cp php.ini-development /usr/local/php7.4/etc/php.ini [root@centos8_1 php7.4]# cd /usr/local/php7.4/etc/现在主配置文件有了，接下来还需要php-fpm的配置文件，好在/usr/local/php/etc/目录中也已经提供，只需要复制模板即可：[root@centos8_1 etc]# cp php-fpm.conf.default php-fpm.conf复制一个模板文件到php-fpm.d的目录中，用于后续控制网站的连接性能：[root@centos8_1 etc]# mv php-fpm.d/www.conf.default php-fpm.d/www.con把php服务加入到启动项中，使其重启后依然生效：[root@centos8_1 etc]# cd /lnmp/php-7.4.33/ [root@centos8_1 php-7.4.33]# cp sapi/fpm/init.d.php-fpm /etc/init.d/php-fpm7.4 [root@centos8_1 php-7.4.33]# chmod 755 /etc/init.d/php-fpm7.4由于php服务程序的配置参数会对Web服务的运行环境造成影响，如果默认开启了一些不必要且高危的功能（如允许用户在网页中执行Linux命令），则会降低网站被入侵的难度，甚至会让入侵人员拿到整台Web服务器的管理权限。因此需要编辑php.ini配置文件，在第312行的disable_functions参数后面追加上要禁止的功能。下面的禁用功能名单是刘遄老师依据本书配套站点的运行经验而定制的，不见得适合每个生产环境，建议大家在此基础上根据自身工作需求酌情删减：[root@centos8_1 php-7.4.33]# vim /usr/local/php7.4/etc/php.ini 309 ; This directive allows you to disable certain functions. 310 ; It receives a comma-delimited list of function names. 311 ; http://php.net/disable-functions 312 disable_functions = passthru,exec,system,chroot,chgrp,chown,shell_exec,proc_open,proc_get_status,popen,ini_alter,ini_restore, dl,openlog,syslog,readlink,symlink,popepassthru,stream_socket_server启动PHP[root@centos8_1 php-7.4.33]# /etc/init.d/php-fpm7.4 start各软件systemctl添加服务并且自启动方法php在/etc/systemd/system 新建文件：php-fpm.service内容入下，内容的文件路径请换成自己环境下的[Unit] Description=PHP 7.4 FastCGI Process Manager After=network.target [Service] Type=simple PIDFile=/usr/local/php7.4/var/run/php-fpm.pid # 需与 php-fpm.conf 中的 pid 设置一致 ExecStart=/usr/local/php7.4/sbin/php-fpm --nodaemonize --fpm-config /usr/local/php7.4/etc/php-fpm.conf ExecReload=/bin/kill -USR2 $MAINPID # 优雅重载配置 PrivateTmp=true [Install] WantedBy=multi-user.target设置开机自动启动在终端输入：systemctl enable php-fpm开机自启动mysql服务复制服务脚本[root@localhost mysql-5.7.34]# cp /usr/local/mysql/support-files/mysql.server /etc/init.d/mysqld添加此服务到系统服务[root@localhost mysql-5.7.34]# chkconfig --add mysqld添加服务后，就可以使用systemctl命令进行管理了启动服务[root@localhost mysql-5.7.34]# systemctl start mysqld设置服务为开机自启动[root@localhost mysql-5.7.34]# systemctl enable mysqld查看服务运行状态[root@localhost mysql-5.7.34]# systemctl status mysqld添加nginx终端运行：vim /usr/lib/systemd/system/nginx.service文件内容：[Unit] Description=nginx service After=network.target [Service] Type=forking PIDFile=/usr/local/nginx/logs/nginx.pid # 必须与 nginx.conf 中的 pid 路径一致 ExecStart=/usr/local/nginx/sbin/nginx # Nginx 启动路径 ExecReload=/usr/local/nginx/sbin/nginx -s reload ExecStop=/usr/local/nginx/sbin/nginx -s stop PrivateTmp=true [Install] WantedBy=multi-user.target添加完毕后可用下面命令管理sudo systemctl daemon-reload # 加载新服务配置 sudo systemctl start nginx # 启动服务 sudo systemctl enable nginx # 设置开机自启 sudo systemctl status nginx # 验证状态
安装PHP8报错：configure: error: iconv does not support errno
遇到 configure: error: iconv does not support errno 错误时，通常与 libiconv 库的版本或配置有关。以下是详细的解决方案：安装或升级 libiconvPHP 8 对 libiconv 的兼容性要求较高，需安装 GNU 的 libiconv（非系统自带的 glibc 的 iconv）。步骤：1.下载 libiconv 源码wget http://ftp.gnu.org/pub/gnu/libiconv/libiconv-1.16.tar.gz2.解压并编译安装tar -zxvf libiconv-1.16.tar.gz cd libiconv-1.16 ./configure --prefix=/usr/local/libiconv make && make install
例如：https://thirdwx.qlogo.cn/mmopen/vi_32/jDiauSBiauFCicDp9CBMjqNLfuaUibXQHciaaESNwvZhK2FQXTBJcvYwYcyOWrOmKO7otewTphq59g4J2GPfxE8DdNQ/132如果用file_get_contents函数下载保存，会影响程序的运行速度，快则几秒，多则几十秒这里建议用CURL操作：/** * 下载头像 * @param $url * @param $save_path * @param $openid * @return false|string[] */ private function saveWechatAvatar($url, $save_path, $openid) { // 创建保存目录 $absolute_path = $save_path; if (!file_exists($absolute_path)) { mkdir($absolute_path, 0755, true); } $filename = $openid . '.jpg'; $file_path = $absolute_path . '/' . $filename; if (is_file($file_path)) { return [ 'file_path' => $file_path, 'filename' => $filename, ]; } // 配置 CURL $ch = curl_init(); curl_setopt_array($ch, [ CURLOPT_URL => $url, CURLOPT_RETURNTRANSFER => true, CURLOPT_FOLLOWLOCATION => true, CURLOPT_ENCODING => 'gzip', // 压缩传输优化 CURLOPT_HTTPHEADER => [ 'User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.212 Safari/537.36', 'Accept-Encoding: gzip, deflate' ], CURLOPT_SSL_VERIFYPEER => false, // 跳过 SSL 验证 CURLOPT_TIMEOUT => 10 // 设置超时时间 ]); $image_data = curl_exec($ch); $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); // 保存图片 if ($http_code == 200 && !empty($image_data)) { return file_put_contents($file_path, $image_data) ? [ 'file_path' => $file_path, 'filename' => $filename, ] : false; } return false; }实际应用CURL获取图片，基本不影响程序使用，体验感大大提升。
生产环境LNMP环境优化配置，高并发配置
初始化服务器后，先挂载数据盘为数据盘创建分区fdisk -lu为数据盘创建分区yum install -y parted #开始分区 /dev/vdb 这个磁盘替换成自己的设备名称 parted /dev/vdb #在Parted交互命令行输入以下内容，设置GPT分区格式。 mklabel gpt #划分一个主分区，并设置分区的开始位置和结束位置。 mkpart primary 1 100% #检查分区是否对齐 align-check optimal 1 #是对齐的，结果如下所示： 1 aligned #查看分区表 print #退出Parted工具 quit #系统重读分区表 partprobe #查看新分区信息 fdisk -lu /dev/vdb分区创建文件系统yum install -y e2fsprogs #运行以下命令，创建文件系统 mkfs -t ext4 /dev/vdb配置开机自动挂载分区#备份/etc/fstab文件 cp /etc/fstab /etc/fstab.bak #修改/etc/fstab文件 echo `blkid /dev/vdb | awk '{print $2}' | sed 's/\"//g'` /www ext4 defaults 0 0 >> /etc/fstab #查看/etc/fstab中的新分区信息 cat /etc/fstab #挂载/etc/fstab配置的文件系统。如果无报错输出，表示您的/etc/fstab配置无误 mount -a #检查挂载结果是否符合 df -h采用lnmp.org一键安装环境采用阿里云Centos7.9系统，安装前可以根据自己的需求，修改安装脚本里的内容直接上传本目录lnmp1.9.tar.gz一键安装包到服务器上文件夹内部的源码包，是放在lnmp1.9/src这个目录下的，避免再使用网络下载比较费时间安装 ./install.sh lnmp单服务高并发配置文件说明本文件夹内的配置文件在服务器24 vCPU 96 GiB 配置的服务器可以直接使用，2000个并服务器主要修改的内容有 ## 打开文件数量 ulimit -a ulimit -n 65536 #永久修改 #修改文件cat /etc/security/limits.conf,这个值可以改大点，不会影响什么 # End of file root soft nofile 655350 root hard nofile 655350 * soft nofile 655350 * hard nofile 655350 # TCP最大连接数 cat /proc/sys/net/core/somaxconn echo 20000 > /proc/sys/net/core/somaxconn ##TCP连接立即回收、回用 cat /proc/sys/net/ipv4/tcp_tw_reuse ##我的结果是1，不需要改了 cat /proc/sys/net/ipv4/tcp_tw_recycle ##我的结果是1，不需要改了 echo 1 > /proc/sys/net/ipv4/tcp_tw_reuse echo 1 > /proc/sys/net/ipv4/tcp_tw_recycle cat /proc/sys/net/ipv4/tcp_syncookies ##我的结果是1 echo 0 > /proc/sys/net/ipv4/tcp_syncookies ## 屏蔽TCP异步Nginx配置信息user www www; worker_processes auto; worker_cpu_affinity auto; error_log /vdb1/wwwlogs/nginx_error.log crit; pid /usr/local/nginx/logs/nginx.pid; #Specifies the value for maximum file descriptors that can be opened by this process. worker_rlimit_nofile 51200; events { use epoll; worker_connections 51200; multi_accept off; accept_mutex off; } http { include mime.types; default_type application/octet-stream; server_names_hash_bucket_size 128; client_header_buffer_size 32k; large_client_header_buffers 4 32k; client_max_body_size 50m; sendfile on; sendfile_max_chunk 512k; tcp_nopush on; #keepalive_timeout 60; keepalive_timeout 0; tcp_nodelay on; fastcgi_connect_timeout 300; fastcgi_send_timeout 300; fastcgi_read_timeout 300; fastcgi_buffer_size 64k; fastcgi_buffers 4 64k; fastcgi_busy_buffers_size 128k; fastcgi_temp_file_write_size 256k; gzip on; gzip_min_length 1k; gzip_buffers 4 16k; gzip_http_version 1.1; gzip_comp_level 2; gzip_types text/plain application/javascript application/x-javascript text/javascript text/css application/xml application/xml+rss; gzip_vary on; gzip_proxied expired no-cache no-store private auth; gzip_disable "MSIE [1-6]\."; #limit_conn_zone $binary_remote_addr zone=perip:10m; ##If enable limit_conn_zone,add "limit_conn perip 10;" to server section. server_tokens off; access_log off; server { listen 80 default_server reuseport; #listen [::]:80 default_server ipv6only=on; server_name _; index index.html index.htm index.php; root /vdb1/www/default; #error_page 404 /404.html; # Deny access to PHP files in specific directory #location ~ /(wp-content|uploads|wp-includes|images)/.*\.php$ { deny all; } include enable-php.conf; location /nginx_status { stub_status on; access_log off; } location ~ .*\.(gif|jpg|jpeg|png|bmp|swf)$ { expires 30d; } location ~ .*\.(js|css)?$ { expires 12h; } location ~ /.well-known { allow all; } location ~ /\. { deny all; } access_log /vdb1/wwwlogs/access.log; } include vhost/*.conf; }Php-fmp配置文件[global] pid = /usr/local/php/var/run/php-fpm.pid error_log = /usr/local/php/var/log/php-fpm.log log_level = notice [www] listen = /tmp/php-cgi.sock listen.backlog = -1 listen.allowed_clients = 127.0.0.1 listen.owner = www listen.group = www listen.mode = 0666 user = www group = www ;pm = dynamic pm = static ;pm.max_children = 40 pm.max_children = 3000 pm.start_servers = 20 pm.min_spare_servers = 20 pm.max_spare_servers = 40 pm.max_requests = 1024 pm.process_idle_timeout = 10s request_terminate_timeout = 100 request_slowlog_timeout = 0 slowlog = var/log/slow.log安装Redis拓展在一键安装包的目录下输入一下命令./addons.sh 输入指定的拓展安装即可#重启redis服务 service redis restart
基础的电工知识人体安全电压：36V以下安全跳闸电流: 30MA电工设备软件：天空直通车对电要有敬畏之心，不要用手去触碰。125mm 能拉进去触电，不要用手摸单相电流的计算公式：电流 = 功率/电压 例如：2.6KW 220v电压，电流=2.6*1000 / 220 = 11.8三相电流的计算公式： 电流=功率/电压/1.732/0.8（无功） 例如：18KW 380v电压，电流=18*1000 / 380 / 1.732 / 0.8 = 11.8 = 34生活中常用负载设备快捷计算电流：功率kw*2，例如：15KW的设备，30A
acme.sh 实现了 acme 协议，可以从 ZeroSSL，Let's Encrypt 等 CA 生成免费的证书1. 安装 acme.sh安装很简单，一条命令:curl https://get.acme.sh | sh -s email=my@example.com或者wget -O - https://get.acme.sh | sh -s email=my@example.com普通用户和 root 用户都可以安装使用。安装过程进行了以下几步:把 acme.sh 安装到你的 home 目录下:~/.acme.sh/并创建 一个 shell 的 alias，例如 .bashrc，方便你的使用: alias acme.sh=~/.acme.sh/acme.sh自动为你创建 cronjob， 每天 0:00 点自动检测所有的证书，如果快过期了，需要更新，则会自动更新证书。更高级的安装选项请参考: https://github.com/acmesh-official/acme.sh/wiki/How-to-install安装过程不会污染已有的系统任何功能和文件，所有的修改都限制在安装目录中: ~/.acme.sh/注意：如果安装完成后提示 -bash: acme.sh: command not found，需要手动执行 source ~/.bashrc2. 生成证书acme.sh 实现了 acme 协议支持的所有验证协议。一般有两种方式验证: HTTP 和 DNS 验证。下面我就用http验证来实现，DNS请点击参考：https://github.com/acmesh-official/acme.sh/wiki/%E8%AF%B4%E6%98%8E#dns-%E9%AA%8C%E8%AF%81HTTP 验证这里优秀修改默认 CAacme.sh 脚本默认 CA 服务器是 ZeroSSL，有时可能会导致获取证书的时候一直出现：Pending，The CA is processing your order，please just wait.只需要把 CA 服务器改成 Let's Encrypt 即可，虽然更改以后还是有概率出现 pending，但基本 2-3 次即可成功acme.sh --set-default-ca --server letsencrypt更高级的用法请参考: https://github.com/acmesh-official/acme.sh/wiki/How-to-issue-a-cert直接签发只需要指定域名，并指定域名所在的网站根目录. acme.sh 会全自动的生成验证文件，并放到网站的根目录，验证完成后会聪明的删除验证文件，整个过程没有任何副作用。acme.sh --issue -d mydomain.com -d www.mydomain.com --webroot /home/wwwroot/mydomain.com/使用 Apache 模式如果你用的 Apache 服务器，acme.sh 还可以智能的从 Apache 的配置中自动完成验证，你不需要指定网站根目录:acme.sh --issue --apache -d example.com -d www.example.com -d cp.example.com使用 Nginx 模式如果你用的 Nginx 服务器，或者反代，acme.sh 还可以智能的从 Nginx 的配置中自动完成验证，你不需要指定网站根目录:acme.sh --issue --nginx -d example.com -d www.example.com -d cp.example.com注意，无论是 Apache 还是 Nginx 模式，acme.sh 在完成验证之后，会恢复到之前的状态，都不会私自更改程序本身的配置. 好处是你不用担心配置被搞坏，也有一个缺点，你需要自己配置 SSL 项，否则只能成功生成证书，你的网站还是无法正常使用 HTTPS。使用独立服务模式如果服务器上没有运行任何 Web 服务，80 端口是空闲的，那么 acme.sh 还能假装自己是一个 WebServer，临时监听 80 端口，完成验证:acme.sh --issue --standalone -d example.com -d www.example.com -d cp.example.com3. 复制证书证书生成好以后，我们需要把证书复制给对应的 Apache、Nginx 或其他服务器去使用，记得再Apache或Nginx中先开启ssl配置。必须使用 --install-cert 命令来把证书复制到目标文件，请勿直接使用 ~/.acme.sh/ 目录下的证书文件，这里面的文件都是内部使用，而且目录结构将来可能会变化。Apache 示例:acme.sh --install-cert -d example.com \ --cert-file /path/to/certfile/in/apache/cert.pem \ --key-file /path/to/keyfile/in/apache/key.pem \ --fullchain-file /path/to/fullchain/certfile/apache/fullchain.pem \ --reloadcmd "service apache2 force-reload" #重启apache服务Nginx 示例:acme.sh --install-cert -d example.com \ --key-file /path/to/keyfile/in/nginx/key.pem \ --fullchain-file /path/to/fullchain/nginx/cert.pem \ --reloadcmd "nginx -s reload" #重启nginx服务Nginx 的配置项 ssl_certificate 需要使用 /etc/nginx/ssl/fullchain.cer ，而非 /etc/nginx/ssl/<domain>.cer ，否则 SSL Labs 的测试会报证书链问题（Chain issues Incomplete）。默认情况下，证书每 60 天更新一次（可自定义）。更新证书后，Apache 或者 Nginx 服务会通过 reloadcmd 传递的命令自动重载配置。注意：reloadcmd 非常重要。证书会自动申请续签，但是如果没有正确的 reloadcmd 命令，证书可能无法被重新应用到 Apache 或者 Nginx，因为配置没有被重载。4. 查看已安装证书信息acme.sh --info -d example.com会输出如下内容：DOMAIN_CONF=/root/.acme.sh/example.com/example.com.conf Le_Domain=example.com Le_Alt=no Le_Webroot=dns_ali Le_PreHook= Le_PostHook= Le_RenewHook= Le_API=https://acme-v02.api.letsencrypt.org/directory Le_Keylength= Le_OrderFinalize=https://acme-v02.api.letsencrypt.org/acme/finalize/23xxxx150/781xxxx4310 Le_LinkOrder=https://acme-v02.api.letsencrypt.org/acme/order/233xxx150/781xxxx4310 Le_LinkCert=https://acme-v02.api.letsencrypt.org/acme/cert/04cbd28xxxxxx349ecaea8d07 Le_CertCreateTime=1649358725 Le_CertCreateTimeStr=Thu Apr 7 19:12:05 UTC 2022 Le_NextRenewTimeStr=Mon Jun 6 19:12:05 UTC 2022 Le_NextRenewTime=1654456325 Le_RealCertPath= Le_RealCACertPath= Le_RealKeyPath=/etc/acme/example.com/privkey.pem Le_ReloadCmd=service nginx force-reload Le_RealFullChainPath=/etc/acme/example.com/chain.pem5. 更新证书目前证书每 60 天自动更新，你无需任何操作。但是你也可以强制续签证书：acme.sh --renew -d example.com --force6. 关于修改 reloadcmd目前修改 reloadcmd 没有专门的命令，可以通过重新安装证书来实现修改 reloadcmd 的目的。此外，安装证书后，相关信息是保存在 ~/.acme.sh/example.com/example.conf 文件下的，内容就是 acme.sh --info -d example.com 输出的信息，不过 reloadcmd 在文件中使用了 Base64 编码。理论上可以通过直接修改该文件来修改 ReloadCmd，且修改时，无需 Base64 编码，直接写命令原文 acme.sh 也可以识别。不过，由于 example.conf 文件的位置和内容格式以后可能会改变，且 example.conf 一直都是内部使用，后续也有可能会改为用 SQLite 或者 MySQL 格式存储. 所以一般不建议自己修改。7. 更新 acme.shacmd.sh 还在不断开发中，因此强烈建议保持并使用最新的版本。升级 acme.sh 到最新版：acme.sh --upgrade如果你不想手动升级，可以开启自动升级:acme.sh --upgrade --auto-upgrade之后，acme.sh 就会自动保持更新了。你也可以随时关闭自动更新:acme.sh --upgrade --auto-upgrade 08. 出错怎么办：如果出错，请添加 --debug 参数输出详细的调试信息：acme.sh --issue ..... --debug 或者输出更详细的信息：acme.sh --issue ..... --debug 2请参考：https://github.com/acmesh-official/acme.sh/wiki/How-to-debug-acme.sh在 DNS 验证模式下如果 debug 中出现诸如 timed out 等字样可能是因为 GFW HTTP(S) Proxy 环境变量。（请按照自己实际设定修改）export http_proxy="socks5h://localhost:1081" export https_proxy="socks5h://localhost:1081" 如果是使用 docker 则完整示例配置如下：docker run --rm -it \ -v "/etc/acme":/acme.sh \ -e "CF_Token=[填入自己的信息]" \ -e "CF_Account_ID=[填入自己的信息]" \ -e "CF_Zone_ID=[填入自己的信息]" \ -e http_proxy="socks5h://[代理A]:1234" \ -e https_proxy="socks5h://[代理A]:1234" \ --network container:[代理A]\ neilpang/acme.sh \ --issue -d example.com --dns dns_cf --debug上述例子中使用 Cloudflare 的 DNS 来签发证书，并通过把 acme.sh 链接到容器[代理A]，来转发 curl 请求（请按照自己实际设定修改）最后，本文并非完全的使用说明，还有很多高级的功能，更高级的用法请参看 wiki 页面：https://github.com/acmesh-official/acme.sh/wiki/