<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0">
    <channel>
        <title><![CDATA[写代码的崔哥]]></title>
        <description><![CDATA[一名后端开发工程师，涉猎广泛：PHP，Golang，运维，前端，Android，iOS。会不定期给大家分享一些技术干货]]></description>
        <link>http://github.com/dylang/node-rss</link>
        <generator>cwf 1.0</generator>
        <lastBuildDate>Thu, 30 Apr 2026 01:00:26 GMT</lastBuildDate>
        <pubDate>Thu, 30 Apr 2026 01:00:25 GMT</pubDate>
        <language><![CDATA[zh-cn]]></language>
        <managingEditor><![CDATA[chudaozhe@outlook.com (cw)]]></managingEditor>
        <webMaster><![CDATA[chudaozhe@outlook.com (cw)]]></webMaster>
        <docs>https://www.rssboard.org/rss-specification</docs>
        <item>
            <title><![CDATA[You may not include more than 128 tools in your request.]]></title>
            <description><![CDATA[<link rel="stylesheet" href="https://www.cuiwei.net/static/css/github-markdown.min.css?t=20260430" type="text/css" media="screen" /><div class="markdown-body"><h2>问题</h2>
<p>使用<code>VS Code</code> Agent 模式时，它会自动发现其他 MCP 客户端中定义的 MCP 服务器，(例如 Claude Desktop、Cursor、Windsurf）。这样 MCP 工具数量很容易超过 128 个，如何禁用自动发现呢？</p>
<h2>解决办法</h2>
<ul>
<li>将<code>chat.mcp.discovery.enabled</code>设置为 <code>false</code></li>
</ul>
<p>设置中搜索“chat.mcp.discovery.enabled”，或编辑文件 <code>~/Library/Application\ Support/Code/User/settings.json</code></p>
<p><img src="https://www.cuiwei.net/storage/uploads/2025-08-13/175509704438574.jpg" alt="image.png" /></p>
<h2>参考</h2>
<p><a href="https://code.visualstudio.com/docs/copilot/chat/mcp-servers#_add-an-mcp-server">https://code.visualstudio.com/docs/copilot/chat/mcp-servers#_add-an-mcp-server</a></p></div>]]></description>
            <guid isPermaLink="false">You may not include more than 128 tools in your request.</guid>
        </item>
        <item>
            <title><![CDATA[laravel11 部署 Reverb - laravel 第一方可扩展的 WebSocket 服务器]]></title>
            <description><![CDATA[<link rel="stylesheet" href="https://www.cuiwei.net/static/css/github-markdown.min.css?t=20260430" type="text/css" media="screen" /><div class="markdown-body"><p>Laravel 11 发布了，一并推出了第一方可扩展的 WebSocket 服务器：Laravel Reverb，为你的应用提供强大的实时功能。</p>
<p>Reverb 只支持Laravel 10和Laravel 11，下面以Laravel 11为例</p>
<h2>安装</h2>
<pre><code>php artisan install:broadcasting</code></pre>
<p>env 配置</p>
<pre><code>REVERB_APP_ID=991168
REVERB_APP_KEY=cwnysrqcajxago4kbhw3
REVERB_APP_SECRET=q8kagq5unvvneork62yl
REVERB_HOST="laravel.cw.net"
REVERB_PORT=9502
REVERB_SCHEME=http</code></pre>
<h2>运行</h2>
<pre><code>php artisan reverb:start
//或
php artisan reverb:start --port=9502
//或
php artisan reverb:start --debug</code></pre>
<h2>简单使用</h2>
<pre><code>//创建广播事件
root@php-fpm:/var/www/example-app# php artisan make:event MessageCreated

//运行reverb
root@php-fpm:/var/www/example-app# php artisan reverb:start --port=9502

//监听队列
root@php-fpm:/var/www/example-app# php artisan queue:listen

//触发事件
root@php-fpm:/var/www/example-app# php artisan tinker
Psy Shell v0.12.2 (PHP 8.2.17 — cli) by Justin Hileman
&gt; event (new \App\Events\MessageCreated('test'))
= []

//网页查看效果</code></pre>
<p>更多请查看： <a href="https://www.cuiwei.net/p/1422053619">https://www.cuiwei.net/p/1422053619</a></p>
<h2>参考</h2>
<p><a href="https://laravel.com/docs/11.x/reverb">https://laravel.com/docs/11.x/reverb</a></p></div>]]></description>
            <guid isPermaLink="false">laravel11 部署 Reverb - laravel 第一方可扩展的 WebSocket 服务器</guid>
        </item>
        <item>
            <title><![CDATA[laravel 集成 vue3 的前端项目]]></title>
            <description><![CDATA[<link rel="stylesheet" href="https://www.cuiwei.net/static/css/github-markdown.min.css?t=20260430" type="text/css" media="screen" /><div class="markdown-body"><p>现在大多项目都是前后端分离的，但是如果前后端都是一个人做，前后端来回切也挺不方便的。</p>
<p>好在 laravel 给我们提供了 Vite，下面我以一个纯前端的项目<a href="https://github.com/chudaozhe/enterprise-admin">企业展示型小程序 - 管理员端</a> 为例，介绍一下如何把 vue3 项目集成到 laravel</p>
<h2>准备</h2>
<h3>创建一个laravel的项目</h3>
<pre><code>composer create-project laravel/laravel=10.* --prefer-dist laravel-demo</code></pre>
<p>大概步骤</p>
<pre><code>cd laravel-demo
composer install
cp .env.example .env
php artisan key:generate
npm install
npm run dev (or if production npm run build)</code></pre>
<p>在运行 Vite 和 Laravel 插件之前，你必须确保已安装 Node.js（16+）和 NPM：</p>
<pre><code>node -v</code></pre>
<h2>配置 Vite &amp; Vue</h2>
<p>vite.config.js 配置文件</p>
<pre><code>import { defineConfig } from 'vite';
import laravel from 'laravel-vite-plugin';

export default defineConfig({
    plugins: [
        laravel({
            input: ['resources/css/app.css', 'resources/js/main.js'],
            refresh: true,
        }),
    ],
    resolve: {
        alias: {
            '@': '/resources/js'
        }
    },
});</code></pre>
<h3>运行 Vite</h3>
<pre><code># Run the Vite development server...
npm run dev

# Build and version the assets for production...
npm run build</code></pre>
<h3>Vue</h3>
<p>如果你想要使用 Vue 框架构建前端，那么你还需要安装 @vitejs/plugin-vue 插件：</p>
<pre><code>npm install --save-dev @vitejs/plugin-vue</code></pre>
<p>修改 vite.config.js</p>
<pre><code>import { defineConfig } from 'vite';
import laravel from 'laravel-vite-plugin';
import vue from '@vitejs/plugin-vue';

export default defineConfig({
    plugins: [
        laravel(['resources/css/app.css', 'resources/js/main.js']),
        vue({
            template: {
                transformAssetUrls: {
                    // Vue 插件会重新编写资产 URL，以便在单文件组件中引用时，指向 Laravel web 服务器。
                    // 将其设置为 `null`，则 Laravel 插件会将资产 URL 重新编写为指向 Vite 服务器。
                    base: null,

                    // Vue 插件将解析绝对 URL 并将其视为磁盘上文件的绝对路径。
                    // 将其设置为 `false`，将保留绝对 URL 不变，以便可以像预期那样引用公共目录中的资源。
                    includeAbsolute: false,
                },
            },
        }),
    ],
    resolve: {
        alias: {
            '@': fileURLToPath(new URL('./resources/js', import.meta.url))
        }
    },
});</code></pre>
<h2>集成 Vue3 项目</h2>
<h3>web路由</h3>
<p>vi routes/web.php</p>
<pre><code>Route::get('{path}', function () {
    return view('spa');
})-&gt;where('path', '(.*)');</code></pre>
<h3>模板文件</h3>
<p>vi resources/views/spa.blade.php</p>
<pre><code>&lt;!DOCTYPE html&gt;
&lt;html lang="{{ str_replace('_', '-', app()-&gt;getLocale()) }}"&gt;
&lt;head&gt;
    &lt;meta charset="utf-8"&gt;
    &lt;meta name="viewport" content="width=device-width, initial-scale=1"&gt;

    &lt;title&gt;管理后台&lt;/title&gt;
{{-- 注意：vite.config.js 中的路径也要修改: plugins[laravel({input: ['resources/js/main.js']})]--}}
    @vite('resources/js/main.js')

&lt;/head&gt;
&lt;body&gt;
&lt;div id="app"&gt;&lt;/div&gt;
&lt;/body&gt;
&lt;/html&gt;</code></pre>
<h3>复制前端项目</h3>
<p>把前端项目克隆到<code>enterprise-admin</code>目录，把相关文件复制到laravel-demo下面（一些参数不能覆盖，需要合并一下）</p>
<ul>
<li>enterprise-admin/src/<em> -&gt; laravel-demo/resources/js/</em></li>
<li>enterprise-admin/package.json -&gt; laravel-demo/package.json</li>
<li>enterprise-admin/.env.development -&gt; laravel-demo/.env</li>
<li>enterprise-admin/vite.config.js -&gt; laravel-demo/vite.config.js</li>
</ul>
<p>安装依赖并运行</p>
<pre><code>npm install
npm run dev / npm run build</code></pre>
<p>最后访问访问laravel项目的域名就可以访问页面了，如：<code>http://newblog.cw.net</code>，即<code>.env</code>中的<code>APP_URL</code> </p>
<h2>参考</h2>
<p><a href="https://learnku.com/docs/laravel/10.x/vite/14853">https://learnku.com/docs/laravel/10.x/vite/14853</a></p>
<p><a href="https://github.com/gdarko/laravel-vue-starter">https://github.com/gdarko/laravel-vue-starter</a></p></div>]]></description>
            <guid isPermaLink="false">laravel 集成 vue3 的前端项目</guid>
        </item>
        <item>
            <title><![CDATA[如何使用大模型]]></title>
            <description><![CDATA[<link rel="stylesheet" href="https://www.cuiwei.net/static/css/github-markdown.min.css?t=20260430" type="text/css" media="screen" /><div class="markdown-body"><h2>开源</h2>
<ul>
<li><a href="https://ollama.com/">Ollama</a></li>
<li><a href="https://github.com/oobabooga/text-generation-webui">Text generation web UI</a></li>
<li><a href="https://github.com/lm-sys/FastChat">FastChat</a></li>
</ul>
<h2>平台</h2>
<ul>
<li><a href="https://replicate.ai/">Replicate</a></li>
<li><a href="https://api.together.xyz/">Together</a></li>
<li><a href="https://www.runpod.io/">RunPod</a></li>
<li><a href="https://www.lepton.ai/">Lepton AI</a></li>
</ul></div>]]></description>
            <guid isPermaLink="false">如何使用大模型</guid>
        </item>
        <item>
            <title><![CDATA[GPT-SoVITS - 1分钟人声样本，完成声音克隆]]></title>
            <description><![CDATA[<link rel="stylesheet" href="https://www.cuiwei.net/static/css/github-markdown.min.css?t=20260430" type="text/css" media="screen" /><div class="markdown-body"><h2>简介</h2>
<p>GPT-SoVITS - 1分钟的语音数据也可以用来训练一个好的TTS模型！</p>
<p>集成了语音伴奏分离、训练集自动分割、中文ASR、文本标注等工具，帮助初学者创建训练数据集和GPT/SoVITS模型。</p>
<h2>部署</h2>
<p>直接用<a href="https://github.com/RVC-Boss/GPT-SoVITS">GPT-SoVITS</a>仓库下的<code>docker-compose.yaml</code>即可</p>
<h2>准备</h2>
<p>准备一个3，5分钟的音频，1分钟也行，我用了一个10几分钟的。</p>
<h2>训练</h2>
<ul>
<li>9874：GPT-SoVITS WebUI，主界面</li>
<li>9873：UVR5-WebUI，人声/伴奏分离和混响去除</li>
<li>9872：语音合成（推理），最终的使用模型</li>
<li>9871：校对工具，音频切片后的校对</li>
<li>9880: api接口</li>
</ul>
<p>服务启动后，即可访问<a href="http://localhost:9874/">程序主界面</a> ，会看到<code>打开 UVR5-WebUI</code>，点击打开，然后就能访问 <a href="http://localhost:9873/">UVR5-WebUI</a>，在这个页面，上传你准备的音频文件，其中模型选择<code>HP2_all_vocals</code>，最后就可以执行了，成功后，在<code>output/uvr5_opt</code>目录会生成两个文件，其中<code>vocal_</code>开头的是纯净的人声文件，下一步会用到</p>
<p><strong>下一步</strong>，音频切片器：将上一步得到的<code>vocal_</code>开头的文件所在目录（其他文件删掉，或把该文件复制到一个新文件夹）的路径添加到<code>音频切片器输入（文件或文件夹）</code>，我这里是<code>/workspace/output/uvr5_opt</code>，然后点击<code>启动音频切片器</code>，成功后会在<code>/workspace/output/slicer_opt</code>目录产生分离后的小文件</p>
<p><strong>接着</strong>，语音降噪工具：忽略</p>
<p><strong>继续</strong>，中文ASR工具：在<code>输入文件夹路径</code>填写上一步得到的小文件目录<code>/workspace/output/slicer_opt</code>，然后点击<code>启动批处理 ASR</code>，成功后会产生一个list文件<code>/workspace/output/asr_opt/slicer_opt.list</code>。注意，这一步会下载一个模型，速度很慢，实在不行就手动执行框框中提示的命令。</p>
<p><strong>继续</strong>，启用语音转文本校对工具：在<code>.list 批注文件路径</code>输入<code>/workspace/output/asr_opt/slicer_opt.list</code>，然后点击<code>开放标签 WebUI</code>，就可以访问<a href="http://localhost:9871/">校对工具</a>了。</p>
<p><strong>继续</strong>，语音转文本校对工具：在这个页面可以检查一下系统生成的文本，标点符号等是否正确，不正确就修改一下。也可以进行合并，拆分等。这里我跳过。</p>
<p><strong>继续</strong>，我们回到主页面，点击第二个tab <code>1-GPT-SOVITS-TTS</code>，填写<code>实验/模型名称</code>，要英文的；</p>
<p>然后看到下面有3个tab，先看第一个<code>1A-数据集格式</code>：填写<code>文字标签文件</code>，就是list文件路径<code>/workspace/output/asr_opt/slicer_opt.list</code>，然后<code>音频数据集文件夹</code>，就是分割后的小文件目录<code>/workspace/output/slicer_opt</code>，接着点击<code>开始一键格式化</code>。</p>
<p><strong>接着</strong>，第二个tab <code>1B-微调训练</code>：训练SoVITS_weights模型，其中参数<code>每个 GPU 的批处理大小</code>和<code>总纪元数</code>要根据自己GPU的性能进行调整，<code>总纪元数</code>越大越好，约耗时间，当然也别成百上千，没必要。我填的25，最后点击<code>开始SoVITS培训</code>。</p>
<p><strong>接着</strong>，训练GPT_weights模型，参数都模型，直接点击<code>开始 GPT 训练</code></p>
<p><strong>接着</strong>，第三个tab，<code>1C-推理</code>，点击<code>刷新模型路径</code>，选择刚训练的模型，然后点击<code>开放TTS推理WEBUI</code>，就可以访问<a href="http://localhost:9872/">语音合成（推理）</a></p>
<p><strong>最后</strong>，先上传<code>参考音频文件</code>，再添加对应的文本，为了省事，我们可以上传一个分割后的小音频文件。然后填写<code>推理文本</code>，就是你要合成语音的文本，最后点击<code>开始推理</code>就能合成了。</p>
<h2>模型分享</h2>
<p>分享需要的模型都在SoVITS_weights和GPT_weights这两个文件夹，选择合适轮数的模型，记得带上参考音频一起打包成压缩文件，就可以分享了。别人只要将GPT模型（ckpt后缀）放入GPT_weights文件夹，SoVITS模型（pth后缀）放入SoVITS_weights文件夹就可以推理了</p>
<p>以为的模型为例</p>
<pre><code>- /workspace/GPT_weights/yangmi-e15.ckpt
- /workspace/SoVITS_weights/yangmi_e24_s1344.pth

- /workspace/output/slicer_opt/vocal_yangmi.WAV_10.flac_0000000000_0000135040.wav
- 音频对应的文本</code></pre>
<p>将以上4部分打包就可以分享了</p>
<p>这里有作者分享的一些模型： <a href="https://www.yuque.com/baicaigongchang1145haoyuangong/ib3g1e/nwnaga50cazb2v93">https://www.yuque.com/baicaigongchang1145haoyuangong/ib3g1e/nwnaga50cazb2v93</a></p>
<h2>API调用</h2>
<h3>启动服务</h3>
<p>执行参数</p>
<ul>
<li><code>-s</code>：SoVITS模型路径</li>
<li><code>-g</code>：GPT模型路径</li>
<li><code>-dr</code>：默认参考音频路径</li>
<li><code>-dt</code>：默认参考音频文本</li>
<li><code>-dl</code>：默认参考音频语种，<code>//中文,英文,日文,zh,en,ja</code></li>
</ul>
<pre><code>python api.py -dr "/workspace/output/slicer_opt/vocal_yangmi.WAV_10.flac_0000000000_0000135040.wav" -dt "我觉得有那些角色在哪儿，是因为我运气好。" -dl "zh" -s "/workspace/SoVITS_weights/yangmi_e24_s1344.pth" -g "/workspace/GPT_weights/yangmi-e15.ckpt"</code></pre>
<p>文本转语音</p>
<pre><code>curl --location 'http://localhost:9880' \
--header 'Content-Type: application/json' \
--data '{
    "refer_wav_path": "/workspace/output/slicer_opt/vocal_yangmi.WAV_10.flac_0000000000_0000135040.wav",
    "prompt_text": "我觉得有那些角色在哪儿，是因为我运气好。",
    "prompt_language": "zh",
    "text": "今天我吃了两个包子，一个鸡蛋，还有一杯豆浆。",
    "text_language": "zh"
}'</code></pre>
<h2>参考</h2>
<p><a href="https://github.com/RVC-Boss/GPT-SoVITS">https://github.com/RVC-Boss/GPT-SoVITS</a></p></div>]]></description>
            <guid isPermaLink="false">GPT-SoVITS - 1分钟人声样本，完成声音克隆</guid>
        </item>
        <item>
            <title><![CDATA[Stable Diffusion 艺术二维码]]></title>
            <description><![CDATA[<link rel="stylesheet" href="https://www.cuiwei.net/static/css/github-markdown.min.css?t=20260430" type="text/css" media="screen" /><div class="markdown-body"><h2>模型</h2>
<ul>
<li>大模型：sd-v1.5</li>
<li>LORA模型：<a href="https://www.liblib.art/modelinfo/76af487591f94da29ed8cb3f9d0cd660">诗意国潮插画_v1.safetensors</a></li>
<li>ControlNet模型：<a href="https://huggingface.co/monster-labs/control_v1p_sd15_qrcode_monster">control_v1p_sd15_qrcode_monster.safetensors</a></li>
<li>ControlNet模型：<a href="https://civitai.com/models/90940/controlnet-qr-pattern-qr-codes">controlnetQRPatternQR_v2Sd15.safetensors</a></li>
<li>ControlNet模型：<a href="https://huggingface.co/ioclab/ioc-controlnet">control_v1p_sd15_brightness.safetensors</a>：调节亮度</li>
<li>二维码生成工具：<a href="https://github.com/antfu/sd-webui-qrcode-toolkit">sd-webui-qrcode-toolkit</a>，据说是可以生成各式各样的二维码，最后还能把二维码发送的文生图tab页，但我这边测试，是发送失败了。如果接受手动保持二维码，直接访问他官网也是一样的。 <a href="https://qrcode.antfu.me/">https://qrcode.antfu.me/</a></li>
</ul>
<h2>使用</h2>
<p><code>control_v1p_sd15_qrcode_monster.safetensors</code> 或 <code>controlnetQRPatternQR_v2Sd15.safetensors</code> 都可以，然后配合<code>control_v1p_sd15_brightness.safetensors</code>调节亮度</p>
<p>下面以<code>control_v1p_sd15_qrcode_monster.safetensors</code>和<code>control_v1p_sd15_brightness.safetensors</code>为例</p>
<p>提示词: 在<code>诗意国潮插画</code>详情页找个好看的图片，复制其提示词，然后粘贴到sd，并填充参数</p>
<pre><code>(((masterpiece,best quality))),illustration,meticulous painting,parameters,1girl,flower,cloud,&lt;lora:诗意国潮插画:0.9&gt;,
Negative prompt: (worst quality:2),(low quality:2),(normal quality:2),((monochrome)),((grayscale)),skin spots,acnes,skin blemishes,age spot,(ugly:1.3),(duplicate:1.3),(morbid:1.2),(mutilated:1.2),(tranny:1.3),mutated hands,(poorly drawn hands:1.5),blurry,(bad anatomy:1.2),(bad proportions:1.3),(disfigured:1.3),(missing arms:1.3),(extra legs:1.3),(fused fingers:1.6),(too many fingers:1.6),(unclear eyes:1.3),lowers,bad hands,missing fingers,extra digit,bad hands,missing fingers,(((extra arms and legs))),(fuze:1.4),(fuze:1.4),easynegative,ng_deepnegative_v1_75t,shadow,Shadow on face:2,(((watermark:2))),text,error,missing fingers,missing arms,missing legs,extra digit,extra fingers,fewer digits,extra limbs,extra arms,extra legs,malformed limbs,fused fingers,too many fingers,long neck,cross-eyed,mutated hands,cropped,poorly drawn hands,poorly drawn face,mutation,deformed,blurry,ugly,duplicate,morbid,mutilated,out of frame,body out of frame,bad-hands-5,badhandv4,bad-artist,bad-artist-anime,bad_prompt_version2,negative_hand-neg,
Steps: 30, Size: 1920x1024, Seed: 3886657620, Sampler: Euler a, CFG scale: 5</code></pre>
<h3>注意</h3>
<p>1、如果生成出来的图片，无法扫码识别，可以尝试调节以下参数
增大QR Pattern/QR Code Monster的权重
增加brightness控制器</p>
<p>2、如果图片中二维码的像素点痕迹太严重了
减小QR Pattern/QR Code Monster的权重
减小brightness控制器的权重</p>
<h2>详细参数截图</h2>
<p><img src="https://www.cuiwei.net/data/upload/2024-03-09/170999260889414.jpg" alt="Screenshot_20240309_133432.png" /></p>
<p>第二个ControlNet 设置</p>
<p><img src="https://www.cuiwei.net/data/upload/2024-03-09/170999261750165.jpg" alt="Screenshot_20240309_133529.png" /></p>
<h2>参考</h2>
<p><a href="https://blog.csdn.net/sinat_29957455/article/details/131755232">https://blog.csdn.net/sinat_29957455/article/details/131755232</a></p></div>]]></description>
            <guid isPermaLink="false">Stable Diffusion 艺术二维码</guid>
        </item>
        <item>
            <title><![CDATA[Stable Diffusion 姓氏头像]]></title>
            <description><![CDATA[<link rel="stylesheet" href="https://www.cuiwei.net/static/css/github-markdown.min.css?t=20260430" type="text/css" media="screen" /><div class="markdown-body"><h2>准备</h2>
<ul>
<li>大模型：<a href="https://www.liblib.art/modelinfo/8869405b339541f2b3aa90cfb697e50f">MR 3DQ _1.5版本 V2.safetensors</a></li>
<li>LORA模型：<a href="https://www.liblib.art/modelinfo/67eccc6bfe224492851283497911a140">【萌宝寻龙】新年IP _ 百变萌宠龙宝宝_VL1.0.safetensors</a></li>
<li>ControlNet模型：<a href="https://huggingface.co/monster-labs/control_v1p_sd15_qrcode_monster">control_v1p_sd15_qrcode_monster.safetensors</a></li>
<li>升频器（放大图片）：据<a href="https://civitai.com/models/116225/4x-ultrasharp">C站</a>介绍,这才是作者的<a href="https://huggingface.co/Kim2091/UltraSharp">Hugging Face主页</a>，据<a href="https://upscale.wiki/w/index.php?title=Model_Database&amp;oldid=1571">upscale.wiki</a>介绍，它对JPG图片支持更好。下载后放到<code>models\ESRGAN</code>目录</li>
</ul>
<h2>提示词</h2>
<p>首先在<code>【萌宝寻龙】新年IP | 百变萌宠龙宝宝</code>的下载页面，找一个好看的图片，并复制其提示词，如下</p>
<pre><code>red,cute pet,illustration cartoon cute art style,HD,body,antlers,soft art style,(masterpiece),original,rich details,extremely delicate,pink theme,&lt;lora:dragon:0.71&gt;,( chinese dragon:1.3),
Negative prompt: (extra arms and legs:2),(worst quality, low quality:1.4),(normal quality:2),(greyscale, monochrome:1.5),((grayscale:2)),cropped,lowres,text,(nsfw:1.3),(worst quality:2),(low quality:2),normal quality,skin spots,skin blemishes,(ugly:1.5),(duplicate:1.331),(morbid:1.5),(mutilated:1.21),(tranny:1.331),mutated hands,(poorly drawn hands:1.5),blurry,(bad anatomy:1.21),bad proportions,extra limbs,(missing arms:1.331),(extra legs:1.331),(fused fingers:1.61051),(too many fingers:1.61051),(unclear eyes:1.331),lowers,bad hands,missing fingers,extra digit,bad hands,
Steps: 33, Size: 1536x1536, Seed: 2037870272, Model: vae-84m-pruned 2.0, vae-ft-mse-840000-ema-pruned.safetensors vae-ft-mse-840000-ema-pruned.safetensors, MR 3DQ  1.5版本 V2, vae-84m-pruned 2.0, vae-ft-mse-840000-ema-pruned.safetensors vae-ft-mse-840000-ema-pruned.safetensors, MR 3DQ  1.5版本 V2, Sampler: DPM++ 2M Karras, CFG scale: 7</code></pre>
<p>然后粘贴到<code>Stable Diffusion</code>并展开填充。接着做些修改：</p>
<ul>
<li>
<p>大模型改为：<code>MR 3DQ _1.5版本 V2.safetensors</code></p>
</li>
<li>
<p>正向提示词修改：把<code>,pink theme,&lt;lora:dragon:0.71&gt;,( chinese dragon:1.3),</code>改为<code>pink theme, &lt;lora:【萌宝寻龙】新年IP _ 百变萌宠龙宝宝_VL1.0:0.8&gt;</code></p>
</li>
<li>
<p>Seed 固定：2037870272 (-1为随机)</p>
</li>
<li>
<p>宽高改为512</p>
</li>
<li>
<p><code>Hires. fix</code>下<code>Denoising strength</code> 改为<code>0.59</code></p>
</li>
<li>
<p><code>Hires. fix</code>下<code>Upscaler</code>的值改为<code>4x-UltraSharp</code></p>
</li>
<li>
<p><code>ControlNet</code>下上传一张黑字白底的图片</p>
</li>
<li>
<p><code>ControlNet</code>下<code>Preprocessor</code>的值改为<code>invert (from white bg &amp; black line)</code>，点击旁边的<code>爆炸</code>图标可以生成预览</p>
</li>
<li>
<p><code>ControlNet</code>下<code>Model</code>选择<code>control_v1p_sd15_qrcode_monster</code></p>
</li>
</ul>
<p><img src="https://www.cuiwei.net/data/upload/2024-03-09/170997995767955.jpg" alt="Screenshot_20240308_095229.png" /></p></div>]]></description>
            <guid isPermaLink="false">Stable Diffusion 姓氏头像</guid>
        </item>
        <item>
            <title><![CDATA[docker使用GPU]]></title>
            <description><![CDATA[<link rel="stylesheet" href="https://www.cuiwei.net/static/css/github-markdown.min.css?t=20260430" type="text/css" media="screen" /><div class="markdown-body"><p>只能使用支持 cuda 的 nvidia 显卡，其他不行😭</p>
<h2>docker run</h2>
<p><a href="https://docs.docker.com/config/containers/resource_constraints/#gpu">https://docs.docker.com/config/containers/resource_constraints/#gpu</a></p>
<p><code>--gpus all</code></p>
<pre><code>docker run -it --rm --gpus all nvidia/cuda:12.3.1-base-ubuntu20.04 nvidia-smi</code></pre>
<h2>docker compose</h2>
<p><a href="https://docs.docker.com/compose/gpu-support/">https://docs.docker.com/compose/gpu-support/</a></p>
<pre><code>services:
  test:
    image: nvidia/cuda:12.3.1-base-ubuntu20.04
    command: nvidia-smi
    deploy:
      resources:
        reservations:
          devices:
            - driver: nvidia
              count: 1
              capabilities: [gpu]</code></pre></div>]]></description>
            <guid isPermaLink="false">docker使用GPU</guid>
        </item>
        <item>
            <title><![CDATA[Docker 集群管理 - Swarm模式]]></title>
            <description><![CDATA[<link rel="stylesheet" href="https://www.cuiwei.net/static/css/github-markdown.min.css?t=20260430" type="text/css" media="screen" /><div class="markdown-body"><p>Docker Swarm和Docker Compose都是由Docker官方提供的容器编排工具。它们之间的区别在于，Docker Compose主要用于在单个服务器或主机上创建多个容器，而Docker Swarm则可以在多个服务器或主机上创建容器集群服务。特别是在微服务的部署场景下，Docker Swarm显然更为适用，因为它能够实现在分布式环境中轻松管理和扩展容器服务。</p>
<h2>准备</h2>
<h3>Swarm、Swarmkit和Swarm模式傻傻分不清</h3>
<p><a href="https://www.linuxprobe.com/swarm-swarmkit.html">https://www.linuxprobe.com/swarm-swarmkit.html</a></p>
<p><a href="https://sreeninet.wordpress.com/2016/07/14/comparing-swarm-swarmkit-and-swarm-mode/">https://sreeninet.wordpress.com/2016/07/14/comparing-swarm-swarmkit-and-swarm-mode/</a></p>
<h3>3台服务器</h3>
<table>
<thead>
<tr>
<th>节点</th>
<th>角色</th>
<th>IP</th>
</tr>
</thead>
<tbody>
<tr>
<td>node1</td>
<td>manager</td>
<td>192.168.0.13</td>
</tr>
<tr>
<td>node2</td>
<td>worker</td>
<td>192.168.0.12</td>
</tr>
<tr>
<td>node3</td>
<td>worker</td>
<td>192.168.0.11</td>
</tr>
</tbody>
</table>
<p>这里我们使用在线服务： <a href="https://labs.play-with-docker.com/">https://labs.play-with-docker.com/</a></p>
<h3>开放端口</h3>
<ul>
<li>Port 2377 TCP for communication with and between manager nodes</li>
<li>Port 7946 TCP/UDP for overlay network node discovery</li>
<li>Port 4789 UDP (configurable) for overlay network traffic</li>
</ul>
<h2>Manager 节点初始化</h2>
<p>查看docker engine是否已激活swarm模式</p>
<pre><code>docker info | grep Swarm</code></pre>
<p>初始化一个swarm集群</p>
<pre><code>[node1] (local) root@192.168.0.13 ~
$ docker swarm init --advertise-addr 192.168.0.13
Swarm initialized: current node (kmi05q0oc98hykhpjcd2kcbn2) is now a manager.

To add a worker to this swarm, run the following command:

    docker swarm join --token SWMTKN-1-0fta3d58gkcrqp7tf9u5lbhfr7cfilzzp875ixzxswlnep08pa-2hw90l3ykhsltn1hmv6s89ng2 192.168.0.13:2377

To add a manager to this swarm, run 'docker swarm join-token manager' and follow the instructions.

#再次查看token
docker swarm join-token manager

#禁用swarm模式
docker swarm leave --force</code></pre>
<h2>Node1</h2>
<p>加入一个已经存在的swarm集群</p>
<pre><code>docker swarm join --token SWMTKN-1-0fta3d58gkcrqp7tf9u5lbhfr7cfilzzp875ixzxswlnep08pa-2hw90l3ykhsltn1hmv6s89ng2 192.168.0.13:2377</code></pre>
<h2>Node2节点</h2>
<p>和node1节点的操作一致</p>
<h2>服务</h2>
<p>以下操作基于 manager 节点</p>
<pre><code>#发布一个服务到集群
docker service create --replicas 1 --name helloworld alpine ping docker.com

#服务列表
docker service ls

#查看服务运行在哪个节点
docker service ps helloworld

#服务详情
docker service inspect --pretty helloworld

#容器列表
docker ps

#扩容
docker service scale helloworld=5

#删除服务
docker service rm helloworld</code></pre>
<p>再发布一个服务到集群</p>
<pre><code>docker service create --replicas 3 --name redis --update-delay 10s redis:3.0.6</code></pre>
<p>将redis:3.0.6滚动升级为redis:3.0.7</p>
<pre><code>docker service update --image redis:3.0.7 redis</code></pre>
<h2>node</h2>
<pre><code>#节点列表
docker node ls

#清空一个节点
docker node update --availability drain node2

#节点详情
docker node inspect --pretty node2

#恢复一个节点
docker node update --availability active node2</code></pre>
<h2>使用compose文件</h2>
<pre><code>#将应用程序部署到 Swarm
docker stack deploy -c bb-stack.yaml demo

#列出服务
docker service ls

#移除服务
docker stack rm demo</code></pre>
<h3>Docker Compose文件在普通的Docker环境和Swarm模式下的主要区别</h3>
<ul>
<li>在Swarm模式下，Compose文件的服务定义可能包含更多的配置项，如replicas（副本数）和deploy（部署配置）等。这些配置项用于指定服务在Swarm集群中的运行方式。</li>
<li>在Swarm模式下，你可以使用配置对象来存储敏感信息，以便在服务中共享。这是Swarm模式中一个重要的安全特性。</li>
</ul>
<h2>参考</h2>
<p><a href="https://blog.csdn.net/u022812849/article/details/134006815">https://blog.csdn.net/u022812849/article/details/134006815</a></p>
<p><a href="https://docs.docker.com/engine/swarm/">https://docs.docker.com/engine/swarm/</a></p>
<p><a href="https://docs.docker.com/compose/compose-file/compose-file-v3/#deploy">https://docs.docker.com/compose/compose-file/compose-file-v3/#deploy</a></p></div>]]></description>
            <guid isPermaLink="false">Docker 集群管理 - Swarm模式</guid>
        </item>
        <item>
            <title><![CDATA[使用 Vault 管理数据库凭据和实现 AppRole 身份验证]]></title>
            <description><![CDATA[<link rel="stylesheet" href="https://www.cuiwei.net/static/css/github-markdown.min.css?t=20260430" type="text/css" media="screen" /><div class="markdown-body"><p>Vault 是一个开源工具，可以安全地存储和管理敏感数据，例如密码、API 密钥和证书。它使用强加密来保护数据，并提供多种身份验证方法来控制对数据的访问。Vault 可以部署在本地或云中，并可以通过 CLI、API 或 UI 进行管理。</p>
<p>本文将介绍 Vault 的初始化、数据库密钥引擎和身份验证方法。我们将首先介绍如何使用 UI、CLI 或 REST API 初始化 Vault。然后，我们将介绍如何使用 Vault 的数据库密钥引擎来管理数据库凭据。最后，我们将介绍如何使用 AppRole 身份验证方法来保护 Vault 中的数据。</p>
<h2>初始化</h2>
<pre><code>{
  "keys": [
    "cf145f5edb6f2dfff30d30ddc0f29f44eec2dee436b8850223df36345660bfe5"
  ],
  "keys_base64": [
    "zxRfXttvLf/zDTDdwPKfRO7C3uQ2uIUCI982NFZgv+U="
  ],
  "root_token": "hvs.PGd4sn4vh80aQIMA9R6CvOwe"
}</code></pre>
<p>共有以下3种方式</p>
<h3>UI界面的方式</h3>
<p>访问<code>https://vault.uqiantu.com</code>按照提示操作，最后保存json文件即可</p>
<h3>CLI的方式</h3>
<pre><code>/ # export VAULT_ADDR='http://127.0.0.1:8200'
/ # vault operator init -key-shares=1 -key-threshold=1
Unseal Key 1: A15zzLWHW18dXEGp3fEW9qUcoOmcjjInXESlS4RAB4w=

Initial Root Token: hvs.F98rg41VGnQFrqIggEjRxXfF

解封
/ # vault operator unseal A15zzLWHW18dXEGp3fEW9qUcoOmcjjInXESlS4RAB4w=

环境变量VAULT_TOKEN和vault login二选一
/ # export VAULT_TOKEN="hvs.F98rg41VGnQFrqIggEjRxXfF"
/ # vault login &lt;initial-root-token&gt;

/ # vault secrets enable -path=kv2 kv
/ # vault kv put -mount=kv2 hello foo=world</code></pre>
<h3>REST API 的方式</h3>
<p><a href="https://developer.hashicorp.com/vault/tutorials/getting-started/getting-started-apis">https://developer.hashicorp.com/vault/tutorials/getting-started/getting-started-apis</a></p>
<p>初始化</p>
<pre><code>curl \
    --request POST \
    --data '{"secret_shares": 1, "secret_threshold": 1}' \
    http://127.0.0.1:8200/v1/sys/init | jq</code></pre>
<p>解封</p>
<pre><code>curl \
    --request POST \
    --data '{"key": "{{keys_base64}}"}' \
    http://127.0.0.1:8200/v1/sys/unseal | jq</code></pre>
<p>启用kv引擎</p>
<pre><code>curl -X POST -H "X-Vault-Token: &lt;root-token&gt;" -d '{"type": "kv", "options": {"path": "kv2"}}' http://127.0.0.1:8200/v1/sys/mounts/kv2</code></pre>
<p>写一条数据</p>
<pre><code>curl -X POST -H "X-Vault-Token: &lt;root-token&gt;" -d '{"data": {"foo": "world"}}' http://127.0.0.1:8200/v1/kv2/hello</code></pre>
<p>验证初始化状态</p>
<pre><code>curl https://vault.uqiantu.com/v1/sys/init</code></pre>
<h2>数据库密钥引擎 - Mysql</h2>
<p><a href="https://developer.hashicorp.com/vault/docs/secrets/databases/mysql-maria#authenticating-to-cloud-dbs-via-iam">https://developer.hashicorp.com/vault/docs/secrets/databases/mysql-maria#authenticating-to-cloud-dbs-via-iam</a></p>
<p>支持的插件</p>
<ul>
<li>mysql-database-plugin</li>
<li>mysql-aurora-database-plugin</li>
<li>mysql-rds-database-plugin</li>
<li>mysql-legacy-database-plugin</li>
</ul>
<p>启用数据库密钥引擎</p>
<pre><code>/ # export VAULT_ADDR='http://127.0.0.1:8200'
/ # export VAULT_TOKEN="hvs.4LhxBdPNxOfgrmL7kFHUBBrx"
/ # vault secrets enable database</code></pre>
<p>创建连接</p>
<pre><code>vault write database/config/nextcloud \
    plugin_name=mysql-database-plugin \
    connection_url="{{username}}:{{password}}@tcp(docker-mysql:3306)/nextcloud?charset=utf8mb4&amp;parseTime=True&amp;loc=Local&amp;timeout=10ms" \
    root_rotation_statements="SET PASSWORD = PASSWORD('{{password}}')" \
    allowed_roles="role1,role2" \
    username="nextcloud" \
    password="nextcloud123"</code></pre>
<p>创建静态角色</p>
<pre><code>vault write database/static-roles/role1 \
    db_name=nextcloud \
    username="nextcloud" \
    rotation_period=86400</code></pre>
<p>创建动态角色</p>
<pre><code>vault write database/roles/role2 \
   db_name=nextcloud \
   creation_statements="CREATE USER '{{name}}'@'%' IDENTIFIED BY '{{password}}';GRANT SELECT ON *.* TO '{{name}}'@'%';" \
   revocation_statements="DROP USER '{{name}}'@'%';" \
   default_ttl="1h" \
   max_ttl="24h"</code></pre>
<p>动态角色查看密码（每次都会生成一对新的）</p>
<pre><code>/ # vault read database/creds/role2
Key                Value
---                -----
lease_id           database/creds/role2/eOpeXLZy6aOqUehZgVKBQjsT
lease_duration     1h
lease_renewable    true
password           XcCWxTi-Vs9NM-uxkh33
username           v-root-role2-dv19zfatqakhQ8NaPJD</code></pre>
<blockquote>
<p>静态角色的密码只能通过UI界面查看了</p>
</blockquote>
<h2>身份验证方法 - AppRole</h2>
<p><a href="https://developer.hashicorp.com/vault/docs/auth/approle">https://developer.hashicorp.com/vault/docs/auth/approle</a></p>
<p>登录（获取token）</p>
<pre><code>vault write auth/approle/login \
  role_id=bb871d16-adcb-257b-9599-513f8610eb62 \
  secret_id=37f8814f-8863-0139-48e5-01a9bd57ca0a</code></pre>
<p>启用身份验证方法 - AppRole</p>
<pre><code>/ # export VAULT_ADDR='http://127.0.0.1:8200'
/ # export VAULT_TOKEN="hvs.4LhxBdPNxOfgrmL7kFHUBBrx"
/ # vault auth enable approle</code></pre>
<p>创建角色</p>
<pre><code>vault write auth/approle/role/my-role \
    policies=my-role \
    secret_id_ttl=10m \
    token_num_uses=0 \
    token_ttl=20m \
    token_max_ttl=30m \
    secret_id_num_uses=0</code></pre>
<p>创建策略</p>
<pre><code>vault policy write my-role - &lt;&lt;EOF
path "secret/config" {
    capabilities = ["read"]
}

path "auth/*" {
    capabilities = ["create", "list", "read", "update"]
}
path "identity/*" {
    capabilities = ["create", "list", "read", "update"]
}

path "sys/mounts/*" {
    capabilities = ["create", "list", "read", "update"]
}

path "kv/*" {
    capabilities = ["create", "list", "read", "update"]
}
EOF
</code></pre>
<p>获取role-id</p>
<pre><code>vault read auth/approle/role/my-role/role-id</code></pre>
<p>获取secret-id</p>
<pre><code>vault write -f auth/approle/role/my-role/secret-id</code></pre>
<blockquote>
<p>注意：Secret ID是一个需要被保护的值</p>
</blockquote>
<pre><code>(https://learn.hashicorp.com/tutorials/vault/secure-introduction?in=vault/app-integration#trusted-orchestrator)
// give the app access to a short-lived response-wrapping token (https://developer.hashicorp.com/vault/docs/concepts/response-wrapping).
// Read more at: https://learn.hashicorp.com/tutorials/vault/approle-best-practices?in=vault/auth-methods#secretid-delivery-best-practices</code></pre></div>]]></description>
            <guid isPermaLink="false">使用 Vault 管理数据库凭据和实现 AppRole 身份验证</guid>
        </item>
        <item>
            <title><![CDATA[Avahi - 轻松实现局域网中的.local域名服务]]></title>
            <description><![CDATA[<link rel="stylesheet" href="https://www.cuiwei.net/static/css/github-markdown.min.css?t=20260430" type="text/css" media="screen" /><div class="markdown-body"><p>Avahi 是一个免费的零配置网络 （zeroconf） 实现，包括一个用于组播 DNS/DNS-SD 服务发现的系统。它允许程序发布和发现在本地网络上运行的服务和主机，而无需特定配置。比如，<code>traefik.local</code>、<code>homepage.local</code>就可以轻松实现。</p>
<p>苹果的Bonjour服务（mDNS）通过使用.local后缀，实现了多址广播域名的设备识别。</p>
<h2>安装 Avahi</h2>
<pre><code># Ubuntu / Debian
$ sudo apt install avahi-daemon avahi-utils

# CentOS
$ sudo yum install nss-mdns avahi avahi-tools

# Fedora 
$ sudo dnf install nss-mdns avahi avahi-tools</code></pre>
<p>如果提示 <code>nss-mdns</code> 找不到，就安装一下<code>epel</code>源1️⃣</p>
<p>开启服务</p>
<pre><code>systemctl restart avahi-daemon.service</code></pre>
<p>开启自启</p>
<pre><code>systemctl enable --now avahi-daemon.service</code></pre>
<h2>其他</h2>
<p>问题排查，执行<code>journalctl -u avahi-daemon</code>
如果看到</p>
<pre><code>WARNING: No NSS support for mDNS detected, consider installing nss-mdns!</code></pre>
<p>就是 nss-mdns 没安装</p>
<p>1️⃣设置yum源：epel源</p>
<pre><code>YUM
yum install epel-release
或者手动
rpm -ivh http://dl.fedoraproject.org/pub/epel/epel-release-latest-{x}.noarch.rpm

#更新下缓存
yum clean all &amp;&amp; yum makecache</code></pre>
<h2>参考</h2>
<p><a href="https://blog.csdn.net/easylife206/article/details/128795903">https://blog.csdn.net/easylife206/article/details/128795903</a></p>
<p><a href="https://avahi.org/">https://avahi.org/</a></p>
<p><a href="https://www.hardill.me.uk/wordpress/2020/10/05/traefik-avahi-helper/">https://www.hardill.me.uk/wordpress/2020/10/05/traefik-avahi-helper/</a></p>
<p><a href="https://www.hardill.me.uk/wordpress/2020/09/22/nginx-proxy-avahi-helper/">https://www.hardill.me.uk/wordpress/2020/09/22/nginx-proxy-avahi-helper/</a></p>
<p><a href="https://wiki.archlinux.org/title/Avahi">https://wiki.archlinux.org/title/Avahi</a></p>
<p><a href="https://www.bilibili.com/read/cv27392864/">https://www.bilibili.com/read/cv27392864/</a></p></div>]]></description>
            <guid isPermaLink="false">Avahi - 轻松实现局域网中的.local域名服务</guid>
        </item>
        <item>
            <title><![CDATA[Docker 部署 Mastodon - 一个去中心化的社交平台]]></title>
            <description><![CDATA[<link rel="stylesheet" href="https://www.cuiwei.net/static/css/github-markdown.min.css?t=20260430" type="text/css" media="screen" /><div class="markdown-body"><p>在当今互联网时代，社交媒体已经成为人们生活的重要一部分，然而，传统社交媒体平台通常集中于单一中央服务器，这引发了一些问题，包括隐私担忧、数据泄露风险以及广告和跟踪滥用。然而，现在有一种新的社交网络正在崭露头角，它将互联网的去中心化精神引入了社交媒体世界 - 那就是 Mastodon。</p>
<p>Mastodon 是一种开源、分布式的社交网络平台，以其强调去中心化、用户隐私和自主控制而引起了广泛的关注。与传统社交媒体巨头如 Twitter 和 Facebook 不同，Mastodon 的去中心化设计使其不依赖于单一中央服务器。相反，它由许多相互连接的服务器（或称为实例）组成，每个实例都是一个独立的社交网络社区，用户可以选择在其中注册。这意味着没有单一的权威机构掌握着所有用户数据，从而降低了个人隐私的风险，减少了数据泄露的可能性，并提供了更好的用户控制。</p>
<p>Mastodon 的开源性质也为用户提供了更多的透明度和参与机会。该平台的源代码是开放的，允许社区审查、修改和贡献，确保了平台的发展和改进不受单一实体的控制。此外，Mastodon 不包含广告，也不追踪用户的在线行为，从而提供了一个更加干净和隐私友好的社交媒体环境。</p>
<p>在本文中，我们将深入探讨如何使用 Mastodon，以及如何通过 Docker 轻松部署自己的 Mastodon 实例，让您能够体验到这一去中心化社交媒体平台的强大功能和优势。无论您是关心隐私和数据安全，还是寻求更好的社交媒体用户体验，Mastodon 都是一个备受欢迎的选择，它在社交媒体的未来中扮演着重要的角色。</p>
<h2>开始之前</h2>
<p>首先，下载我整理好的<code>docker-compose.yml</code>文件</p>
<p><a href="https://github.com/chudaozhe/docker-compose-samples/tree/main/mastodon">https://github.com/chudaozhe/docker-compose-samples/tree/main/mastodon</a></p>
<p>接着，准备一个域名和证书</p>
<ul>
<li>域名：<code>test.cuiwei.net</code></li>
<li>证书：<code>test.cuiwei.net.key</code>、<code>test.cuiwei.net.pem</code></li>
</ul>
<p>如果你只是想本地跑一下，也行</p>
<ul>
<li>修改hosts：<code>127.0.0.1 test.cuiwei.net</code></li>
<li>web、streaming、sidekiq 这3个服务增加<code>extra_hosts</code>，如下：</li>
</ul>
<pre><code>extra_hosts: 
  - "test.cuiwei.net:192.168.11.241"

#192.168.11.241 为宿主机ip
#extra_hosts作用是 往容器内/etc/hosts文件中添加记录，注意格式是相反的</code></pre>
<h2>快速开始</h2>
<h3>初始化</h3>
<pre><code>docker compose -f docker-compose.yml run --rm web bundle exec rake mastodon:setup</code></pre>
<p>上一步执行成功，会启动<code>db</code>和<code>redis</code>两个容器，同时会提示你输入域名（先别输），先进到<code>db</code>容器创建一个给<code>mastodon</code>用的数据库，如下创建一个用户和数据库，名称都是<code>mastodon</code>，密码为空</p>
<pre><code>psql -U postgres
CREATE USER mastodon CREATEDB;
create database mastodon owner mastodon encoding UTF8;</code></pre>
<p>接着，按照提示，一步步来</p>
<p><img src="https://www.cuiwei.net/data/upload/2023-11-04/169908382570482.jpg" alt="169908382570482.jpg" /></p>
<p>接下来，生成一份配置，需要手动复制到<code>.env.production</code>文件</p>
<p><img src="https://www.cuiwei.net/data/upload/2023-11-04/169908382556411.jpg" alt="169908382556411.jpg" /></p>
<p>最后是导入数据，和创建管理员用户</p>
<p><img src="https://www.cuiwei.net/data/upload/2023-11-04/169908382571691.jpg" alt="169908382571691.jpg" /></p>
<h3>启动服务</h3>
<p>初始化完成，就能启动服务了</p>
<pre><code>docker compose up -d</code></pre>
<h2>访问</h2>
<p><a href="https://test.cuiwei.net">https://test.cuiwei.net</a></p>
<h2>其他</h2>
<ol>
<li><code>.env.production</code> 从何而来？</li>
</ol>
<p>下载官方代码</p>
<pre><code>git clone git@github.com:mastodon/mastodon.git</code></pre>
<p>根目录有个<code>.env.production.sample</code>文件，改名为 <code>.env.production</code>，(必须的)</p>
<p>如果是初次运行，记得把里面<code>LOCAL_DOMAIN</code>, <code>PostgreSQL</code>，<code>redis</code>这些你知道的都配好（不配也可以，只是最后一步创建管理员账号会失败）</p>
<h2>参考</h2>
<p><a href="https://github.com/mastodon/mastodon">https://github.com/mastodon/mastodon</a></p>
<p><a href="https://blog.csdn.net/halobug/article/details/131704066">https://blog.csdn.net/halobug/article/details/131704066</a></p>
<p><a href="https://docs.joinmastodon.org/admin/install/#setting-up-nginx">https://docs.joinmastodon.org/admin/install/#setting-up-nginx</a></p></div>]]></description>
            <guid isPermaLink="false">Docker 部署 Mastodon - 一个去中心化的社交平台</guid>
        </item>
        <item>
            <title><![CDATA[CeSI - 管理多个 Supervisor 的Web界面]]></title>
            <description><![CDATA[<link rel="stylesheet" href="https://www.cuiwei.net/static/css/github-markdown.min.css?t=20260430" type="text/css" media="screen" /><div class="markdown-body"><p><code>CeSI（Centralized Supervisor Interface）</code>，它是一个用于管理多个监督者（Supervisor）的Web界面。监督者本身具有自己的Web用户界面，但是使用单独的界面来管理多个监督者安装是复杂的。<code>CeSI</code>的目标是通过基于监督者的RPC接口创建一个集中式的Web用户界面，以解决这个问题。</p>
<p><img src="https://www.cuiwei.net/data/upload/2023-10-09/169686417118596.jpg" alt="WX202310092307042x.png" /></p>
<h2>docker-compose.yml</h2>
<p><a href="https://github.com/chudaozhe/docker-compose-samples/tree/main/cesi">https://github.com/chudaozhe/docker-compose-samples/tree/main/cesi</a></p>
<h2>关于ui</h2>
<p>这个项目的前端部分是用React写的，正常情况构建镜像 需要先<code>yarn build</code>，然后把构建好的html,css,js等打包到基于nginx的镜像中，</p>
<p>但是作者构建的镜像，是直接把开发环境搬到了容器中：镜像基于<code>node:14.4.0-alpine3.12</code>，在容器内执行<code>yarn start</code>开启的服务，这样大大增加了镜像的体积</p>
<h3>失败的尝试</h3>
<p>我尝试把构建好的html,css,js等打包到基于nginx的镜像中，但是失败，主要因为：</p>
<ul>
<li>跨域问题：作者的后端代码 登录状态保持 用的session，跨域不能自动携带cookie</li>
</ul>
<p>解决办法：登录成功后响应头里有Set-Cookie：<code>session=eyJsb2dnZWRfaW4iOnRydWUsInVzZXJuYW1lIjoiYWRtaW4ifQ.ZSDCMQ.dgiruPrR9x-YWYT8nFg44EJ_jG4; HttpOnly; Path=/</code>，把这个值持久化存储；然后其他接口访问时header里都带上Cookie，如：<code>curl --location 'http://localhost:8092/test/aa.php' \ --header 'Cookie: session=eyJsb2dnZWRfaW4iOnRydWUsInVzZXJuYW1lIjoiYWRtaW4ifQ.ZSDCMQ.dgiruPrR9x-YWYT8nFg44EJ_jG4'</code></p>
<h2>调试UI</h2>
<p>老的nodejs项目，先确认他用到nodejs版本
（如果你的node是新的，他的是几年前的，肯定要升级各种依赖才能跑起来）</p>
<pre><code>npm install -g yarn
D:\DockerProjects\cesi\cesi\ui&gt; nvm use 15
Now using node v15.14.0 (64-bit)</code></pre>
<p>安装依赖</p>
<pre><code>D:\DockerProjects\cesi\cesi\ui&gt; yarn</code></pre>
<p>编译</p>
<pre><code>yarn build</code></pre>
<p>得到build目录</p></div>]]></description>
            <guid isPermaLink="false">CeSI - 管理多个 Supervisor 的Web界面</guid>
        </item>
        <item>
            <title><![CDATA[PM2 - 进程管理工具]]></title>
            <description><![CDATA[<link rel="stylesheet" href="https://www.cuiwei.net/static/css/github-markdown.min.css?t=20260430" type="text/css" media="screen" /><div class="markdown-body"><p>PM2 是具有内置负载均衡器的 Node.js 应用程序的生产过程管理器。它允许您使应用程序永远保持活动状态，在不停机的情况下重新加载它们，并促进常见的系统管理任务。</p>
<h2>全局安装pm2</h2>
<pre><code>npm install pm2 -g</code></pre>
<h2>启动监听模式</h2>
<p>自动监听文件变化</p>
<pre><code>pm2 start app.js --watch</code></pre>
<h2>常用命令</h2>
<pre><code>pm2 start app.js # 启动
pm2 stop app.js # 停止
pm2 logs # 日志
pm2 stop all # 停止全部
pm2 delete all # 杀死全部进程
pm2 startup # 开机自启
pm2 update pm2  # 更新 pm2
pm2 serve ./dist 9090   # 将目录dist作为静态服务器根目录，端口为9090
pm2 list   # 查看启动列表
pm2 monit   # 查看每个应用程序占用情况
pm2 ecosystem # 生成一个示例JSON配置文件
pm2 init</code></pre>
<h2>管理laravel队列</h2>
<p>todo：停止脚本待测试</p>
<h3>pm2和php安装在一个环境中</h3>
<p>都安装在一个宿主机中，或都安装在一个容器中</p>
<pre><code>cuiwei@weideMacBook-Pro laravel-demo % cat process.yml 
apps:
  - name: "laravel:queue:work" # 这里自己命名
    script: artisan #指定脚本为 artisan 脚本
    watch: false # 不监听文件变化
    interpreter: php # 脚本为php，如果你的 php 不在全局变量可以指定绝对路径脚本
    args: "queue:work --tries=3" # artisan 命令和参数
    restart_delay: 3000 # 进程中断三秒后重启
    error_file: ./storage/logs/queue.error.log # 错误日志
    out_file: ./storage/logs/queue.out.log # 输出日志
    pid_file: ./storage/app/queue.pid # pid 文件路径</code></pre>
<h3>宿主机安装pm2，然后控制容器内的php</h3>
<pre><code>cuiwei@weideMacBook-Pro laravel-demo % cat process2.yml
apps:
  - name: "laravel:queue:work" # 这里自己命名
    script: docker_artisan.sh #指定脚本为 artisan 脚本
    watch: false # 不监听文件变化
    interpreter: bash # 使用 Bash 解释器来执行命令
    args: "" # artisan 命令和参数
    restart_delay: 3000 # 进程中断三秒后重启
    error_file: ./storage/logs/queue.error.log # 错误日志
    out_file: ./storage/logs/queue.out.log # 输出日志
    pid_file: ./storage/app/queue.pid # pid 文件路径
    pre-stop: docker_artisan_clear.sh # 停止脚本</code></pre>
<pre><code>cuiwei@weideMacBook-Pro laravel-demo % cat docker_artisan.sh 
#!/usr/bin/env bash
docker exec -u www-data server-docker-php-fpm-1 /var/www/laravel-demo/artisan queue:work --tries=3</code></pre>
<pre><code>
cuiwei@weideMacBook-Pro koa-demo % pm2 init simple   

                        -------------

__/\\\\\\\\\\\\\____/\\\\____________/\\\\____/\\\\\\\\\_____
 _\/\\\/////////\\\_\/\\\\\\________/\\\\\\__/\\\///////\\\___
  _\/\\\_______\/\\\_\/\\\//\\\____/\\\//\\\_\///______\//\\\__
   _\/\\\\\\\\\\\\\/__\/\\\\///\\\/\\\/_\/\\\___________/\\\/___
    _\/\\\/////////____\/\\\__\///\\\/___\/\\\________/\\\//_____
     _\/\\\_____________\/\\\____\///_____\/\\\_____/\\\//________
      _\/\\\_____________\/\\\_____________\/\\\___/\\\/___________
       _\/\\\_____________\/\\\_____________\/\\\__/\\\\\\\\\\\\\\\_
        _\///______________\///______________\///__\///////////////__

                          Runtime Edition

        PM2 is a Production Process Manager for Node.js applications
                     with a built-in Load Balancer.

                Start and Daemonize any application:
                $ pm2 start app.js

                Load Balance 4 instances of api.js:
                $ pm2 start api.js -i 4

                Monitor in production:
                $ pm2 monitor

                Make pm2 auto-boot at server restart:
                $ pm2 startup

                To go further checkout:
                http://pm2.io/

                        -------------

[PM2] Spawning PM2 daemon with pm2_home=/Users/cuiwei/.pm2
[PM2] PM2 Successfully daemonized
File /Users/cuiwei/PhpstormProjects/koa-demo/ecosystem.config.js generated
</code></pre>
<h2>参考</h2>
<p><a href="https://github.com/Unitech/pm2">https://github.com/Unitech/pm2</a></p>
<p><a href="https://blog.csdn.net/qq_41008918/article/details/118439088">https://blog.csdn.net/qq_41008918/article/details/118439088</a></p>
<p><a href="https://pm2.fenxianglu.cn/docs/start">https://pm2.fenxianglu.cn/docs/start</a></p>
<p><a href="https://www.cnblogs.com/sweetsunnyflower/p/11466349.html">https://www.cnblogs.com/sweetsunnyflower/p/11466349.html</a></p></div>]]></description>
            <guid isPermaLink="false">PM2 - 进程管理工具</guid>
        </item>
        <item>
            <title><![CDATA[sqlx - golang database/sql 的通用扩展]]></title>
            <description><![CDATA[<link rel="stylesheet" href="https://www.cuiwei.net/static/css/github-markdown.min.css?t=20260430" type="text/css" media="screen" /><div class="markdown-body"><p>go 操作数据库有多种方式，比如之前介绍的 <code>gorm</code>：<a href="https://www.cuiwei.net/p/1926321413">go gin 封装gorm
</a>，<a href="https://www.cuiwei.net/p/1875213576">gorm 基本操作
</a></p>
<p>今天介绍的<code>sqlx</code>，是Go的另一个包，它在优秀的内置<code>database/sql</code>包之上提供了一组扩展。</p>
<h2>安装</h2>
<pre><code>go get github.com/jmoiron/sqlx</code></pre>
<h2>init</h2>
<pre><code>package db

import (
    "fmt"
    "github.com/jmoiron/sqlx"
)

var Conn *sqlx.DB

func InitDB() (err error) {
    dsn := "root:@tcp(127.0.0.1:3306)/ent?charset=utf8mb4&amp;parseTime=True"
    // 也可以使用MustConnect连接不成功就panic
    Conn, err = sqlx.Connect("mysql", dsn)
    if err != nil {
        fmt.Printf("connect DB failed, err:%v\n", err)
        return
    }
    Conn.SetMaxOpenConns(20)
    Conn.SetMaxIdleConns(10)
    return
}</code></pre>
<h2>main</h2>
<pre><code>func main() {
    err := db.InitDB()
    if err != nil {
        return
    }
}</code></pre>
<h2>models</h2>
<pre><code>package models

import (
    "database/sql/driver"
    "enterprise-api/core/db"
    "fmt"
    "github.com/jmoiron/sqlx"
)

type User struct {
    Id         int    `json:"id"`
    Name       string `json:"name"`
    Memo       string `json:"memo"`
    CreateTime int64  `json:"create_time"`
    UpdateTime int64  `json:"update_time"`
}

// 插入数据
func InsertRowDemo() (id int64, err error) {
    sqlstr := "insert into cw_test (name, memo) values (?,?)"
    ret, err := db.Conn.Exec(sqlstr, "沙河小王子", "xx")
    if err != nil {
        fmt.Printf("insert failed, err:%v\n", err)
        return
    }
    id, err = ret.LastInsertId() //新插入数据的id
    if err != nil {
        fmt.Printf("get lastinsert ID failed, err:%v\n", err)
        return
    }
    fmt.Printf("insert success, the id is %d. In", id)
    return
}

// 删除数据
func DeleteRowDemo() (n int64, err error) {
    sqlstr := "delete from cw_test where id = ?"
    ret, err := db.Conn.Exec(sqlstr, 6)
    if err != nil {
        fmt.Printf("delete failed, err:% in", err)
        return
    }
    n, err = ret.RowsAffected() //操作影响的行数
    if err != nil {
        fmt.Printf("get RowsAffected failed, err:%v\n", err)
        return
    }
    fmt.Printf("delete success, affected rows :%d\n", n)
    return
}

// 更新数据
func UpdateRowDemo() (n int64, err error) {
    sqlstr := "update cw_test set memo=? where id = ?"
    ret, err := db.Conn.Exec(sqlstr, "xx2", 7)
    if err != nil {
        fmt.Printf("update failed, err:%\n", err)
        return
    }
    n, err = ret.RowsAffected() //操作影响的行数
    if err != nil {
        fmt.Printf("get RowSAffected failed, err:%v\n", err)
        return
    }
    fmt.Printf("update success, affected rows :%d\n", n)
    return
}

// 查询单条数据示例
func QueryRowDemo() (u User, err error) {
    sqlstr := "select id, name, memo from cw_test where id=?"
    err = db.Conn.Get(&amp;u, sqlstr, 1)
    if err != nil {
        fmt.Printf("get failed, err:%v\n", err)
        return
    }
    fmt.Printf("id :%d name :%s memo:%sln", u.Id, u.Name, u.Memo)
    return
}

// 查询多条数据示例
func QueryMultiRowDemo() (users []User, err error) {
    sqlstr := "select id, name, memo from cw_test where id &gt; ?"
    err = db.Conn.Select(&amp;users, sqlstr, 0)
    if err != nil {
        fmt.Printf("query failed, err:%v\n", err)
        return
    }
    fmt.Printf("users :%tv\n", users)
    return
}

// NameExec方法用来鄉定SQL语句与结构体或map中的同名字段。
func InsertUserDemo() (id int64, err error) {
    sqlstr := "INSERT INTO cw_test (name, memo) VALUES (:name, :memo)"
    ret, err := db.Conn.NamedExec(sqlstr, map[string]interface{}{
        "name": "小王子2",
        "memo": "xx",
    })
    id, err = ret.LastInsertId() //新插入数据的id
    if err != nil {
        fmt.Printf("get lastinsert ID failed, err:%v\n", err)
        return
    }
    return
}

func (u User) Value() (driver.Value, error) {
    return []interface{}{u.Name, u.Memo}, nil
}

func InsertAll() (id int64, err error) {
    sqlStr := "insert into cw_test(name,memo) values(?),(?),(?),(?),(?)"
    users := []interface{}{
        User{Name: "骚包1号", Memo: "21"},
        User{Name: "骚包2号", Memo: "22"},
        User{Name: "骚包3号", Memo: "23"},
        User{Name: "骚包4号", Memo: "24"},
        User{Name: "骚包5号", Memo: "25"},
    }
    query, args, _ := sqlx.In(sqlStr, users...)
    fmt.Println(query) // 查看生成的查询语句
    fmt.Println(args)  // 查看生成的args
    ret, err := db.Conn.Exec(query, args...)
    id, err = ret.LastInsertId() //新插入数据的id
    if err != nil {
        fmt.Printf("get lastinsert ID failed, err:%v\n", err)
        return
    }
    return
}

// 通过ids进行查询数据
func FindUserByIds() (users []User, err error) {
    sqlStr := "select id, name, memo from cw_test where id in (?)"
    ids := []int{1, 2, 3, 4, 5}
    // 动态进行查询
    query, args, _ := sqlx.In(sqlStr, ids)
    query = db.Conn.Rebind(query)
    err = db.Conn.Select(&amp;users, query, args...)
    if err != nil {
        fmt.Printf("failed, err:%v\n", err)
        return
    }
    return
}</code></pre>
<h2>controllers</h2>
<pre><code>    //id, err := models.InsertRowDemo()
    //n, err := models.DeleteRowDemo()
    //n, err := models.UpdateRowDemo()
    //u, err := models.QueryRowDemo()
    //us, err := models.QueryMultiRowDemo()
    //id, err := models.InsertUserDemo()
    //id, err := models.InsertAll()
    //us, err := models.FindUserByIds()</code></pre>
<h2>参考</h2>
<p><a href="https://www.jianshu.com/p/c8a0e56cefdd">https://www.jianshu.com/p/c8a0e56cefdd</a></p>
<p><a href="https://blog.csdn.net/qq_43514659/article/details/121554276">https://blog.csdn.net/qq_43514659/article/details/121554276</a></p>
<p><a href="http://jmoiron.github.io/sqlx/">http://jmoiron.github.io/sqlx/</a></p></div>]]></description>
            <guid isPermaLink="false">sqlx - golang database/sql 的通用扩展</guid>
        </item>
        <item>
            <title><![CDATA[gin 全局的异常处理]]></title>
            <description><![CDATA[<link rel="stylesheet" href="https://www.cuiwei.net/static/css/github-markdown.min.css?t=20260430" type="text/css" media="screen" /><div class="markdown-body"><h2>panic类比其他语言中的异常处理</h2>
<p>有的人把 Go 中的 <code>panic/recover</code>类比 PHP 中的 <code>throw/try catch</code>，类比 Python 中的<code>raise/try except</code>，类比 Java 中的 <code>throw/try catch</code></p>
<p>当然也有的人不这么认为。比如：“用户名密码错误”时，在PHP中使用throw语句来抛出异常，大家都觉得很合理，属于“遇到无法处理的错误或异常”</p>
<p>但在Go语言中，&quot;用户名密码错误&quot;这样的预料之中的错误，使用 panic 来处理并不是一个好的选择。panic 适用于无法恢复的严重错误或异常情况，它会立即停止程序执行并触发异常处理机制。而&quot;用户名密码错误&quot;这样的错误通常是一种可预见的情况，并且可以通过合适的错误处理来解决。</p>
<p>虽然我也觉得大量用 panic 不合适，但是给出代码，记录一下探索过程</p>
<pre><code>#创建文件 middlewares/recover.go
package middlewares

func Recover(c *gin.Context) {
    defer func() {
        if r := recover(); r != nil {
            c.JSON(http.StatusOK, gin.H{
                "code": "1",
                "msg":  errorToString(r),
                "data": nil,
            })
            c.Abort()
        }
    }()
    c.Next()
}

func errorToString(r interface{}) string {
    switch v := r.(type) {
    case error:
        return v.Error()
    default:
        return r.(string)
    }
}</code></pre>
<p>使用</p>
<pre><code>router := gin.Default()
//注意 Recover 要尽量放在第一个被加载
router.Use(middlewares.Recover)</code></pre>
<blockquote>
<p>注意，子协程里的 panic 拦截不了</p>
<p>Gin 框架内置了 Recovery 中间件，用于处理 panic 异常和未知异常</p>
</blockquote>
<h3>参考</h3>
<p><a href="https://learnku.com/articles/58952">https://learnku.com/articles/58952</a></p>
<p><a href="https://blog.csdn.net/u014155085/article/details/106733391">https://blog.csdn.net/u014155085/article/details/106733391</a></p>
<p><a href="https://www.tizi365.com/question/1611.html">https://www.tizi365.com/question/1611.html</a></p>
<h2>gin 建议的处理方式</h2>
<p>gin提供了一个全局的错误列表<code>c.Errors</code>，遇到错误只需调用<code>c.Error</code>把错误对象推到列表，然后在合适的时机读取错误列表，做相应的处理即可。</p>
<p>gin源代码中 Context.go 文件：</p>
<pre><code>type Context struct {
        //......
    // Errors is a list of errors attached to all the handlers/middlewares who used this context.
    Errors errorMsgs
        //......
}

/************************************/
/********* ERROR MANAGEMENT *********/
/************************************/

// Error attaches an error to the current context. The error is pushed to a list of errors.
// It's a good idea to call Error for each error that occurred during the resolution of a request.
// A middleware can be used to collect all the errors and push them to a database together,
// print a log, or append it in the HTTP response.
// Error will panic if err is nil.
func (c *Context) Error(err error) *Error {
    if err == nil {
        panic("err is nil")
    }

    parsedError, ok := err.(*Error)
    if !ok {
        parsedError = &amp;Error{
            Err:  err,
            Type: ErrorTypePrivate,
        }
    }

    c.Errors = append(c.Errors, parsedError)
    return parsedError
}</code></pre>
<p>下面看如何实现</p>
<h3>第一步，自定义错误</h3>
<pre><code>package errors

type CustomError struct {
    Err int
    Msg string
}

func (e *CustomError) Error() string {
    return e.Msg
}

func New(err int, msg string) *CustomError {
    return &amp;CustomError{
        Err: err,
        Msg: msg,
    }
}</code></pre>
<h3>第二步，把自定义错误追加到错误列表</h3>
<pre><code>package user

import (
    customError "enterprise-api/app/models/errors"
    "github.com/gin-gonic/gin"
)

func CreateTest(c *gin.Context) {
    c.Error(customError.New(3, "用户名密码错误"))
    return
}</code></pre>
<h3>第三步，中间件拦截处理</h3>
<p>定义中间件</p>
<pre><code>package middlewares

import (
    "enterprise-api/app/models/errors"
    "github.com/gin-gonic/gin"
    "net/http"
)

func Error() gin.HandlerFunc {
    return func(c *gin.Context) {
        c.Next() // 先调用c.Next()执行后面的中间件
        // 所有中间件及router处理完毕后从这里开始执行
        // 检查c.Errors中是否有错误
        for _, errorItem := range c.Errors {
            err := errorItem.Err
            // 若是自定义的错误则将err、msg返回
            if customErr, ok := err.(*errors.CustomError); ok {
                c.JSON(http.StatusOK, gin.H{
                    "err": customErr.Err,
                    "msg": customErr.Msg,
                })
            } else {
                // 非自定义错误则返回详细错误信息err.Error()
                c.JSON(http.StatusOK, gin.H{
                    "err": 500,
                    "msg": err.Error(), //服务器异常
                })
            }
            return // 检查一个错误就行
        }
    }
}</code></pre>
<p>使用中间件</p>
<pre><code>router := gin.Default()
router.Use(middlewares.Error())</code></pre>
<h3>参考</h3>
<p><a href="https://juejin.cn/post/7064770224515448840">https://juejin.cn/post/7064770224515448840</a></p></div>]]></description>
            <guid isPermaLink="false">gin 全局的异常处理</guid>
        </item>
        <item>
            <title><![CDATA[gin 模型绑定和验证]]></title>
            <description><![CDATA[<link rel="stylesheet" href="https://www.cuiwei.net/static/css/github-markdown.min.css?t=20260430" type="text/css" media="screen" /><div class="markdown-body"><h2>缺点</h2>
<h3>无法设置默认值</h3>
<p>比如需要分页的接口，页码和条数是非必传的，如果不传页码默认1，条数默认10，但是<a href="https://github.com/go-playground/validator">go-playground/validator/v10</a>做不到</p>
<h3>无法同时获取路径参数和（查询参数或正文参数）</h3>
<p>restful风格的路由中会遇到这个问题，比如：有如下路由</p>
<pre><code>r.GET("/:user_id/category/:category_id/article", myFunc)</code></pre>
<p>请求如下</p>
<pre><code>curl 'localhost:7097/v1/user/0/category/4/article?page=1&amp;max=10'</code></pre>
<p>我该如何用一个模型同时取到<code>category_id</code>，<code>page</code>，<code>max</code>参数呢？做不到！
只能拆成两个模型，然后分别用<code>ShouldBindUri</code>、<code>ShouldBindQuery</code>获取</p>
<h2>Bind和ShouldBind</h2>
<p>Gin提供了两类绑定方法：</p>
<h3>Type - Must bind</h3>
<ul>
<li>Methods - Bind, BindJSON, BindXML, BindQuery, BindYAML</li>
<li>Behavior - 这些方法属于 <code>MustBindWith</code> 的具体调用。 如果发生绑定错误，则请求终止，并触发 <code>c.AbortWithError(400, err).SetType(ErrorTypeBind)</code>。响应状态码被设置为 400 并且 Content-Type 被设置为 <code>text/plain; charset=utf-8</code>。 如果您在此之后尝试设置响应状态码，Gin会输出日志 <code>[GIN-debug] [WARNING] Headers were already written. Wanted to override status code 400 with 422</code>。 如果您希望更好地控制绑定，考虑使用 <code>ShouldBind</code> 等效方法。</li>
</ul>
<h3>Type - Should bind</h3>
<ul>
<li>Methods - ShouldBind, ShouldBindJSON, ShouldBindXML, ShouldBindQuery, ShouldBindYAML</li>
<li>Behavior - 这些方法属于 <code>ShouldBindWith</code> 的具体调用。 如果发生绑定错误，Gin 会返回错误并由开发者处理错误和请求。
使用 <code>Bind</code> 方法时，Gin 会尝试根据 <code>Content-Type</code> 推断如何绑定。 如果你明确知道要绑定什么，可以使用 <code>MustBindWith</code> 或 <code>ShouldBindWith</code>。</li>
</ul>
<h2>自定义验证器&amp;转化中文</h2>
<p><a href="https://learnku.com/articles/59498#745bcc">https://learnku.com/articles/59498#745bcc</a></p>
<h2>参考</h2>
<p><a href="https://gin-gonic.com/zh-cn/docs/examples/binding-and-validation/">https://gin-gonic.com/zh-cn/docs/examples/binding-and-validation/</a></p></div>]]></description>
            <guid isPermaLink="false">gin 模型绑定和验证</guid>
        </item>
        <item>
            <title><![CDATA[NLTK 的安装]]></title>
            <description><![CDATA[<link rel="stylesheet" href="https://www.cuiwei.net/static/css/github-markdown.min.css?t=20260430" type="text/css" media="screen" /><div class="markdown-body"><p>Natural Language Toolkit，自然语言处理工具包，在NLP领域中，最常使用的一个Python库。</p>
<h2>自动安装</h2>
<p>如果您不确定需要哪些数据集/模型，可以安装<code>流行的</code></p>
<pre><code>python -m nltk.downloader popular

//或者
import nltk;
nltk.download('popular')</code></pre>
<h2>手动安装</h2>
<p>已知的原因，自动安装会失败</p>
<p>手动下载这些包<code>https://github.com/nltk/nltk_data/tree/gh-pages/packages</code>，放在<code>nltk_data</code>目录，然后移动到正确的位置。</p>
<p>比如我的：
<code>~/Library/Caches/pypoetry/virtualenvs/langchaintest-SW7TORgA-py3.9/nltk_data </code></p>
<h2>参考</h2>
<p><a href="https://blog.csdn.net/justlpf/article/details/121707391">https://blog.csdn.net/justlpf/article/details/121707391</a></p>
<p><a href="https://zhuanlan.zhihu.com/p/433423216">https://zhuanlan.zhihu.com/p/433423216</a></p>
<p><a href="https://www.nltk.org/data.html">https://www.nltk.org/data.html</a></p></div>]]></description>
            <guid isPermaLink="false">NLTK 的安装</guid>
        </item>
        <item>
            <title><![CDATA[openai whisper 语音识别，语音翻译]]></title>
            <description><![CDATA[<link rel="stylesheet" href="https://www.cuiwei.net/static/css/github-markdown.min.css?t=20260430" type="text/css" media="screen" /><div class="markdown-body"><h2>简介</h2>
<p>Whisper 是openai开源的一个通用的语音识别模型，同时支持把各种语言的音频翻译为成英文（音频-&gt;文本）。</p>
<h2>安装</h2>
<pre><code>apt install ffmpeg

pip install -U openai-whisper</code></pre>
<h2>使用</h2>
<p>指令</p>
<pre><code>whisper video.mp4
whisper audio.flac audio.mp3 audio.wav --model medium
whisper japanese.wav --language Japanese
whisper chinese.mp4 --language Chinese --task translate
whisper --help</code></pre>
<p>代码中使用，以下是Python示例</p>
<pre><code>import whisper

model = whisper.load_model("base")
result = model.transcribe("audio.mp3")
print(result["text"])</code></pre>
<h2>扩展，Whisper ASR Webservice</h2>
<p>whisper 只支持服务端代码调用，如果前端要使用得通过接口，<code>Whisper ASR Webservice</code>帮我们提供了这样的接口，目前提供两个接口，一个音频语言识别和音频转文字（支持翻译和转录）</p>
<p><code>Whisper ASR Webservice</code>除了支持<code>Whisper</code>，还支持<code>faster-whisper</code>；<code>faster-whisper</code>据说能够实现比 <code>Whisper</code>更快的转录功能，同时显存占用也比较小。</p>
<p><code>Whisper ASR Webservice</code>的 <a href="https://github.com/ahmetoner/whisper-asr-webservice">git 仓库</a> 下的<code>docker-compose.gpu.yml</code>可以直接使用</p>
<h3>接口文档</h3>
<p><a href="http://localhost:9000/docs">http://localhost:9000/docs</a></p>
<p>其中，<code>音频转文字接口</code>，识别出的文字可能是简体，繁体混合的，可以通过参数<code>initial_prompt</code>调节，比如设置参数值为<code>以下是普通话的句子，这是一段会议记录。</code>，来源： <a href="https://blog.csdn.net/gootyking/article/details/134475995">https://blog.csdn.net/gootyking/article/details/134475995</a></p>
<h2>参考</h2>
<p><a href="https://zhuanlan.zhihu.com/p/617770448">https://zhuanlan.zhihu.com/p/617770448</a></p>
<p><a href="https://github.com/openai/whisper">https://github.com/openai/whisper</a></p>
<p><a href="https://github.com/SYSTRAN/faster-whisper">https://github.com/SYSTRAN/faster-whisper</a></p></div>]]></description>
            <guid isPermaLink="false">openai whisper 语音识别，语音翻译</guid>
        </item>
        <item>
            <title><![CDATA[Jupyter Notebook 安装 PHP 内核]]></title>
            <description><![CDATA[<link rel="stylesheet" href="https://www.cuiwei.net/static/css/github-markdown.min.css?t=20260430" type="text/css" media="screen" /><div class="markdown-body"><h2>普通安装</h2>
<h3>安装zmq扩展</h3>
<p>官方的<a href="https://pecl.php.net/package/zmq">zmq</a>已多年不维护了，并且在php7.4中报错，所以只能选择第三方的了</p>
<pre><code>wget https://github.com/stijnvdb88/php-zmq/archive/refs/tags/v4.3.4.tar.gz

tar -xvzf php-zmq-4.3.4.tar.gz 
mv php-zmq-4.3.4 /usr/src/php/ext/php-zmq

#安装依赖
apt-get install -y libzmq3-dev

#安装扩展
docker-php-ext-install php-zmq</code></pre>
<h3>安装Jupyter-PHP-Installer</h3>
<p>这个也是多年未更新了，已经不能正常使用了。据说是作者电脑丢了，代码都在里面😂</p>
<p>下面介绍三种方案</p>
<h4>方案一：降级composer</h4>
<p><strong>有瑕疵，比如不支持箭头函数，php标签等...，建议直接看方案三</strong></p>
<p>现在最新的composer是<code>2.5.5</code>，直接执行这个包会报如下错误</p>
<pre><code>The package name jupyter-php-instance is invalid, it should be lowercase and have a vendor name, a forward slash, and a package name, matching: [a-z0-9_.-]+/[a-z0-9_.-]+  </code></pre>
<p>下面把composer降到<code>1.8.5</code></p>
<pre><code>root@php-jupyter:~# composer self-update 1.8.5 
Upgrading to version 1.8.5 (stable channel).

Use composer self-update --rollback to return to version 2.5.5</code></pre>
<p>接着，下载并安装</p>
<pre><code>curl -sS -o /tmp/jupyter-php-installer.phar https://litipk.github.io/Jupyter-PHP-Installer/dist/jupyter-php-installer.phar

php /tmp/jupyter-php-installer.phar install -v

#查看可用的内核列表
jupyter kernelspec list

#查看服务列表
jupyter server list</code></pre>
<h4>方案二：自己修改</h4>
<p><strong>有瑕疵，比如不支持箭头函数，php标签等...，建议直接看方案三</strong></p>
<p>我fork了一份代码，然后做了些修改</p>
<ul>
<li>升级box至4.3.8</li>
<li>兼容composer2.x</li>
<li>兼容php8.1</li>
</ul>
<p>使用方法：下载我修改的<code>jupyter-php-installer.phar</code>，然后执行</p>
<pre><code>php /tmp/jupyter-php-installer.phar install -v</code></pre>
<p>项目地址：<a href="https://github.com/chudaozhe/Jupyter-PHP-Installer">https://github.com/chudaozhe/Jupyter-PHP-Installer</a></p>
<h4>方案三：</h4>
<p><a href="https://github.com/Rabrennie/jupyter-php-kernel">https://github.com/Rabrennie/jupyter-php-kernel</a></p>
<p>方便简单，详见<code>Dockerfile</code></p>
<h2>docker 部署</h2>
<p>Dockerfile</p>
<pre><code>FROM php:8.1.9-fpm

WORKDIR /notebooks

COPY ./php-zmq /usr/src/php/ext/php-zmq

RUN apt-get update \
  &amp;&amp; apt-get install -y python3-pip zlib1g-dev libzmq3-dev libzip-dev \
  &amp;&amp; pip3 install jupyterlab \
  &amp;&amp; docker-php-ext-install zip php-zmq \
  &amp;&amp; curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer

ENV PATH="/root/.composer/vendor/bin:$PATH"

RUN composer global require rabrennie/jupyter-php-kernel \
  &amp;&amp; jupyter-php-kernel --install \
  &amp;&amp; jupyter-lab --generate-config

CMD ["jupyter", "lab", "--allow-root", "--ip=0.0.0.0", "--LabApp.token=''", "--notebook-dir=/notebooks"]</code></pre>
<p>构建和运行</p>
<pre><code class="language-shell">docker build -t php-jupyter:v1 .

docker run -d --name php-jupyter -p 8888:8888 php-jupyter:v1</code></pre>
<h3>详见</h3>
<p><a href="https://github.com/chudaozhe/docker-php-jupyter">https://github.com/chudaozhe/docker-php-jupyter</a></p>
<h2>总结</h2>
<p>虽然<a href="https://github.com/jupyter/jupyter/wiki/Jupyter-kernels">Jupyter官方仓库</a>推荐的PHP内核是<a href="https://github.com/Litipk/Jupyter-PHP">Jupyter-PHP</a>，但<code>Jupyter-PHP</code>项目年久失修，已经无法适应高版本的PHP，所以这里推荐<a href="https://github.com/Rabrennie/jupyter-php-kernel">Rabrennie/jupyter-php-kernel</a></p>
<p>至此，PHP内核就安装完成了。那代码安装在哪里了呢？看这里</p>
<pre><code>root@php-jupyter:~# jupyter kernelspec list
Available kernels:
  php        /root/.local/share/jupyter/kernels/PHP
  python3    /usr/local/share/jupyter/kernels/python3

root@php-jupyter:~# cat /root/.local/share/jupyter/kernels/PHP/kernel.json 
{
  "argv": ["jupyter-php-kernel", "-r", "-c", "{connection_file}"],
  "display_name": "PHP",
  "language": "php"
}</code></pre>
<p><code>jupyter-php-kernel</code>的完整路径：<code>/root/.composer/vendor/bin/jupyter-php-kernel</code></p>
<h3>token在哪里？</h3>
<pre><code>root@php-jupyter:/notebooks# cat /root/.local/share/jupyter/runtime/jupyter_cookie_secret 
ZpyOvPvqZPqxmo42E7gb18itWFfKu7fvh5dMm2dW8m8=

#远程连接
http://127.0.0.1:8888/lab?token=ZpyOvPvqZPqxmo42E7gb18itWFfKu7fvh5dMm2dW8m8=</code></pre>
<h2>参考</h2>
<p><a href="https://litipk.github.io/Jupyter-PHP-Installer/">https://litipk.github.io/Jupyter-PHP-Installer/</a></p>
<p><a href="https://github.com/Rabrennie/jupyter-php-kernel">https://github.com/Rabrennie/jupyter-php-kernel</a></p>
<p><a href="https://github.com/hoto17296/docker-jupyter-php/blob/master/Dockerfile">https://github.com/hoto17296/docker-jupyter-php/blob/master/Dockerfile</a></p></div>]]></description>
            <guid isPermaLink="false">Jupyter Notebook 安装 PHP 内核</guid>
        </item>
        <item>
            <title><![CDATA[Jupyter Notebook 安装 GO 内核]]></title>
            <description><![CDATA[<link rel="stylesheet" href="https://www.cuiwei.net/static/css/github-markdown.min.css?t=20260430" type="text/css" media="screen" /><div class="markdown-body"><h2>普通安装</h2>
<p><a href="https://github.com/janpfeifer/gonb#linux-and-mac-installation">https://github.com/janpfeifer/gonb#linux-and-mac-installation</a></p>
<pre><code>go install github.com/janpfeifer/gonb@latest &amp;&amp; go install golang.org/x/tools/cmd/goimports@latest &amp;&amp; go install golang.org/x/tools/gopls@latest &amp;&amp; gonb --install</code></pre>
<p>接着我用<code>PyCharm</code>/<code>VS Code</code>测试，报错了</p>
<pre><code>fmt: package fmt is not in GOROOT (/usr/local/Cellar/go/1.19/src/fmt)</code></pre>
<p>这就神奇了，我GoLand 中的几个项目都可以正常跑。于是我打开GoLand，选择一个项目，在终端输入</p>
<pre><code>cuiwei@weideMacBook-Pro demo % echo $GOROOT

/usr/local/opt/go/libexec</code></pre>
<p>结果发现和报错中的<code>GOROOT</code>值不一致。于是我重新设置了全局的<code>GOROOT</code></p>
<pre><code>vi ~/.zshrc
export GOROOT=/usr/local/opt/go/libexec</code></pre>
<p>保存后，重新打开<code>PyCharm</code>/<code>VS Code</code>结果都好了🥳</p>
<h2>docker 安装</h2>
<pre><code>docker pull janpfeifer/gonb_jupyter:latest
docker run -it --rm -p 8888:8888 -v "${PWD}":/home/jovyan/work janpfeifer/gonb_jupyterlab:latest</code></pre>
<p>服务启动成功就可以访问 <code>http://localhost:8899/lab</code></p>
<p><img src="https://www.cuiwei.net/data/upload/2023-05-18/168439945315608.jpg" alt="WX202305181643332x.png" /></p>
<p>如果不想用网页版，请继续往下看</p>
<h2>配合PyCharm使用</h2>
<p>这里介绍的是使用jupyter远程服务</p>
<p>上面docker启动<code>janpfeifer/gonb_jupyter</code>成功，会得到如下地址</p>
<pre><code>http://127.0.0.1:8888/lab?token=d7455ae2e0478a620695814a8c70e4ae890942ea072b23be</code></pre>
<p>粘贴到如下图的位置即可
<img src="https://www.cuiwei.net/data/upload/2023-05-18/168439823154656.jpg" alt="jupyter server" /></p>
<p>结果是能正常跑，但语法飘红。。。（PyCharm肯定不支持GO🤣）</p>
<p><img src="https://www.cuiwei.net/data/upload/2023-05-18/168439924915638.jpg" alt="WX202305181640222x.png" /></p>
<h2>配合VS Code使用</h2>
<p>这里也是使用jupyter远程服务，如下</p>
<p><img src="https://www.cuiwei.net/data/upload/2023-05-18/168439890286207.jpg" alt="WX202305181632532x.png" /></p>
<p>效果还不错～</p>
<p><img src="https://www.cuiwei.net/data/upload/2023-05-18/168439917659583.jpg" alt="WX202305181638562x.png" /></p>
<h2>参考</h2>
<p><a href="https://github.com/janpfeifer/gonb">https://github.com/janpfeifer/gonb</a></p></div>]]></description>
            <guid isPermaLink="false">Jupyter Notebook 安装 GO 内核</guid>
        </item>
        <item>
            <title><![CDATA[Alembic - 用于 SQLAlchemy 的数据库迁移工具]]></title>
            <description><![CDATA[<link rel="stylesheet" href="https://www.cuiwei.net/static/css/github-markdown.min.css?t=20260430" type="text/css" media="screen" /><div class="markdown-body"><p>Alembic 是<code>SQLAlchemy</code>的作者编写的数据库迁移工具。</p>
<h2>安装配置</h2>
<pre><code>pip install alembic

#初始化
alembic init {指定目录，比如 alembic }</code></pre>
<p>配置</p>
<p>将<code>alembic.ini</code>中的<code>sqlalchemy.url</code>改为你数据库地址：<code>sqlalchemy.url = sqlite:///./database/app.sqlite3</code></p>
<h2>迁移脚本</h2>
<p><a href="https://alembic.sqlalchemy.org/en/latest/tutorial.html#create-a-migration-script">https://alembic.sqlalchemy.org/en/latest/tutorial.html#create-a-migration-script</a></p>
<pre><code>#创建
alembic revision -m "create account table"
#执行，升到最高版本
alembic upgrade head

#创建
alembic revision -m "Add a column"
#执行，升到最高版本
alembic upgrade head

#其他命令
alembic current
alembic downgrade base</code></pre>
<h2>迁移脚本2（自动生成迁移）</h2>
<p>上面那种方式是需要手动填充表字段，下面这种方式可以自动生成</p>
<p><a href="https://alembic.sqlalchemy.org/en/latest/autogenerate.html">https://alembic.sqlalchemy.org/en/latest/autogenerate.html</a></p>
<p>修改<code>alembic</code>文件夹下的<code>env.py</code>，找到<code>target_metadata = None</code>，替换为：</p>
<pre><code>　　#有几个模型就导几个模型
　　from app.models.article import ArticleModel
　　from app.models.category import CategoryModel
　　from core.db.sqlite import Base
　　target_metadata = Base.metadata </code></pre>
<blockquote>
<p>一些文档说要知道路径，否则会引入失败；我这用的新版本没遇到这个问题</p>
</blockquote>
<p>迁移命令</p>
<pre><code>#开发的时候可能创建很多个版本，上线时想合并成一个：删除数据库和多个版本的文件，重新生成即可

#创建一个迁移版本
alembic revision --autogenerate -m "create table"

#执行迁移，升到最高版本
alembic upgrade head</code></pre>
<h2>生成sql</h2>
<p>Alembic 的一个主要功能是将迁移生成为 SQL 脚本</p>
<p><a href="https://alembic.sqlalchemy.org/en/latest/offline.html">https://alembic.sqlalchemy.org/en/latest/offline.html</a></p>
<pre><code>alembic upgrade ec32dafdf7fe --sql

#获取起始版本
alembic upgrade 1975ea83b712:ae1027a6acf --sql

#导出到文件
alembic upgrade 1975ea83b712:ae1027a6acf --sql &gt; migration.sql</code></pre>
<h2>参考</h2>
<p><a href="https://www.cnblogs.com/wanghong1994/p/16687895.html">https://www.cnblogs.com/wanghong1994/p/16687895.html</a></p>
<p><a href="https://segmentfault.com/a/1190000006949536">https://segmentfault.com/a/1190000006949536</a></p></div>]]></description>
            <guid isPermaLink="false">Alembic - 用于 SQLAlchemy 的数据库迁移工具</guid>
        </item>
        <item>
            <title><![CDATA[sqlalchemy的基本使用]]></title>
            <description><![CDATA[<link rel="stylesheet" href="https://www.cuiwei.net/static/css/github-markdown.min.css?t=20260430" type="text/css" media="screen" /><div class="markdown-body"><p>SQLAlchemy 是 Python 的 SQL 工具包和 ORM 框架</p>
<h2>安装</h2>
<pre><code>pip install SQLAlchemy</code></pre>
<h2>封装</h2>
<pre><code>#path: core/db/sqlite.py
from sqlalchemy import create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker

#数据库访问地址
SQLALCHEMY_DATABASE_URL = "sqlite:///./database/app.sqlite3"     # SQL
# SQLALCHEMY_DATABASE_URL = "postgresql://user:password@postgresserver/db"  # MYSQL

#启动引擎
engine = create_engine(
    SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False}
)

#启动会话
DB_Session = sessionmaker(autocommit=False, autoflush=False, bind=engine)

# 创建会话对象
session = DB_Session()

#数据模型的基类
Base = declarative_base()</code></pre>
<h2>1.x和2.0 查询语法的区别</h2>
<p><a href="https://docs.sqlalchemy.org/en/14/orm/session_basics.html#querying-1-x-style">https://docs.sqlalchemy.org/en/14/orm/session_basics.html#querying-1-x-style</a></p>
<p>迁移指南: <a href="https://docs.sqlalchemy.org/en/20/changelog/migration_20.html#migration-20-query-usage">https://docs.sqlalchemy.org/en/20/changelog/migration_20.html#migration-20-query-usage</a></p>
<h3>1.x 的查询</h3>
<pre><code># query from a class
results = session.query(User).filter_by(name="ed").all()

# query with multiple classes, returns tuples
results = session.query(User, Address).join("addresses").filter_by(name="ed").all()

# query using orm-columns, also returns tuples
results = session.query(User.name, User.fullname).all()

#总数
session.query(ArticleModel).select_from().count()
session.query(func.count(ArticleModel.id)).scalar()

# 关联查询，获取列表
# .with_entities(ArticleModel.title, CategoryModel.name) \
# sqlite不支持concat
# .filter(func.concat(ArticleModel.title, ArticleModel.content).like("%cc%")) \
result = session.query(ArticleModel, CategoryModel)\
    .join(CategoryModel, CategoryModel.id == ArticleModel.category_id)\
    .filter(ArticleModel.status == 1, ArticleModel.category_id.in_([1,2])) \
    .order_by(desc(ArticleModel.create_time))\
    .offset((page - 1) * max).limit(max)\
    .all()</code></pre>
<h3>2.0 的查询</h3>
<pre><code>from sqlalchemy import select
from sqlalchemy.orm import Session

session = Session(engine, future=True)

# query from a class
statement = select(User).filter_by(name="ed")

# list of first element of each row (i.e. User objects)
result = session.execute(statement).scalars().all()

# query with multiple classes
statement = select(User, Address).join("addresses").filter_by(name="ed")

# list of tuples
result = session.execute(statement).all()

# query with ORM columns
statement = select(User.name, User.fullname)

# list of tuples
result = session.execute(statement).all()</code></pre>
<h2>添加新项或更新现有项</h2>
<pre><code>user1 = User(name="user1")
user2 = User(name="user2")
session.add(user1)
session.add(user2)

session.commit()  # write changes to the database</code></pre>
<p>要一次向会话添加项目列表，请使用：</p>
<pre><code>session.add_all([item1, item2, item3])</code></pre>
<h2>删除</h2>
<pre><code># mark two objects to be deleted
session.delete(obj1)
session.delete(obj2)

# commit (or flush)
session.commit()</code></pre>
<h2>练习</h2>
<pre><code>#查询
session.get(ArticleModel, 5)
session.query(ArticleModel).filter(ArticleModel.id == 5).first()
session.query(ArticleModel).filter(ArticleModel.id == id).one()
session.query(ArticleModel).get(5)

#https://docs.sqlalchemy.org/en/14/orm/session_basics.html#update-and-delete-with-arbitrary-where-clause
#1.x的更新
session.query(User).filter(User.name == "squidward").update(
    {"name": "spongebob"}, synchronize_session="fetch"
)

#2.0的更新
from sqlalchemy import update

stmt = (
    update(User)
    .where(User.name == "squidward")
    .values(name="spongebob")
    .execution_options(synchronize_session="fetch")
)

result = session.execute(stmt)

#获取UPDATE 或 DELETE 受影响的行数，使用 
num_rows_matched = result.rowcount

#1.x的删除
session.query(User).filter(User.name == "squidward").delete(synchronize_session="fetch")

#2.0的删除
from sqlalchemy import delete

stmt = (
    delete(User)
    .where(User.name == "squidward")
    .execution_options(synchronize_session="fetch")
)

session.execute(stmt)</code></pre>
<h2>参考</h2>
<p><a href="https://blog.csdn.net/weixin_41085315/article/details/123940220">https://blog.csdn.net/weixin_41085315/article/details/123940220</a></p></div>]]></description>
            <guid isPermaLink="false">sqlalchemy的基本使用</guid>
        </item>
        <item>
            <title><![CDATA[Laravel CSRF 保护]]></title>
            <description><![CDATA[<link rel="stylesheet" href="https://www.cuiwei.net/static/css/github-markdown.min.css?t=20260430" type="text/css" media="screen" /><div class="markdown-body"><p>跨站点请求伪造（英语：Cross-site request forgery）是一种恶意利用，利用这种手段，代表经过身份验证的用户执行未经授权的命令。值得庆幸的是，Laravel 可以轻松保护您的应用程序免受跨站点请求伪造（CSRF）攻击。</p>
<h2>漏洞的解释</h2>
<p>如果您不熟悉跨站点请求伪造，我们讨论一个利用此漏洞的示例。假设您的应用程序有一个 <code>/user/email</code> 路由，它接受 <code>POST</code> 请求来更改经过身份验证用户的电子邮件地址。最有可能的情况是，此路由希望 <code>email</code> 输入字段包含用户希望开始使用的电子邮件地址。</p>
<p>没有 <code>CSRF</code> 保护，恶意网站可能会创建一个 <code>HTML</code> 表单，指向您的应用程序 <code>/user/email</code> 路由，并提交恶意用户自己的电子邮件地址：</p>
<pre><code>&lt;form action="https://your-application.com/user/email" method="POST"&gt;
    &lt;input type="email" value="malicious-email@example.com"&gt;
&lt;/form&gt;

&lt;script&gt;
    document.forms[0].submit();
&lt;/script&gt;</code></pre>
<p>如果恶意网站在页面加载时自动提交了表单，则恶意用户只需要诱使您的应用程序的一个毫无戒心的用户访问他们的网站，他们的电子邮件地址就会在您的应用程序中更改。</p>
<p>为了防止这种漏洞，我们需要检查每一个传入的 <code>POST</code>，<code>PUT</code>，<code>PATCH</code> 或 <code>DELETE</code> 请求以获取恶意应用程序无法访问的秘密会话值。</p>
<p>以上摘自 <code>Laravel</code> 文档；下面自我理解一下：</p>
<p>表单是可以跨域的。</p>
<p>用户打开了浏览器，有两个标签页，一个是您的网站（your-application.com），一个是恶意网站（怎么打开的？可能是短信，E-mail，论坛博客等，诱导用户点击链接打开的）。用户登陆了您的网站，浏览器记录了cookie ，每次请求都会自带 cookie；然后恶意网站，有如上代码（js 自动提交 form 表单），虽然恶意网站不知道你的 cookie，但你的浏览器知道啊，所以自动提交表单时会自动携带 cookie，然后就攻击成功了。</p>
<p>通过<a href="https://www.cuiwei.net/p/1333774718">Laravel 用户认证</a>我们知道了<code>web 浏览器应用</code>和<code>API 应用</code>，基于此我们今天总结下 CSRF 保护 </p>
<h2>API 应用</h2>
<p>没有这玩意。</p>
<p>不依赖 cookies 做安全验证的话，则不需要预防 CSRF。</p>
<p>CSRF 攻击关键在于 cookie，如果 cookie 里不含登陆令牌，你把登录令牌放到 header 里就没问题。因为 CSRF 所利用的 form 和四个特殊 tag 都无法添加 header。现代应用的 API 不接受 form 提交，都是 json 风格的，现代的 web 浏览器都具备 CSP， samesite 等防范机制。</p>
<h2>web 浏览器应用</h2>
<h3>阻止 CSRF 请求</h3>
<pre><code>&lt;form method="POST" action="/profile"&gt;
    @csrf

    &lt;!-- 等同于... --&gt;
    &lt;input type="hidden" name="_token" value="{{ csrf_token() }}" /&gt;
&lt;/form&gt;</code></pre>
<h3>从 CSRF 保护中排除 URI</h3>
<p>再次强调一下，只有用到<code>web</code>中间件组了，<code>Csrf</code>验证才会生效，也才需要禁用；比如api应用用不到<code>web</code>中间件组，就不用理会。</p>
<p>全局禁用，（当然这是不推荐的），注释掉<code>\App\Http\Middleware\VerifyCsrfToken::class</code>中间件</p>
<pre><code>&lt;?php

namespace App\Http;

use Illuminate\Foundation\Http\Kernel as HttpKernel;

class Kernel extends HttpKernel
{
    /**
     * The application's route middleware groups.
     *
     * @var array&lt;string, array&lt;int, class-string|string&gt;&gt;
     */
    protected $middlewareGroups = [
        'web' =&gt; [
//            \App\Http\Middleware\VerifyCsrfToken::class,
        ],</code></pre>
<p>排除部分链接，比如支付回调等</p>
<pre><code>&lt;?php

namespace App\Http\Middleware;

use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken as Middleware;

class VerifyCsrfToken extends Middleware
{
    /**
     * 从 CSRF 验证中排除的 URI。
     *
     * @var array
     */
    protected $except = [
        'stripe/*',
        'http://example.com/foo/bar',
        'http://example.com/foo/*',
    ];
}</code></pre>
<h2>参考</h2>
<p><a href="https://learnku.com/docs/laravel/9.x/csrf/12211">https://learnku.com/docs/laravel/9.x/csrf/12211</a></p>
<p><a href="https://blog.csdn.net/william_n/article/details/107954577">https://blog.csdn.net/william_n/article/details/107954577</a></p></div>]]></description>
            <guid isPermaLink="false">Laravel CSRF 保护</guid>
        </item>
        <item>
            <title><![CDATA[python 虚拟环境venv、pipenv、poetry、conda如何选择？]]></title>
            <description><![CDATA[<link rel="stylesheet" href="https://www.cuiwei.net/static/css/github-markdown.min.css?t=20260430" type="text/css" media="screen" /><div class="markdown-body"><h2>Install Poetry</h2>
<pre><code>curl -sSL https://install.python-poetry.org | python3 -</code></pre>
<p><a href="https://python-poetry.org/docs/#installing-with-the-official-installer">https://python-poetry.org/docs/#installing-with-the-official-installer</a></p>
<p>别名</p>
<pre><code>cuiwei@weideMacBook-Pro ~ % cat ~/.bashrc
alias poetry='~/.local/bin/poetry'</code></pre>
<h2>区别</h2>
<h3>virtualenv</h3>
<p>太老，除非你还在使用python 2，否则不推荐。</p>
<h3>venv</h3>
<p>python自带的虚拟环境管理，简单是它的优势，也是它的劣势。</p>
<p>只能创建虚拟环境，不能指定系统不存在的python环境版本，不能管理系统中的环境列表（例如选择一个已经创建好了的虚拟环境）。
venv的虚拟环境默认是存放在项目文件夹里的，这会影响项目文件的管理。</p>
<h3>pipenv</h3>
<p>requests库作者Kenneth Reitz大神的作品。但pipenv并不稳定，例如，如果你运行pip install ...两次，结果可能不一样，pipenv曾承诺解决这个问题，但实际上，它只是多次尝试运行pip install &lt;单个包&gt;，直到结果看起来差不多符合规范。显然，这样的方式更慢，但最终问题依然存在。</p>
<h3>anaconda / conda</h3>
<p>如果是科学计算的新手，推荐使用，但：</p>
<p>anaconda实在过于臃肿，它的安装包里包括了众多科学计算会用到的packages，安装后动辄5-6个G。
anaconda有个不包含packages的版本，叫miniconda，但miniconda仍然存在安装依赖库过于激进的问题，安装同样的packages，conda总会比别的包管理器安装更多的“依赖包”，即便有的“依赖包”并不是必须，这会导致你的项目出现不必要的膨胀。
同时，conda的packages列表“conda list”还存在和“pip list”不一致的问题。</p>
<h3>poetry</h3>
<p>唯一的真神。poetry没有上述缺点，同时轻便强大。</p>
<p>poetry使用pyproject.toml 和 poetry.lock文件来管理依赖，类似于JavaScript/Node.js的Npm和Rust的Cargo，这俩都是非常成熟好用的依赖管理方案。
poetry本身并不具有管理Python解释器的功能，推荐和pyenv/pyenv-win使用，可以轻松下载和设置不同版本的Python解释器。
poetry的缺点可能是较为复杂，上手困难。由于poetry严格的依赖管理策略，你可能会在安装依赖包时遇到更多的问题。
在国内，poetry还有另一个缺点，无法设置全局镜像源，只可针对单个项目设置镜像源。</p>
<h3>pip</h3>
<p>你应该仅使用pip来安装poetry，就像IE的唯一用途是下载Chrome。</p>
<h2>转自</h2>
<p><a href="https://zhuanlan.zhihu.com/p/663735038">https://zhuanlan.zhihu.com/p/663735038</a></p></div>]]></description>
            <guid isPermaLink="false">python 虚拟环境venv、pipenv、poetry、conda如何选择？</guid>
        </item>
        <item>
            <title><![CDATA[laravel 自定义中间件实现身份验证]]></title>
            <description><![CDATA[<link rel="stylesheet" href="https://www.cuiwei.net/static/css/github-markdown.min.css?t=20260430" type="text/css" media="screen" /><div class="markdown-body"><p>通过<a href="https://www.cuiwei.net/p/1333774718">Laravel 用户认证</a>我们知道了<code>基于 api 的身份验证</code>，实现方式有<a href="https://www.cuiwei.net/p/1422198972">Laravel Sanctum API 授权</a> 、 <a href="https://www.cuiwei.net/p/1172554297">Laravel 使用 Json Web Token(JWT)</a> 等，今天介绍一下自定义中间件实现身份验证</p>
<h2>中间件</h2>
<p>使用中间件需要提前在<code>app/Http/Kernel.php</code>这里配置，分为全局中间件、中间件、中间件组</p>
<h3>全局中间件</h3>
<p>全局中间件无需主动调用，系统会自动应用到每次请求。比如：<code>TrimStrings</code>中间件会自动去掉请求参数左右两边的空格；<code>ConvertEmptyStringsToNull</code>中间件会自动把请求参数中的空字符串转为 null。</p>
<p><code>ConvertEmptyStringsToNull</code>中间件建议不要开启，空字符串和 null 类型不同要区分开。我们之前就遇到一个坑：一个支持关键词搜索的列表，参数校验为<code>'keyword'   =&gt; 'string',</code>，因为启用了该中间件，传空字符串时报错了，<code>The keyword must be a string</code></p>
<p>按照我们通常理解关键词可以传(string)，也可以不传(null)；这里可以传又分为空字符串和有值的字符串</p>
<ul>
<li>
<p>不启用该中间件，传空字符串：参数校验<code>'keyword'   =&gt; 'string',</code>，通过参数校验，我拿到空字符串。。。</p>
</li>
<li>
<p>启用该中间件，传空字符串：参数校验<code>'keyword'   =&gt; 'string|nullable',</code>，通过参数校验，我拿到null值。。。</p>
</li>
</ul>
<p>最终我选择不启用该中间件</p>
<h3>中间件、中间件组</h3>
<p>一、上面提到的<code>Laravel Sanctum API 授权</code>使用的是<code>auth</code>中间件</p>
<pre><code>    protected $routeMiddleware = [
        'auth' =&gt; \App\Http\Middleware\Authenticate::class,
    ...
    ];

//比如
Route::group(['middleware' =&gt; ['auth:sanctum']], function () {}
</code></pre>
<p>但在<code>SPA 认证</code>场景下也会使用<code>api</code>中间件组</p>
<pre><code>    protected $middlewareGroups = [
    ...
        'api' =&gt; [
            \Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful::class,
    ...
        ],
    ];</code></pre>
<p>二、<code>JWT</code>使用的也是<code>auth</code>中间件</p>
<pre><code>    protected $routeMiddleware = [
        'auth' =&gt; \App\Http\Middleware\Authenticate::class,
    ];

//比如
$this-&gt;middleware('auth:api', ['except' =&gt; ['login']]);</code></pre>
<h2>自定义中间件</h2>
<p>该中间件支持多端，比如用户端和管理员端</p>
<p><code>vi app/Http/Middleware/ApiAuth.php</code></p>
<pre><code>&lt;?php
namespace App\Http\Middleware;

use Closure;
use Illuminate\Support\Facades\Redis;

class ApiAuth {
    public $key='{role}.{id}.token';
    /**
     * api鉴权中间件
     * @param $request
     * @param Closure $next
     * @param $role
     * @return \Illuminate\Contracts\Foundation\Application|\Illuminate\Contracts\Routing\ResponseFactory|\Illuminate\Http\Response|mixed
     */
    public function handle($request, Closure $next, $role) {//$role=user/admin
        $token=$request-&gt;header('token', '');
        if(empty($token)){
            return response(['msg'=&gt;'未传递token，请重新登录'], 403);
        }

        $_token=Redis::get(str_replace(['{role}', '{id}'], [$role, $request-&gt;route($role.'_id')], $this-&gt;key));

        if (empty($_token)) {
            return response(['msg'=&gt;'token已失效，请重新登录'], 401);
        }

        if($token !==$_token){
            return response(['msg'=&gt;'未通过验证，请重新登录'], 401);
        }
        return $next($request);
    }
}</code></pre>
<p>在<code>app/Http/Kernel.php</code>配置一下</p>
<pre><code>    protected $routeMiddleware = [
    ...
        'auth.api' =&gt; \App\Http\Middleware\ApiAuth::class,
    ];</code></pre>
<p>在路由中使用</p>
<pre><code>#用户端
Route::group(['prefix' =&gt; 'user', 'middleware'=&gt;['auth.api:user']], function(){}

#管理员端
Route::group(['prefix' =&gt; 'admin', 'middleware'=&gt;['auth.api:admin']], function(){}</code></pre></div>]]></description>
            <guid isPermaLink="false">laravel 自定义中间件实现身份验证</guid>
        </item>
        <item>
            <title><![CDATA[laravel 以服务提供者的方式使用 elasticsearch]]></title>
            <description><![CDATA[<link rel="stylesheet" href="https://www.cuiwei.net/static/css/github-markdown.min.css?t=20260430" type="text/css" media="screen" /><div class="markdown-body"><h2>安装</h2>
<p>安装<code>elasticsearch</code>官方扩展包</p>
<pre><code>composer require elasticsearch/elasticsearch</code></pre>
<h3>以服务提供者的方式使用 elasticsearch</h3>
<p>可以参考这篇文章：<a href="https://www.cuiwei.net/p/1239185878">Laravel 以服务提供者的方式使用第三方扩展包</a></p>
<p>下面给出关键配置</p>
<p><code>config/es.php</code></p>
<pre><code>&lt;?php
declare(strict_types=1);

return [
    'hosts' =&gt; explode(',', env('ELASTIC_HOSTS')),//['http://elasticsearch:9200']
    'username'  =&gt; env('ELASTIC_USERNAME', ''),
    'password'  =&gt; env('ELASTIC_PASSWORD', ''),
    'prefix'  =&gt; env('ELASTIC_PREFIX'),
];</code></pre>
<p><code>Providers/ElasticsearchServiceProvider.php</code></p>
<pre><code>&lt;?php

namespace App\Providers;

use Elasticsearch\ClientBuilder;
use Illuminate\Support\ServiceProvider;

class ElasticsearchServiceProvider extends ServiceProvider
{
    /**
     * Register services.
     *
     * @return void
     */
    public function register()
    {
        $this-&gt;app-&gt;singleton(ClientBuilder::class, function ($app) {
            $conf = config('es');
            $client = ClientBuilder::create()-&gt;setHosts($conf['hosts']);
            if ($conf['username']) $client-&gt;setBasicAuthentication($conf['username'], $conf['password']);
            return $client-&gt;build();
        });

        $this-&gt;app-&gt;alias(ClientBuilder::class, 'es');
    }
}</code></pre>
<h2>使用</h2>
<pre><code>    $this-&gt;prefix = config('es.prefix');
    $this-&gt;initArticleIndex();

    /**
     * 创建索引
     */
    protected function initArticleIndex()
    {
        $this-&gt;setMapping();

        app('es')-&gt;indices()
            -&gt;create([
                'index' =&gt; $this-&gt;prefix . 'article_index',
                'body'  =&gt; [
                    'settings' =&gt; [
                        'number_of_shards'   =&gt; 2,
                        'number_of_replicas' =&gt; 1,
                        'max_result_window'  =&gt; 100000
                    ],
                    'mappings' =&gt; [
                        'properties' =&gt; $this-&gt;mapping
                    ]
                ]
            ]);

        app('es')-&gt;indices()
            -&gt;putAlias([
                'index' =&gt; $this-&gt;prefix . 'article_index',
                'name'  =&gt; $this-&gt;prefix . 'article'
            ]);
    }

    /**
     * 设置字段
     * @throws \Common\Types\Exception
     */
    protected function setMapping()
    {
        $fields = Article::getFields();
        foreach ($fields as $key =&gt; $value) {
            if ( ! in_array($value, ['keyword', 'text', 'long', 'integer', 'byte', 'date', 'float', 'double'])) {
                throw new Exception('类型不存在');
            }
            $this-&gt;mapping[$key] = ['type' =&gt; $value];
        }
    }</code></pre>
<h2>参考</h2>
<p>参考文档包含一个完整的商品同步，搜索的示例，非常不错</p>
<p><a href="https://blog.csdn.net/weixin_41753567/article/details/125605497">https://blog.csdn.net/weixin_41753567/article/details/125605497</a></p></div>]]></description>
            <guid isPermaLink="false">laravel 以服务提供者的方式使用 elasticsearch</guid>
        </item>
        <item>
            <title><![CDATA[初步分析 Elasticsearch 文档]]></title>
            <description><![CDATA[<link rel="stylesheet" href="https://www.cuiwei.net/static/css/github-markdown.min.css?t=20260430" type="text/css" media="screen" /><div class="markdown-body"><h2>Query DSL</h2>
<p>Elasticsearch提供基于JSON的完整查询DSL（Domain Specific Language）来定义查询。</p>
<p><a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl.html">https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl.html</a></p>
<p>例如：</p>
<pre><code>GET /_search
{
  "query": { 
    "bool": { 
      "must": [
        { "match": { "title":   "Search"        }},
        { "match": { "content": "Elasticsearch" }}
      ],
      "filter": [ 
        { "term":  { "status": "published" }},
        { "range": { "publish_date": { "gte": "2015-01-01" }}}
      ]
    }
  }
}</code></pre>
<h2>REST API</h2>
<p><a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/rest-apis.html">https://www.elastic.co/guide/en/elasticsearch/reference/current/rest-apis.html</a></p>
<ul>
<li><a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/docs.html">Document APIs</a></li>
<li><a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/indices.html">Index APIs</a></li>
<li><a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/sql-apis.html">SQL APIs</a></li>
</ul>
<h2>SQL</h2>
<p><a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/xpack-sql.html">https://www.elastic.co/guide/en/elasticsearch/reference/current/xpack-sql.html</a></p>
<pre><code>POST _sql?format=txt
{
    "query": "SELECT title FROM article limit 10"
}</code></pre>
<h3>SQL翻译API</h3>
<p><code>SQL</code> 转 <code>Query DSL</code></p>
<pre><code>POST /_sql/translate
{
  "query": "SELECT * FROM article ORDER BY id DESC",
  "fetch_size": 10
}</code></pre>
<p>三个双引号的使用（使用 Kibana Console 才支持）</p>
<pre><code>POST /_sql/translate
{
  "query": """
  SELECT * FROM "index-*" ORDER BY id DESC
  """,
  "fetch_size": 10
}</code></pre>
<h3>SQL CLI</h3>
<blockquote>
<p>中文有乱码，待解决。。</p>
</blockquote>
<pre><code>root@elasticsearch:/usr/share/elasticsearch# ./bin/elasticsearch-sql-cli

# root@elasticsearch:/usr/share/elasticsearch# ./bin/elasticsearch-sql-cli https://some.server:9200

# root@elasticsearch:/usr/share/elasticsearch# ./bin/elasticsearch-sql-cli https://sql_user:strongpassword@some.server:9200

sql&gt; SELECT title FROM article limit 10;
                 title                 
---------------------------------------
vmware u5b89u88c5 android-x86    
Elastic Stack
logstash
yum logstash
Docker - Android
Docker for Android SDK
docker-compose RocketMQ           
vscode php         

sql&gt; </code></pre>
<h2>Command line tools</h2>
<p>命令行工具</p>
<p><a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/commands.html">https://www.elastic.co/guide/en/elasticsearch/reference/current/commands.html</a></p>
<ul>
<li><a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/reset-password.html">重置密码</a></li>
</ul>
<h2>Snapshot and restore（快照和恢复）</h2>
<p><a href="https://www.cuiwei.net/p/1940148482">elasticsearch 快照和恢复</a></p>
<p><a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/snapshot-restore.html">https://www.elastic.co/guide/en/elasticsearch/reference/current/snapshot-restore.html</a></p>
<h2>php操作文档</h2>
<p><a href="https://www.elastic.co/guide/en/elasticsearch/client/php-api/8.7/operations.html">https://www.elastic.co/guide/en/elasticsearch/client/php-api/8.7/operations.html</a></p>
<h2>参考</h2>
<p><a href="https://www.elastic.co/guide/index.html">https://www.elastic.co/guide/index.html</a></p></div>]]></description>
            <guid isPermaLink="false">初步分析 Elasticsearch 文档</guid>
        </item>
        <item>
            <title><![CDATA[Laravel 使用 Json Web Token(JWT)]]></title>
            <description><![CDATA[<link rel="stylesheet" href="https://www.cuiwei.net/static/css/github-markdown.min.css?t=20260430" type="text/css" media="screen" /><div class="markdown-body"><p>关于 JWT 之前写过</p>
<p><a href="https://www.cuiwei.net/p/1712912446">php - Json Web Token(JWT)的使用</a></p>
<p><a href="https://www.cuiwei.net/p/1355270275">go - gin 使用 Json Web Token(JWT)</a></p>
<p>今天总结下 Laravel 中 JWT 的使用</p>
<h2>安装</h2>
<pre><code>composer require tymon/jwt-auth

#发布配置
php artisan vendor:publish --provider="Tymon\JWTAuth\Providers\LaravelServiceProvider"

#生成密钥，这将更新您的.env文件，例如JWT_SECRET=foobar
php artisan jwt:secret
</code></pre>
<h2>快速开始</h2>
<h3>更新你的 User model</h3>
<p>首先，您需要在<code>User model</code>上实现<code>Tymon\JWTAuth\Contracts\JWTSubject</code>合同，这要求您实现2种方法<code>getJWTIdentifier()</code>和<code>getJWTCustomClaims()</code></p>
<p>下面的示例应该能让您了解这会是什么样子。显然，你应该根据需要做任何改变，以满足你自己的需求。</p>
<pre><code>&lt;?php

namespace App;

use Tymon\JWTAuth\Contracts\JWTSubject;
use Illuminate\Notifications\Notifiable;
use Illuminate\Foundation\Auth\User as Authenticatable;

class User extends Authenticatable implements JWTSubject
{
    use Notifiable;

    // Rest omitted for brevity

    /**
     * Get the identifier that will be stored in the subject claim of the JWT.
     *
     * @return mixed
     */
    public function getJWTIdentifier()
    {
        return $this-&gt;getKey();//默认取的是主键id：$this-&gt;id
    }

    /**
     * Return a key value array, containing any custom claims to be added to the JWT.
     *
     * @return array
     */
    public function getJWTCustomClaims()
    {
        //一些附加信息，不要太敏感，因为这些信息是可以从token里解析出来的，即使不知道签名
        return ['name'=&gt;$this-&gt;name, 'email'=&gt;$this-&gt;email];
    }
}</code></pre>
<p>配置看守器</p>
<pre><code>'defaults' =&gt; [
    'guard' =&gt; 'api',
    'passwords' =&gt; 'users',
],

...

'guards' =&gt; [
    'api' =&gt; [
        'driver' =&gt; 'jwt',
        'provider' =&gt; 'users',
    ],
],</code></pre>
<p>添加一些基本的身份验证路由</p>
<pre><code>Route::group(['middleware' =&gt; 'api', 'prefix' =&gt; 'auth/jwt'], function () {
    Route::post('login', [AuthJWTController::class, 'login']);
    Route::post('logout', [AuthJWTController::class, 'logout']);
    Route::post('refresh', [AuthJWTController::class, 'refresh']);
    Route::get('me', [AuthJWTController::class, 'me']);
});</code></pre>
<p>控制器</p>
<pre><code>&lt;?php

namespace App\Http\Controllers;

use Illuminate\Support\Facades\Auth;
use App\Http\Controllers\Controller;

class AuthController extends Controller
{
    /**
     * Create a new AuthController instance.
     *
     * @return void
     */
    public function __construct()
    {
        $this-&gt;middleware('auth:api', ['except' =&gt; ['login']]);
    }

    /**
     * Get a JWT via given credentials.
     *
     * @return \Illuminate\Http\JsonResponse
     */
    public function login()
    {
        $credentials = request(['email', 'password']);

        if (! $token = auth()-&gt;attempt($credentials)) {
            return response()-&gt;json(['error' =&gt; 'Unauthorized'], 401);
        }

        return $this-&gt;respondWithToken($token);
    }

    /**
     * Get the authenticated User.
     *
     * @return \Illuminate\Http\JsonResponse
     */
    public function me()
    {
        return response()-&gt;json(auth()-&gt;user());
    }

    /**
     * Log the user out (Invalidate the token).
     *
     * @return \Illuminate\Http\JsonResponse
     */
    public function logout()
    {
        auth()-&gt;logout();

        return response()-&gt;json(['message' =&gt; 'Successfully logged out']);
    }

    /**
     * Refresh a token.
     *
     * @return \Illuminate\Http\JsonResponse
     */
    public function refresh()
    {
        return $this-&gt;respondWithToken(auth()-&gt;refresh());
    }

    /**
     * Get the token array structure.
     *
     * @param  string $token
     *
     * @return \Illuminate\Http\JsonResponse
     */
    protected function respondWithToken($token)
    {
        return response()-&gt;json([
            'access_token' =&gt; $token,
            'token_type' =&gt; 'bearer',
            'expires_in' =&gt; auth()-&gt;factory()-&gt;getTTL() * 60
        ]);
    }
}</code></pre>
<p>您现在应该能够使用一些有效的凭据POST到登录端点（例如http://example.dev/auth/jwt/login），并看到这样的响应：</p>
<pre><code>{
    "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ",
    "token_type": "bearer",
    "expires_in": 3600
}</code></pre>
<h2>已知问题</h2>
<p><del>jwt本身不依赖缓存（注销功能依赖缓存）</del></p>
<ul>
<li>jwt不能互踢
连续创建两个token，a，b：</li>
</ul>
<p>a没发起请求</p>
<p>b发起请求，然后注销，a依然可用</p>
<p>解决办法：</p>
<p>既然支持注销token，那我把之前生成过的token都注销，只保留最新的一个，这样不就实现了互踢？</p>
<p>想法是可以的，只是有一个小问题：没有token生成记录？那就在生成token后记录一下</p>
<h3>注销功能分析</h3>
<blockquote>
<p>jti 是 JWT 的一个唯一标识符，主要用来作为一次性 token，从而回避重放（replay）攻击。jti 的值区分大小写。此声明可选。</p>
</blockquote>
<p>token中包含<code>jti</code>参数，注销的时候会吧<code>jti</code>添加到缓存中（黑名单），并设置到期时间（即<code>token</code>到期时间）；下次再拿这个<code>token</code>来请求，系统会先查黑名单，如果存在就提示授权未通过</p>
<h2>参考</h2>
<p><a href="https://jwt.io">在线解析JWT token</a></p>
<p><a href="https://jwt-auth.readthedocs.io/en/develop/">https://jwt-auth.readthedocs.io/en/develop/</a></p>
<p><a href="https://github.com/tymondesigns/jwt-auth">https://github.com/tymondesigns/jwt-auth</a></p></div>]]></description>
            <guid isPermaLink="false">Laravel 使用 Json Web Token(JWT)</guid>
        </item>
        <item>
            <title><![CDATA[Laravel Sanctum API 授权]]></title>
            <description><![CDATA[<link rel="stylesheet" href="https://www.cuiwei.net/static/css/github-markdown.min.css?t=20260430" type="text/css" media="screen" /><div class="markdown-body"><blockquote>
<p>Laravel Sanctum 为 SPA（单页应用程序）、移动应用程序和基于令牌的、简单的 API 提供轻量级身份验证系统。Sanctum 允许应用程序的每个用户为他们的帐户生成多个 API 令牌。这些令牌可以被授予指定允许令牌执行哪些操作的能力 / 范围。</p>
</blockquote>
<p>简单来说，前后端分离的项目，使用 token 验证登陆状态，可以选它；另外，同类型的还有 jwt 比较火</p>
<h2>安装</h2>
<p>Laravel 9 已经包含了 Laravel Sanctum，所以下面的步骤看看就行了</p>
<pre><code>composer require laravel/sanctum
php artisan vendor:publish --provider="Laravel\Sanctum\SanctumServiceProvider"
php artisan migrate</code></pre>
<p>接下来，如果您想利用 Sanctum 对 SPA 进行身份验证，您应该将 Sanctum 的中间件添加到您应用的 <code>app/Http/Kernel.php</code> 文件中的 api 中间件组中：</p>
<pre><code>'api' =&gt; [
    \Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful::class,
    'throttle:api',
    \Illuminate\Routing\Middleware\SubstituteBindings::class,
],</code></pre>
<blockquote>
<p>注意，EnsureFrontendRequestsAreStateful 这一行，Laravel 9默认是注释掉的，需要取消注释</p>
</blockquote>
<h2>API 令牌认证</h2>
<h3>发布 API Tokens</h3>
<p>要开始为用户颁发令牌，你的 User 模型应使用 <code>Laravel\Sanctum\HasApiTokens</code> trait：</p>
<pre><code>use Laravel\Sanctum\HasApiTokens;

class User extends Authenticatable
{
    use HasApiTokens, HasFactory, Notifiable;
}</code></pre>
<blockquote>
<p>Laravel 9已经默认添加了</p>
</blockquote>
<p>要发布令牌，你可以使用 createToken 方法。 createToken 方法返回一个 Laravel\Sanctum\NewAccessToken 实例。 在存入数据库之前，API 令牌已使用 SHA-256 哈希加密过，但你可以使用 NewAccessToken 实例的 plainTextToken 属性访问令牌的纯文本值。创建令牌后，你应该立即向用户显示此值：</p>
<pre><code>$token = $request-&gt;user()-&gt;createToken($request-&gt;token_name);
return ['token' =&gt; $token-&gt;plainTextToken];</code></pre>
<p>你可以使用 HasApiTokens trait 提供的 tokens Eloquent 关系访问用户的所有令牌：</p>
<pre><code>foreach ($user-&gt;tokens as $token) {
    //
}</code></pre>
<h3>令牌能力</h3>
<p>Sanctum 允许你将 「能力」分配给令牌。能力的用途与 OAuth 的「Scope」类似。你可以将字符串能力数组作为第二个参数传递给 createToken 方法：</p>
<pre><code>return $user-&gt;createToken('token-name', ['server:update'])-&gt;plainTextToken;</code></pre>
<p>在处理由 Sanctum 验证的传入请求时，你可以使用 tokenCan 方法确定令牌是否具有给定的能力：</p>
<pre><code>if ($user-&gt;tokenCan('server:update')) {
    //
}</code></pre>
<h3>令牌能力中间件</h3>
<h3>保护路由</h3>
<pre><code>use Illuminate\Http\Request;

Route::middleware('auth:sanctum')-&gt;get('/user', function (Request $request) {
    return $request-&gt;user();
});</code></pre>
<h3>撤销令牌</h3>
<pre><code>// 撤销所有令牌...
$user-&gt;tokens()-&gt;delete();

// 撤销用于验证当前请求的令牌...
$request-&gt;user()-&gt;currentAccessToken()-&gt;delete();

// 撤销指定令牌...
$user-&gt;tokens()-&gt;where('id', $tokenId)-&gt;delete();</code></pre>
<h3>令牌有效期</h3>
<p>默认情况下，sanctum 的 token 无过期时限并且仅能通过撤销令牌来使它无效。当然如果您想在您的程序里设置 token 的有效期也是可以的。修改 sanctum 的配置文件中的 expiration 选项（默认为 null），此选项设置的数字表示多少分钟后过期：</p>
<pre><code>// 365天后过期
'expiration'  =&gt;  525600,</code></pre>
<p>如果您的程序中配置了 token 的过期时间，那您多半会希望能用任务调度自动删除过期了的 token 数据。有个好消息，sanctum 提供了一个 Artisan 命令，可以实现这个想法：</p>
<pre><code>    php artisan sanctum:prune-expired</code></pre>
<p>比如，您可以设置一个调度任务用于删除你数据库中所有过期超过 24 小时的 token 记录：</p>
<pre><code>$schedule-&gt;command('sanctum:prune-expired --hours=24')-&gt;daily();</code></pre>
<h2>SPA 认证</h2>
<p>这块应该是混合开发模式，再议。。</p>
<h2>移动应用身份验证</h2>
<h2>测试</h2>
<p>在测试时，Sanctum::actingAs 方法可用于验证用户并指定为其令牌授予哪些能力：</p>
<pre><code>use App\Models\User;
use Laravel\Sanctum\Sanctum;

public function test_task_list_can_be_retrieved()
{
    Sanctum::actingAs(
        User::factory()-&gt;create(),
        ['view-tasks']
    );

    $response = $this-&gt;get('/api/task');

    $response-&gt;assertOk();
}</code></pre>
<p>如果你想授予令牌所有的能力，你应该在提供给 actingAs 方法的能力列表中包含 *：</p>
<pre><code>Sanctum::actingAs(
    User::factory()-&gt;create(),
    ['*']
);</code></pre>
<h2>待解决的问题</h2>
<p>token失效后，会报</p>
<pre><code>Route [login] not defined.</code></pre>
<p>只有增加header头才会触发授权异常</p>
<pre><code>Accept:application/json</code></pre>
<h2>参考</h2>
<p><a href="https://www.fujuhao.com/posts/laravel-sanctum.html">https://www.fujuhao.com/posts/laravel-sanctum.html</a></p>
<p><a href="https://learnku.com/docs/laravel/9.x/sanctum/12272">https://learnku.com/docs/laravel/9.x/sanctum/12272</a></p></div>]]></description>
            <guid isPermaLink="false">Laravel Sanctum API 授权</guid>
        </item>
        <item>
            <title><![CDATA[Laravel 表单验证失败跳首页的解决办法]]></title>
            <description><![CDATA[<link rel="stylesheet" href="https://www.cuiwei.net/static/css/github-markdown.min.css?t=20260430" type="text/css" media="screen" /><div class="markdown-body"><p>首先，官方不觉得这是一个问题</p>
<pre><code>如果在传统 HTTP 请求期间验证失败，则会生成对先前 URL 的重定向响应。如果传入的请求是 XHR，将将返回包含验证错误信息的 JSON 响应。</code></pre>
<p><a href="https://learnku.com/docs/laravel/9.x/validation/12219#quick-writing-the-validation-logic">https://learnku.com/docs/laravel/9.x/validation/12219#quick-writing-the-validation-logic</a></p>
<h2>问题复现</h2>
<pre><code>cuiwei@weideMacBook-Pro ~ % curl -X POST 'http://laravel.cw.net/api/login' \
--header 'Content-Type: application/json' \
--data '{
    "email1": "11@qq.com",
    "password": "a"
}'

...
        Redirecting to &lt;a href="http://laravel.cw.net"&gt;http://laravel.cw.net&lt;/a&gt;.
    &lt;/body&gt;
&lt;/html&gt;</code></pre>
<p>如上，一个正常的请求，因为参数错误，跳首页去了。。</p>
<p>按照官方的说法，模拟 XHR 请求，即增加 header 头<code>X-Requested-With: XMLHttpRequest</code></p>
<pre><code>cuiwei@weideMacBook-Pro ~ % curl -X POST 'http://laravel.cw.net/api/login' \
--header 'Content-Type: application/json' \
--header 'X-Requested-With: XMLHttpRequest' \
--data '{
    "email1": "11@qq.com",
    "password": "a"
}'
{"message":"The email field is required.","errors":{"email":["The email field is required."]}}</code></pre>
<p>这下符合预期了。如果这个项目只是前端对接，默认就是ajax请求，很合理。</p>
<p>但我这个项目有 <code>Android</code>端 和 <code>iOS</code>端，让他们额外加这么一个参数就不合适了。</p>
<h2>解决方案</h2>
<h3>方案1</h3>
<p>重写 <code>failedValidation</code> 方法</p>
<pre><code>&lt;?php

namespace App\Http\Requests;

use Illuminate\Contracts\Validation\Validator;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Http\Exceptions\HttpResponseException;

class BaseRequests extends FormRequest
{
    /**
     * validate验证失败模板
     * @param Validator $validator
     */
    protected function failedValidation(Validator $validator)
    {
        $message = '';
        foreach (json_decode(json_encode($validator-&gt;errors()),1) as $error){
            $message = $error[0];
            break;
        }
        throw (new HttpResponseException(response()-&gt;json([
            'code' =&gt; 400,
            'msg'  =&gt; $message,
            'data' =&gt; []
        ])));
    }
}</code></pre>
<p><a href="https://blog.csdn.net/woshissss/article/details/120397036">https://blog.csdn.net/woshissss/article/details/120397036</a></p>
<h3>方案2</h3>
<p>方案1我没试，借鉴网上的。重点是方案2</p>
<p>拦截<code>ValidationException</code>异常</p>
<pre><code>&lt;?php
namespace App\Exceptions;

class Handler extends ExceptionHandler
{
    public function render($request, Throwable $e)
    {
        if ($e instanceof ValidationException) {
            //errorValidate为自定义的统一输出，重点是你拿到了$e
            return $this-&gt;errorValidate($e-&gt;errors(), $e-&gt;getCode(), $e-&gt;getMessage());
        }
        //其他异常
        if ($e instanceof NotFoundHttpException) {
            return $this-&gt;errorNotFound();
        }
        return $this-&gt;error(new \ArrayObject(), $e-&gt;getCode() ?: 400, $e-&gt;getMessage());
    }
}
</code></pre></div>]]></description>
            <guid isPermaLink="false">Laravel 表单验证失败跳首页的解决办法</guid>
        </item>
        <item>
            <title><![CDATA[laravel 代码提示 - laravel-ide-helper]]></title>
            <description><![CDATA[<link rel="stylesheet" href="https://www.cuiwei.net/static/css/github-markdown.min.css?t=20260430" type="text/css" media="screen" /><div class="markdown-body"><p>在开发过程中，可能会遇到有些代码不能跳转，如下图</p>
<p><img src="https://www.cuiwei.net/data/upload/2023-10-19/169770998843836.jpg" alt="20231014222415.jpg" /></p>
<p>laravel-ide-helper 可以解决这个问题。</p>
<h2>使用</h2>
<pre><code>composer require --dev barryvdh/laravel-ide-helper
#低版本Laravel 5.5
composer require --dev barryvdh/laravel-ide-helper v2.4.1

php artisan ide-helper:generate
php artisan ide-helper:meta

#模型注释
composer require --dev doctrine/dbal
php artisan ide-helper:models</code></pre>
<p>如果用的Lumen框架，执行命令可能会看到这样的错误</p>
<pre><code>root@c15f4a9bc298:/var/www/lumen-demo# php artisan ide-helper:meta

   ERROR  There are no commands defined in the "ide-helper" namespace.  
</code></pre>
<p>解决办法：</p>
<p>在<code>bootstrap/app.php</code>文件注册一下就好了</p>
<pre><code>$app-&gt;register(Barryvdh\LaravelIdeHelper\IdeHelperServiceProvider::class);</code></pre>
<p><a href="https://github.com/barryvdh/laravel-ide-helper">https://github.com/barryvdh/laravel-ide-helper</a></p></div>]]></description>
            <guid isPermaLink="false">laravel 代码提示 - laravel-ide-helper</guid>
        </item>
        <item>
            <title><![CDATA[Laravel 用户认证]]></title>
            <description><![CDATA[<link rel="stylesheet" href="https://www.cuiwei.net/static/css/github-markdown.min.css?t=20260430" type="text/css" media="screen" /><div class="markdown-body"><p>应用的身份认证一般包含两种：<code>web 浏览器认证</code>和<code>API 认证</code></p>
<p>基于 web 浏览器的身份验证：常见于前后端混合开发的项目，php混合html模版；使用<code>session</code>+<code>cookie</code>完成身份验证。现在很少见了</p>
<p>基于 api 的身份验证：常见于前后端分离的项目，一套api同时给前端，Android，iOS提供服务；使用<code>token</code>完成身份验证。也是当下最流行的开发模式</p>
<blockquote>
<p>在其核心，Laravel 的用户认证是由「看守器」和「提供器」。看守器定义如何对每个请求的用户进行身份验证。例如，Laravel 附带了一个 session 守护程序，它使用 session 存储和 cookie 来维护状态。</p>
<p>提供器定义如何从持久存储中检索用户。Laravel 支持使用 Eloquent 和数据库查询生成器检索用户。不仅如此，你甚至可以根据应用程序的需要自由定制其他提供程序。</p>
</blockquote>
<p>下面介绍都是基于 api 的身份验证</p>
<h2>手动验证用户</h2>
<pre><code>        $credentials = $request-&gt;validate([
            'email' =&gt; ['required', 'email'],
            'password' =&gt; ['required'],
        ]);

        if (Auth::attempt($credentials)) {
            $request-&gt;session()-&gt;regenerate();

            return redirect()-&gt;intended('dashboard');
        }</code></pre>
<p><code>Auth::attempt</code>方法会做两件事：</p>
<ul>
<li>查询用户：除了password以外的字段都会作为查询条件</li>
<li>比对密码：明文密码即可，因为框架将该值与数据库中的散列密码进行比较之前会自动加密</li>
</ul>
<p>以上两个操作都成功才会返回true</p>
<p>源码位置：</p>
<pre><code>vendor/laravel/framework/src/Illuminate/Contracts/Auth/StatefulGuard.php

    public function attempt(array $credentials = [], $remember = false);</code></pre>
<h3>访问特定的看守器实例</h3>
<p>传递给 guard 方法的名称应存在 auth.php 配置文件中</p>
<pre><code>if (Auth::guard('admin')-&gt;attempt($credentials)) {
    // ...
}</code></pre>
<h3>记住用户</h3>
<p>users 表必须包含字符串 remember_token 列</p>
<p>过时的功能。。</p>
<h3>其他认证方法</h3>
<pre><code>use Illuminate\Support\Facades\Auth;

Auth::login($user);

Auth::login($user, $remember = true);
Auth::guard('admin')-&gt;login($user);

Auth::loginUsingId(1);
Auth::loginUsingId(1, $remember = true);

if (Auth::once($credentials)) {
    //
}
</code></pre>
<h2>HTTP Basic 用户认证</h2>
<p>不常用，再议。。</p>
<h2>退出登录</h2>
<p>要在应用程序中手动注销用户，可以使用 Auth facade 提供的 logout 方法。</p>
<pre><code>Auth::logout();</code></pre>
<h2>添加自定义的看守器</h2>
<p>你可以使用 Auth facade 上的 extend 方法定义自己的身份验证看守器。你应该在 服务提供器 中调用 extend 方法。 由于 Laravel 已经附带了 AuthServiceProvider，因此我们可以将代码放置在该提供程序中：</p>
<pre><code>&lt;?php

namespace App\Providers;

use App\Services\Auth\JwtGuard;
use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;
use Illuminate\Support\Facades\Auth;

class AuthServiceProvider extends ServiceProvider
{
    /**
     * 注册任意的身份验证和身份授权的服务
     *
     * @return void
     */
    public function boot()
    {
        $this-&gt;registerPolicies();

        Auth::extend('jwt', function ($app, $name, array $config) {
            // 返回 Illuminate\Contracts\Auth\Guard 的实例 ...

            return new JwtGuard(Auth::createUserProvider($config['provider']));
        });
    }
}</code></pre>
<p>正如你在上面的示例中所看到的，传递给 extend 方法的回调应该返回 Illuminate\Contracts\Auth\Guard 的实例。此接口包含一些方法，你需要实现这些方法来定义自定义看守器。一旦你的自定义看守器被定义，你就可以在你的应用程序 auth.php 配置文件的 guards 配置中引用该看守器：</p>
<pre><code>'guards' =&gt; [
    'api' =&gt; [
        'driver' =&gt; 'jwt',
        'provider' =&gt; 'users',
    ],
],</code></pre>
<h3>闭包请求看守器</h3>
<p>实现自定义的、基于 HTTP 请求的身份验证系统的最简单方法是使用 Auth::viaRequest 方法。此方法允许你使用单个闭包快速定义身份验证过程。</p>
<p>首先，请在您的 AuthServiceProvider 的 boot 方法中调用 Auth::viaRequest 方法。 VIASRequest 方法接受身份验证驱动程序名称作为其第一个参数。此名称可以是描述自定义看守器的任何字符串。传递给方法的第二个参数应该是一个闭包，该闭包接收传入的 HTTP 请求并返回用户实例，或者，如果验证失败返回 null:</p>
<pre><code>use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;

/**
 * 注册任何应用程序验证 / 授权服务。
 *
 * @return void
 */
public function boot()
{
    $this-&gt;registerPolicies();

    Auth::viaRequest('custom-token', function (Request $request) {
        return User::where('token', $request-&gt;token)-&gt;first();
    });
}</code></pre>
<p>一旦你定义自定义身份验证驱动程序，就可以将其配置为 auth.php 配置文件：</p>
<pre><code>'guards' =&gt; [
    'api' =&gt; [
        'driver' =&gt; 'custom-token',
    ],
],</code></pre>
<h2>添加自定义的用户提供器</h2>
<p>如果不使用传统的关系数据库来存储用户，则需要使用自己的身份验证用户提供程序来扩展 Laravel 。 我们将使用 Auth facade 上的 provider 方法来定义自定义用户提供器。提供器解析器应返回 Illuminate\Contracts\Auth\UserProvider 的实例：</p>
<pre><code>&lt;?php

namespace App\Providers;

use App\Extensions\MongoUserProvider;
use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;
use Illuminate\Support\Facades\Auth;

class AuthServiceProvider extends ServiceProvider
{
    /**
     * 注册任何应用程序验证 / 授权服务。
     *
     * @return void
     */
    public function boot()
    {
        $this-&gt;registerPolicies();

        Auth::provider('mongo', function ($app, array $config) {
            // 返回 illighte\Contracts\Auth\UserProvider 的实例...

            return new MongoUserProvider($app-&gt;make('mongo.connection'));
        });
    }
}</code></pre>
<p>使用 provider 方法注册提供程序后，你可以在 auth.php 配置文件中切换到新的提供程序。 首先，定义一个使用新驱动程序的 provider :</p>
<pre><code>'providers' =&gt; [
    'users' =&gt; [
        'driver' =&gt; 'mongo',
    ],
],</code></pre>
<h3>用户提供器契约</h3>
<p>建议看原文档</p>
<h3>Authenticatable 契约</h3>
<p>建议看原文档</p>
<h2>参考</h2>
<p><a href="https://learnku.com/docs/laravel/9.x/authentication/12239">https://learnku.com/docs/laravel/9.x/authentication/12239</a></p></div>]]></description>
            <guid isPermaLink="false">Laravel 用户认证</guid>
        </item>
        <item>
            <title><![CDATA[docker-compose 快速部署 Soketi]]></title>
            <description><![CDATA[<link rel="stylesheet" href="https://www.cuiwei.net/static/css/github-markdown.min.css?t=20260430" type="text/css" media="screen" /><div class="markdown-body"><h2>docker-compose</h2>
<pre><code>version: '3'
# 使用外部网络
# docker network create server_web-network
networks:
  server_web-network:
    external: true

services:
  docker-soketi:
    image: 'quay.io/soketi/soketi:1.5.0-16-debian'
#    environment:
#      SOKETI_DEBUG: '1'
#      SOKETI_METRICS_SERVER_PORT: '9601'
#      SOKETI_DEFAULT_APP_ID: '12345'
#      SOKETI_DEFAULT_APP_KEY: 'ABCDEFG'
#      SOKETI_DEFAULT_APP_SECRET: 'HIJKLMNOP'
    command: ["node", "/app/bin/server.js", "start", "--config=/app/config.json"]
    volumes:
#      - ./data/app:/app
#      - ./data/app:/backup
#      - ./data/.env:/app/.env
      - ./data/config.json:/app/config.json
    ports:
      - '6001:6001'
      - '9601:9601'
    networks:
      - server_web-network</code></pre>
<h2>环境变量</h2>
<p>支持的环境变量</p>
<pre><code># cat app/dist/cli/cli.js

        this.envVariables = {
            ADAPTER_DRIVER: 'adapter.driver',
            ADAPTER_CLUSTER_REQUESTS_TIMEOUT: 'adapter.cluster.requestsTimeout',
            ADAPTER_REDIS_PREFIX: 'adapter.redis.prefix',
            ADAPTER_REDIS_CLUSTER_MODE: 'adapter.redis.clusterMode',
            ADAPTER_REDIS_REQUESTS_TIMEOUT: 'adapter.redis.requestsTimeout',
            ADAPTER_REDIS_SUB_OPTIONS: 'adapter.redis.redisSubOptions',
            ADAPTER_REDIS_PUB_OPTIONS: 'adapter.redis.redisPubOptions',
            ADAPTER_NATS_PREFIX: 'adapter.nats.prefix',
            ADAPTER_NATS_SERVERS: 'adapter.nats.servers',
            ADAPTER_NATS_USER: 'adapter.nats.user',
            ADAPTER_NATS_PASSWORD: 'adapter.nats.pass',
            ADAPTER_NATS_TOKEN: 'adapter.nats.token',
            ADAPTER_NATS_TIMEOUT: 'adapter.nats.timeout',
            ADAPTER_NATS_REQUESTS_TIMEOUT: 'adapter.nats.requestsTimeout',
            ADAPTER_NATS_NODES_NUMBER: 'adapter.nats.nodesNumber',
            APP_MANAGER_DRIVER: 'appManager.driver',
            APP_MANAGER_CACHE_ENABLED: 'appManager.cache.enabled',
            APP_MANAGER_CACHE_TTL: 'appManager.cache.ttl',
            APP_MANAGER_DYNAMODB_TABLE: 'appManager.dynamodb.table',
            APP_MANAGER_DYNAMODB_REGION: 'appManager.dynamodb.region',
            APP_MANAGER_DYNAMODB_ENDPOINT: 'appManager.dynamodb.endpoint',
            APP_MANAGER_MYSQL_TABLE: 'appManager.mysql.table',
            APP_MANAGER_MYSQL_VERSION: 'appManager.mysql.version',
            APP_MANAGER_POSTGRES_TABLE: 'appManager.postgres.table',
            APP_MANAGER_POSTGRES_VERSION: 'appManager.postgres.version',
            APP_MANAGER_MYSQL_USE_V2: 'appManager.mysql.useMysql2',
            CHANNEL_LIMITS_MAX_NAME_LENGTH: 'channelLimits.maxNameLength',
            CHANNEL_CACHE_TTL: 'channelLimits.cacheTtl',
            CACHE_DRIVER: 'cache.driver',
            CACHE_REDIS_CLUSTER_MODE: 'cache.redis.clusterMode',
            CACHE_REDIS_OPTIONS: 'cache.redis.redisOptions',
            CLUSTER_CHECK_INTERVAL: 'cluster.checkInterval',
            CLUSTER_HOST: 'cluster.hostname',
            CLUSTER_IGNORE_PROCESS: 'cluster.ignoreProcess',
            CLUSTER_BROADCAST_ADDRESS: 'cluster.broadcast',
            CLUSTER_KEEPALIVE_INTERVAL: 'cluster.helloInterval',
            CLUSTER_MASTER_TIMEOUT: 'cluster.masterTimeout',
            CLUSTER_MULTICAST_ADDRESS: 'cluster.multicast',
            CLUSTER_NODE_TIMEOUT: 'cluster.nodeTimeout',
            CLUSTER_PORT: 'cluster.port',
            CLUSTER_PREFIX: 'cluster.prefix',
            CLUSTER_UNICAST_ADDRESSES: 'cluster.unicast',
            DEBUG: 'debug',
            DEFAULT_APP_ID: 'appManager.array.apps.0.id',
            DEFAULT_APP_KEY: 'appManager.array.apps.0.key',
            DEFAULT_APP_SECRET: 'appManager.array.apps.0.secret',
            DEFAULT_APP_MAX_CONNS: 'appManager.array.apps.0.maxConnections',
            DEFAULT_APP_ENABLE_CLIENT_MESSAGES: 'appManager.array.apps.0.enableClientMessages',
            DEFAULT_APP_ENABLED: 'appManager.array.apps.0.enabled',
            DEFAULT_APP_MAX_BACKEND_EVENTS_PER_SEC: 'appManager.array.apps.0.maxBackendEventsPerSecond',
            DEFAULT_APP_MAX_CLIENT_EVENTS_PER_SEC: 'appManager.array.apps.0.maxClientEventsPerSecond',
            DEFAULT_APP_MAX_READ_REQ_PER_SEC: 'appManager.array.apps.0.maxReadRequestsPerSecond',
            DEFAULT_APP_USER_AUTHENTICATION: 'appManager.array.apps.0.enableUserAuthentication',
            DEFAULT_APP_WEBHOOKS: 'appManager.array.apps.0.webhooks',
            DB_POOLING_ENABLED: 'databasePooling.enabled',
            DB_POOLING_MIN: 'databasePooling.min',
            DB_POOLING_MAX: 'databasePooling.max',
            DB_MYSQL_HOST: 'database.mysql.host',
            DB_MYSQL_PORT: 'database.mysql.port',
            DB_MYSQL_USERNAME: 'database.mysql.user',
            DB_MYSQL_PASSWORD: 'database.mysql.password',
            DB_MYSQL_DATABASE: 'database.mysql.database',
            DB_POSTGRES_HOST: 'database.postgres.host',
            DB_POSTGRES_PORT: 'database.postgres.port',
            DB_POSTGRES_USERNAME: 'database.postgres.user',
            DB_POSTGRES_PASSWORD: 'database.postgres.password',
            DB_POSTGRES_DATABASE: 'database.postgres.database',
            DB_REDIS_HOST: 'database.redis.host',
            DB_REDIS_PORT: 'database.redis.port',
            DB_REDIS_DB: 'database.redis.db',
            DB_REDIS_USERNAME: 'database.redis.username',
            DB_REDIS_PASSWORD: 'database.redis.password',
            DB_REDIS_KEY_PREFIX: 'database.redis.keyPrefix',
            DB_REDIS_SENTINELS: 'database.redis.sentinels',
            DB_REDIS_SENTINEL_PASSWORD: 'database.redis.sentinelPassword',
            DB_REDIS_CLUSTER_NODES: 'database.redis.clusterNodes',
            DB_REDIS_INSTANCE_NAME: 'database.redis.name',
            EVENT_MAX_BATCH_SIZE: 'eventLimits.maxBatchSize',
            EVENT_MAX_CHANNELS_AT_ONCE: 'eventLimits.maxChannelsAtOnce',
            EVENT_MAX_NAME_LENGTH: 'eventLimits.maxNameLength',
            EVENT_MAX_SIZE_IN_KB: 'eventLimits.maxPayloadInKb',
            HOST: 'host',
            HTTP_ACCEPT_TRAFFIC_MEMORY_THRESHOLD: 'httpApi.acceptTraffic.memoryThreshold',
            METRICS_ENABLED: 'metrics.enabled',
            METRICS_DRIVER: 'metrics.driver',
            METRICS_HOST: 'metrics.host',
            METRICS_PROMETHEUS_PREFIX: 'metrics.prometheus.prefix',
            METRICS_SERVER_PORT: 'metrics.port',
            MODE: 'mode',
            PORT: 'port',
            PATH_PREFIX: 'pathPrefix',
            PRESENCE_MAX_MEMBER_SIZE: 'presence.maxMemberSizeInKb',
            PRESENCE_MAX_MEMBERS: 'presence.maxMembersPerChannel',
            QUEUE_DRIVER: 'queue.driver',
            QUEUE_REDIS_CONCURRENCY: 'queue.redis.concurrency',
            QUEUE_REDIS_OPTIONS: 'queue.redis.redisOptions',
            QUEUE_REDIS_CLUSTER_MODE: 'queue.redis.clusterMode',
            QUEUE_SQS_REGION: 'queue.sqs.region',
            QUEUE_SQS_CLIENT_OPTIONS: 'queue.sqs.clientOptions',
            QUEUE_SQS_URL: 'queue.sqs.queueUrl',
            QUEUE_SQS_ENDPOINT: 'queue.sqs.endpoint',
            QUEUE_SQS_PROCESS_BATCH: 'queue.sqs.processBatch',
            QUEUE_SQS_BATCH_SIZE: 'queue.sqs.batchSize',
            QUEUE_SQS_POLLING_WAIT_TIME_MS: 'queue.sqs.pollingWaitTimeMs',
            RATE_LIMITER_DRIVER: 'rateLimiter.driver',
            RATE_LIMITER_REDIS_OPTIONS: 'rateLimiter.redis.redisOptions',
            RATE_LIMITER_REDIS_CLUSTER_MODE: 'rateLimiter.redis.clusterMode',
            SHUTDOWN_GRACE_PERIOD: 'shutdownGracePeriod',
            SSL_CERT: 'ssl.certPath',
            SSL_KEY: 'ssl.keyPath',
            SSL_PASS: 'ssl.passphrase',
            SSL_CA: 'ssl.caPath',
            USER_AUTHENTICATION_TIMEOUT: 'userAuthenticationTimeout',
            WEBHOOKS_BATCHING: 'webhooks.batching.enabled',
            WEBHOOKS_BATCHING_DURATION: 'webhooks.batching.duration',
        };</code></pre>
<h2>docker-compose设置环境变量</h2>
<h3>方式1</h3>
<p>支持的变量见上文，上文变量加<code>SOKETI_</code>前缀即可</p>
<pre><code>#    environment:
#      SOKETI_DEBUG: '1'
#      SOKETI_METRICS_SERVER_PORT: '9601'
#      SOKETI_DEFAULT_APP_ID: '12345'
#      SOKETI_DEFAULT_APP_KEY: 'ABCDEFG'
#      SOKETI_DEFAULT_APP_SECRET: 'HIJKLMNOP'</code></pre>
<h3>方式2</h3>
<p>在<code>/app</code>目录下增加<code>.env</code>文件</p>
<pre><code>    volumes:
      - ./data/.env:/app/.env</code></pre>
<p><code>.env</code>文件大致如下</p>
<pre><code>SOKETI_DEBUG=1
SOKETI_METRICS_SERVER_PORT=9601
SOKETI_DEFAULT_APP_ID=12345
SOKETI_DEFAULT_APP_KEY=ABCDEFG
SOKETI_DEFAULT_APP_SECRET=HIJKLMNOP</code></pre>
<p>支持的变量见上文，上文变量加<code>SOKETI_</code>前缀即可</p>
<h3>方式3</h3>
<p>启动时支持<code>config</code>参数，可以指定一个<code>json</code>文件</p>
<pre><code>    command: ["node", "/app/bin/server.js", "start", "--config=/app/config.json"]</code></pre>
<p><code>json</code>文件大致如下</p>
<pre><code>{
  "debug": true,
  "port": 6001,
  "appManager.array.apps": [
    {
      "id": "12345",
      "key": "ABCDEFG",
      "secret": "HIJKLMNOP",
      "webhooks": [
        {
          "url": "https://...",
          "event_types": ["channel_occupied"]
        }
      ]
    }
  ]
}</code></pre>
<blockquote>
<p>这3种方式，选1种即可，推荐方式3</p>
</blockquote>
<p><a href="https://github.com/chudaozhe/docker-soketi">https://github.com/chudaozhe/docker-soketi</a></p>
<h2>参考</h2>
<p><a href="https://docs.soketi.app/v/soketi-docs/getting-started/environment-variables">https://docs.soketi.app/v/soketi-docs/getting-started/environment-variables</a></p>
<p><a href="https://github.com/soketi/soketi">https://github.com/soketi/soketi</a></p></div>]]></description>
            <guid isPermaLink="false">docker-compose 快速部署 Soketi</guid>
        </item>
        <item>
            <title><![CDATA[php 高精确度运算 - bc函数]]></title>
            <description><![CDATA[<link rel="stylesheet" href="https://www.cuiwei.net/static/css/github-markdown.min.css?t=20260430" type="text/css" media="screen" /><div class="markdown-body"><p>项目中存储金额一般用<code>int</code>(分)，或者<code>decimal(8,2)</code>，如果用 decimal 会涉及到精度问题。比如：比较字符串<code>0.01</code>和<code>0</code>哪个大，结果是一样大，因为php会把<code>0.01</code>强转为<code>0</code>，这就不符合预期了</p>
<pre><code>#两个任意精度的数字除法计算
bcdiv('200', '100', 2);//分转元，200/100

#比较两个任意精度的数字
bccomp($price, $step, 2)
#两个任意精度数字的加法计算
bcadd($price, $step, 2)
#两个任意精度数字的减法
bcsub($price, $step, 2)
//将两个任意精度数字相乘
bcmul($sku-&gt;price, (string) $orderGoods['num'], 2);</code></pre>
<p>例子：根据传过来的金额返回价格上下10元的金额</p>
<pre><code>        $step = '10.00';
        if (bccomp($price, $step, 2) !== 1) {
            $start = '0.00';
        } else {
            $start = bcsub($price, $step, 2);
        }
        $end                      = bcadd($price, $step, 2);
        $params['price']          = [$start, $end];</code></pre>
<h2>参考</h2>
<p><a href="https://www.cnblogs.com/fangdada/p/14793664.html">https://www.cnblogs.com/fangdada/p/14793664.html</a></p>
<p><a href="https://www.php.net/manual/zh/ref.bc.php">https://www.php.net/manual/zh/ref.bc.php</a></p></div>]]></description>
            <guid isPermaLink="false">php 高精确度运算 - bc函数</guid>
        </item>
        <item>
            <title><![CDATA[docker 从容器创建新镜像，及镜像的备份和恢复]]></title>
            <description><![CDATA[<link rel="stylesheet" href="https://www.cuiwei.net/static/css/github-markdown.min.css?t=20260430" type="text/css" media="screen" /><div class="markdown-body"><p>从容器创建新镜像：<code>docker commit</code></p>
<p>备份/恢复镜像：<code>docker save + docker load</code></p>
<p>将容器直接导出为tar包/导入：<code>docker export + docker import</code></p>
<h2>docker commit</h2>
<p>操作的是容器。从容器创建新镜像</p>
<p><a href="https://docs.docker.com/engine/reference/commandline/commit/">https://docs.docker.com/engine/reference/commandline/commit/</a></p>
<pre><code>cuiwei@weideMacBook-Pro server % docker ps                                                  
CONTAINER ID   IMAGE                COMMAND                  CREATED              STATUS              PORTS                                                                                    NAMES
df15d5b449c6   nginx:1.21.3         "/docker-entrypoint.…"   About a minute ago   Up About a minute   0.0.0.0:80-&gt;80/tcp   server-docker-nginx-1

#在此之前，先进入容器进行一些修改
cuiwei@weideMacBook-Pro server % docker commit df15d5b449c6  chudaozhe/nginx:test1
sha256:98f7915e8f85b81d12eadad38dc6124bae858384c03734886432f8dce7ea5c36

cuiwei@weideMacBook-Pro server % docker images                                    
REPOSITORY                                                TAG                                        IMAGE ID       CREATED              SIZE
chudaozhe/nginx                                           test1                                      98f7915e8f85   About a minute ago   133MB
nginx                                                     1.21.3                                     87a94228f133   17 months ago        133MB

#运行一下
cuiwei@weideMacBook-Pro server % docker run -d chudaozhe/nginx:test1          
6417f9eccd033d651351cb1d35004aa63efa163bd2d56be2e16c749d3c062dae

#进入新容器，看下刚才的修改是否还在
#肯定在！</code></pre>
<p>其他命令</p>
<pre><code>cuiwei@weideMacBook-Pro server % docker inspect -f "{{ .Config.Env }}" 6417f9eccd03                                                    
[PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin NGINX_VERSION=1.21.3 NJS_VERSION=0.6.2 PKG_RELEASE=1~buster]

docker commit --change "ENV DEBUG=true" df15d5b449c6 chudaozhe/nginx:test1

docker commit --change='CMD ["apachectl", "-DFOREGROUND"]' -c "EXPOSE 80" df15d5b449c6 chudaozhe/nginx:test1</code></pre>
<h2>docker save + docker load</h2>
<p>操作的是镜像。备份和恢复镜像</p>
<pre><code>cuiwei@weideMacBook-Pro server % docker save nginx:1.21.3 | gzip &gt; nginx_1.21.3.tar.gz
cuiwei@weideMacBook-Pro server % docker load &lt; nginx_1.21.3.tar.gz
43f4e41372e4: Loading layer [==================================================&gt;]  64.97MB/64.97MB
788e89a4d186: Loading layer [==================================================&gt;]  3.072kB/3.072kB
f8e880dfc4ef: Loading layer [==================================================&gt;]  4.096kB/4.096kB
f7e00b807643: Loading layer [==================================================&gt;]  3.584kB/3.584kB
9959a332cf6e: Loading layer [==================================================&gt;]  7.168kB/7.168kB
Loaded image: nginx:1.21.3</code></pre>
<h3>docker save</h3>
<p>将一个或多个图像保存到<code>tar</code>存档（默认流式传输到STDOUT）</p>
<p><a href="https://docs.docker.com/engine/reference/commandline/save/">https://docs.docker.com/engine/reference/commandline/save/</a></p>
<p>创建一个备份，然后与<code>docker load</code>一起使用。</p>
<pre><code>docker save nginx:1.21.3 | gzip &gt; nginx_1.21.3.tar.gz

docker save nginx:1.21.3 &gt; nginx_1.21.3.tar

docker save --output nginx_1.21.3.tar nginx:1.21.3

docker save -o nginx_1.21.3.tar nginx:1.21.3

#同时备份两个标签
docker save -o nginx.tar nginx:1.21.3 nginx:mainline</code></pre>
<h3>docker load</h3>
<p>从<code>tar</code>存档或<code>STDIN</code>加载图像</p>
<p><a href="https://docs.docker.com/engine/reference/commandline/load/">https://docs.docker.com/engine/reference/commandline/load/</a></p>
<pre><code>docker load &lt; nginx_1.21.3.tar.gz

docker load --input nginx_1.21.3.tar.gz</code></pre>
<h2>docker export + docker import</h2>
<p>操作的是容器。从容器创建新镜像，和<code>docker commit</code>导出所有层级不同，它只有一层。</p>
<p>另外运行时要加-t和bash参数，否则无法启动新容器1️⃣</p>
<pre><code>cuiwei@weideMacBook-Pro server % docker export server-docker-nginx-1 &gt; nginx_1.21.3.tar
cuiwei@weideMacBook-Pro server % docker export df15d5b449c6 &gt; nginx_1.21.3_2.tar

#一定要指定镜像名和tag，否则就成虚悬镜像了（仓库名 (镜像名) 和标签 TAG 都是&lt;none&gt;的镜像。）
cuiwei@weideMacBook-Pro server % docker import nginx_1.21.3.tar chudaozhe/nginx:test1
sha256:c7905c3274d23683e66bcab36b860144857faec6bb3813384212002092dc0971

#运行一下，注意：要加-t和bash参数
cuiwei@weideMacBook-Pro server % docker run -d -t chudaozhe/nginx:test1 bash
fb64767ec3dacc97f0ccd26252fa9e51ceebddd453a07a0a7e380a2781c33791</code></pre>
<h3>docker export</h3>
<p>将容器导出为tar存档</p>
<p><a href="https://docs.docker.com/engine/reference/commandline/export/">https://docs.docker.com/engine/reference/commandline/export/</a></p>
<pre><code>docker export server-docker-nginx-1 &gt; nginx_1.21.3.tar
docker export df15d5b449c6 &gt; nginx_1.21.3_2.tar
docker export --output="nginx_1.21.3.tar" server-docker-nginx-1</code></pre>
<h3>docker import</h3>
<p>从tarball导入内容以创建文件系统映像（tarball 即 Tar包(Tarball)）</p>
<p><a href="https://docs.docker.com/engine/reference/commandline/import/">https://docs.docker.com/engine/reference/commandline/import/</a></p>
<pre><code>docker import https://example.com/exampleimage.tgz chudaozhe/nginx:test1

cat exampleimage.tgz | docker import - exampleimagelocal:new

cat exampleimage.tgz | docker import --message "New image imported from tarball" - exampleimagelocal:new

docker import /path/to/exampleimage.tgz chudaozhe/nginx:test1

sudo tar -c . | docker import - exampleimagedir

sudo tar -c . | docker import --change "ENV DEBUG=true" - exampleimagedir</code></pre>
<h2>备份、恢复或迁移数据卷</h2>
<p><a href="https://docs.docker.com/storage/volumes/#back-up-restore-or-migrate-data-volumes">https://docs.docker.com/storage/volumes/#back-up-restore-or-migrate-data-volumes</a></p>
<p>备份</p>
<pre><code>#创建一个名为dbstore的新容器
docker run -v /dbdata --name dbstore ubuntu /bin/bash

#...上面容器运行一段时间，dbdata卷已经产生了数据

#备份dbdata卷到/backup目录
docker run --rm --volumes-from dbstore -v $(pwd):/backup ubuntu tar cvf /backup/backup.tar /dbdata</code></pre>
<p>恢复</p>
<pre><code>#创建一个名为dbstore2的新容器
docker run -v /dbdata --name dbstore2 ubuntu /bin/bash

#恢复备份
docker run --rm --volumes-from dbstore2 -v $(pwd):/backup ubuntu bash -c "cd /dbdata &amp;&amp; tar xvf /backup/backup.tar --strip 1" 2️⃣</code></pre>
<h1>备注</h1>
<p>1️⃣ 比如nginx</p>
<p>docker-compose.yml</p>
<pre><code>...
    restart: always
    tty: true
    command:
      - /bin/bash
      - -c
      - nginx -g "daemon off;"
    volumes:
...</code></pre>
<p>2️⃣ --strip 1：tar解压删除上层目录
需求</p>
<p>使用tar解压文件，需要移除第一层目录，例如</p>
<pre><code>package
└── target
    └── vsomeip.json</code></pre>
<p>转化为</p>
<pre><code>target
└── vsomeip.json</code></pre>
<p>实现</p>
<p>使用<code>tar --strip</code>参数</p>
<pre><code>tar xvf xxxx.tar.gz --directory /your/path --strip 1</code></pre>
<p><a href="https://www.cnblogs.com/azureology/p/15834298.html">https://www.cnblogs.com/azureology/p/15834298.html</a></p></div>]]></description>
            <guid isPermaLink="false">docker 从容器创建新镜像，及镜像的备份和恢复</guid>
        </item>
        <item>
            <title><![CDATA[Laravel 广播]]></title>
            <description><![CDATA[<link rel="stylesheet" href="https://www.cuiwei.net/static/css/github-markdown.min.css?t=20260430" type="text/css" media="screen" /><div class="markdown-body"><h2>安装</h2>
<h3>服务端</h3>
<h4>付费方案</h4>
<ul>
<li>Pusher Channels</li>
<li>Ably</li>
</ul>
<p>这里不作介绍</p>
<h4>开源方案</h4>
<ul>
<li>laravel-websockets</li>
</ul>
<p>安装请移步 <a href="https://www.cuiwei.net/p/1659113677">https://www.cuiwei.net/p/1659113677</a></p>
<ul>
<li>Soketi</li>
</ul>
<p>安装请移步 <a href="https://www.cuiwei.net/p/1093836635">https://www.cuiwei.net/p/1093836635</a></p>
<ul>
<li>Laravel Reverb - Laravel 第一方可扩展的 WebSocket 服务器</li>
</ul>
<p>安装请移步 <a href="https://www.cuiwei.net/p/1502119488">https://www.cuiwei.net/p/1502119488</a></p>
<h3>前端</h3>
<p>安装 <code>laravel-echo</code></p>
<pre><code>npm install --save-dev laravel-echo pusher-js</code></pre>
<h2>以私人频道为例</h2>
<p>场景如下：用户支付完成，前端需要从后端获取支付结果，并展示给用户</p>
<h3>基本流程</h3>
<p>后端</p>
<ul>
<li>配置</li>
<li>注册<code>BroadcastServiceProvider</code></li>
<li>创建广播事件，设置私人频道<code>orders.{order_id}</code></li>
<li>在<code>routes/channels.php</code>完成频道授权</li>
<li>触发广播事件<code>OrderStatusUpdatedEvent::dispatch($order);</code></li>
</ul>
<p>前端</p>
<ul>
<li>实例化了 Laravel Echo</li>
<li>监听<code>orders.{order_id}</code>频道</li>
</ul>
<h2>后端</h2>
<h3>配置</h3>
<p>安装<code>Pusher SDK</code></p>
<pre><code>composer require pusher/pusher-php-server</code></pre>
<p>配置文件 <code>config/broadcasting.php</code>，一般是在<code>.env</code>文件中修改</p>
<pre><code>BROADCAST_DRIVER=pusher

PUSHER_APP_ID=12345
PUSHER_APP_KEY=ABCDEFG
PUSHER_APP_SECRET=HIJKLMNOP
PUSHER_HOST=docker-laravel-websockets
PUSHER_PORT=6001
PUSHER_SCHEME=http
PUSHER_APP_CLUSTER=mt1

VITE_PUSHER_APP_KEY="${PUSHER_APP_KEY}"
VITE_PUSHER_HOST="laravel2.cw.net"
VITE_PUSHER_PORT="${PUSHER_PORT}"
VITE_PUSHER_SCHEME="${PUSHER_SCHEME}"
VITE_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}"</code></pre>
<blockquote>
<p>::: 提示 当使用Laravel WebSockets作为Pumper替换时，之前没有使用过Puscher，您设置什么作为PUSHER_变量并不重要。只要确保它们对每个项目都是独一无二的。 :::</p>
</blockquote>
<h3>注册<code>BroadcastServiceProvider</code></h3>
<p>在广播任何事件之前，您首先需要注册 <code>App\Providers\BroadcastServiceProvider</code>。在新的 <code>Laravel</code> 应用程序中，您只需在 <code>config/app.php</code> 配置文件的 <code>providers</code> 数组中取消注释此提供程序。这个 <code>BroadcastServiceProvider</code> 包含注册广播授权路由和回调所需的代码。</p>
<h3>创建广播事件</h3>
<pre><code>php artisan make:event OrderStatusUpdatedEvent

#修改一下
class OrderStatusUpdatedEvent implements ShouldBroadcast
{
    use Dispatchable, InteractsWithSockets, SerializesModels;

    public Order $order;
    public function __construct(Order $order)
    {
        $this-&gt;order=$order;
    }
    public function broadcastOn()
    {
        //Channel代表任何用户都可以订阅的公共频道
        return new Channel('orders.'.$this-&gt;order-&gt;id);
        //PrivateChannel, PresenceChannel 代表需要 频道授权 的私人频道
        return new PrivateChannel('orders.'.$this-&gt;order-&gt;id);
        return new PresenceChannel('orders.'.$this-&gt;order-&gt;id);
    }
}</code></pre>
<h3>授权频道</h3>
<p>请记住，用户必须获得授权才能在私人频道上收听。我们可以在应用程序的 <code>routes/channels.php</code> 文件中定义我们的频道授权规则。在此示例中，我们需要验证任何尝试在私有 <code>orders.1</code> 频道上收听的用户实际上是订单的创建者：</p>
<pre><code>use App\Models\Order;

Broadcast::channel('orders.{id}', function ($user, string $order_id) {
//    return true;
    \Illuminate\Support\Facades\Log::info($user);
    return $user-&gt;id === Order::findOrNew($order_id)-&gt;user_id;
});</code></pre>
<h3>触发广播事件OrderStatusUpdatedEvent::dispatch($order);</h3>
<pre><code>        Auth::loginUsingId(1, true);
        $order = \App\Models\Order::query()-&gt;first();
        OrderStatusUpdatedEvent::dispatch($order);</code></pre>
<p>如果只是调试一下，也可以用<code>tinker</code></p>
<pre><code>#启动 Laravel 的交互式解释器
php artisan tinker

#执行
event (new \App\Events\NewTrade('test'))</code></pre>
<h2>前端</h2>
<h3>实例化 Laravel Echo</h3>
<p>安装 <code>Echo</code> 后，您就可以在应用程序的 <code>JavaScript</code> 中创建一个新的 <code>Echo</code> 实例了。一个很好的地方是在 Laravel 框架中包含的 <code>resources/js/bootstrap.js</code> 文件的底部。默认情况下，此文件中已包含一个示例 <code>Echo</code> 配置 - 您只需取消注释即可：</p>
<pre><code>import Echo from 'laravel-echo';

import Pusher from 'pusher-js';
window.Pusher = Pusher;

window.Echo = new Echo({
    broadcaster: 'pusher',
    key: import.meta.env.VITE_PUSHER_APP_KEY,
    cluster: import.meta.env.VITE_PUSHER_APP_CLUSTER ?? 'mt1',
    wsHost: import.meta.env.VITE_PUSHER_HOST ? import.meta.env.VITE_PUSHER_HOST : `ws-${import.meta.env.VITE_PUSHER_APP_CLUSTER}.pusher.com`,
    wsPort: import.meta.env.VITE_PUSHER_PORT ?? 80,
    wssPort: import.meta.env.VITE_PUSHER_PORT ?? 443,
    forceTLS: (import.meta.env.VITE_PUSHER_SCHEME ?? 'https') === 'https',
    enabledTransports: ['ws', 'wss'],
});</code></pre>
<h3>监听orders.{order_id}频道</h3>
<p>我选择在项目入口页添加</p>
<pre><code>vi resources/views/welcome.blade.php

        @vite('resources/js/app.js')
        &lt;script&gt;
            window.onload = function(){
                Echo.private(`orders.1`)
                    .listen('OrderStatusUpdatedEvent', (e) =&gt; {
                        console.log('privite:')
                        console.log(e.order);
                    });
                // Echo.channel(`orders.1`)
                //     .listen('OrderStatusUpdatedEvent', (e) =&gt; {
                //         console.log(e.order);
                //     });
            };
        &lt;/script&gt;
    &lt;/head&gt;</code></pre>
<h2>测试一下</h2>
<h3>启动 websockets 服务</h3>
<pre><code>php artisan websockets:serve</code></pre>
<h3>运行 Vite</h3>
<p>Laravel9 不再推荐Mix，而是推荐Vite</p>
<pre><code># 运行 Vite 开发服务器...
npm run dev

# 构建并为生产环境版本化资产...
npm run build</code></pre>
<p>Vite开发服务器，为您的Laravel应用程序提供热更新。默认使用5173端口，访问<code>http://localhost:5173/</code>，就一个普通页面，看看就行了。和你的项目路由没有关系</p>
<p>这个开发服务器将自动检测您文件的改变并在任何打开的浏览器窗口中立即反映它们。</p>
<blockquote>
<p>1、注意：运行dev 会改变js的引入方式</p>
</blockquote>
<p>正常是这样的</p>
<p><img src="https://www.cuiwei.net/data/upload/2023-03-19/167923877945304.jpg" alt="WX202303192305442x.png" /></p>
<p>运行dev 后</p>
<p><img src="https://www.cuiwei.net/data/upload/2023-03-19/167923859118919.jpg" alt="WX202303192304582x.png" /></p>
<blockquote>
<p>2、注意：引入websockets后，运行dev后，控制台日志也会有变化</p>
</blockquote>
<p>正常是看不到<code>[vite] connecting...</code>、<code>[vite] connected.</code>这种日志</p>
<p>运行dev 后，在浏览器控制台会看到</p>
<p><img src="https://www.cuiwei.net/data/upload/2023-03-19/167923888026846.jpg" alt="WX202303192308582x.png" /></p>
<h3>最后</h3>
<p>先访问项目首页<code>http://laravel2.cw.net</code>，并打开 浏览器控制台</p>
<p>然后，执行命令触发广播事件</p>
<pre><code>root@php-fpm:/var/www/laravel-demo2# php artisan order:update</code></pre>
<p>这时你应该可以看到输出：</p>
<p><img src="https://www.cuiwei.net/data/upload/2023-03-19/167924146530755.jpg" alt="WX202303192357082x.png" /></p>
<h2>参考</h2>
<p><a href="https://learnku.com/docs/laravel/9.x/broadcasting/12223#client-side-installation">https://learnku.com/docs/laravel/9.x/broadcasting/12223#client-side-installation</a></p></div>]]></description>
            <guid isPermaLink="false">Laravel 广播</guid>
        </item>
        <item>
            <title><![CDATA[docker-compose 快速部署 laravel-websockets]]></title>
            <description><![CDATA[<link rel="stylesheet" href="https://www.cuiwei.net/static/css/github-markdown.min.css?t=20260430" type="text/css" media="screen" /><div class="markdown-body"><h2>安装</h2>
<pre><code># 通过composer安装
composer require beyondcode/laravel-websockets

# 发布迁移文件
php artisan vendor:publish --provider="BeyondCode\LaravelWebSockets\WebSocketsServiceProvider" --tag="migrations"

# 执行迁移
php artisan migrate

# 发布WebSocket配置文件
php artisan vendor:publish --provider="BeyondCode\LaravelWebSockets\WebSocketsServiceProvider" --tag="config"

//成功后会创建 config/websockets.php 文件
</code></pre>
<h2>启动服务</h2>
<pre><code>php artisan websockets:serve</code></pre>
<p>建议搭配<code>Supervisor</code>使用</p>
<h3>仪表盘</h3>
<p>服务启动成功，可以访问仪表盘 <code>http://laravel.cw.net/laravel-websockets</code></p>
<h2>docker镜像</h2>
<p><code>laravel-websockets</code>官方并没有提供docker镜像，本人构建一个镜像并已上传到<code>hub.docker.com</code>，可以直接使用，要求使用laravel9</p>
<p>docker-compose.yml</p>
<pre><code>version: '3'
# 使用外部网络
# docker network create server_web-network
networks:
  server_web-network:
    external: true

services:
  docker-laravel-websockets:
    image: 'chudaozhe/php:8.1.9-cli-laravel-websockets-v1.0'
    volumes:
      - ./storage:/var/www/app/storage
      - ./app/.env:/var/www/app/.env
    ports:
      - '6001:6001'
    networks:
      - server_web-network</code></pre>
<p><a href="https://github.com/chudaozhe/docker-laravel-websockets">https://github.com/chudaozhe/docker-laravel-websockets</a></p>
<h2>参考</h2>
<p><a href="https://beyondco.de/docs/laravel-websockets/getting-started/introduction">https://beyondco.de/docs/laravel-websockets/getting-started/introduction</a></p></div>]]></description>
            <guid isPermaLink="false">docker-compose 快速部署 laravel-websockets</guid>
        </item>
        <item>
            <title><![CDATA[Laravel 消息通知]]></title>
            <description><![CDATA[<link rel="stylesheet" href="https://www.cuiwei.net/static/css/github-markdown.min.css?t=20260430" type="text/css" media="screen" /><div class="markdown-body"><p><img src="https://www.cuiwei.net/storage/uploads/2024-07-11/172070355257891.jpg" alt="Laravel 消息通知" /></p>
<h2>创建通知</h2>
<pre><code>php artisan make:notification InvoicePaid</code></pre>
<p>这个命令会在 <code>app/Notifications</code> 目录下生成一个新的通知类。每个通知类都包含一个 via 方法以及一个或多个消息构建的方法比如 toMail 或 toDatabase，它们会针对特定的渠道把通知转换为对应的消息。</p>
<h2>发送通知</h2>
<h3>使用 Notifiable Trait</h3>
<p>该方法默认包含在应用程序的 <code>App\Models\User</code> 模型中：</p>
<pre><code>&lt;?php

namespace App\Models;

use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;

class User extends Authenticatable
{
    use Notifiable;
}

use App\Notifications\InvoicePaid;

$user-&gt;notify(new InvoicePaid($invoice));</code></pre>
<blockquote>
<p>你可以在任何模型中使用 Notifiable trait。而不仅仅是在 User 模型中。</p>
</blockquote>
<h3>使用 Notification Facade</h3>
<p>主要用在当你需要给多个可接收通知的实体发送的时候，比如给用户集合发送通知。</p>
<pre><code>$users = User::query()-&gt;get();
Notification::send($users, new InvoicePaid());</code></pre>
<p>您也可以使用 <code>sendNow</code> 方法立即发送通知。即使通知实现了 ShouldQueue 接口，该方法也会立即发送通知：</p>
<pre><code>Notification::sendNow($developers, new DeploymentCompleted($deployment));</code></pre>
<h3>发送指定频道</h3>
<p>每个通知类都有一个 via 方法，用于确定将在哪些通道上传递通知。通知可以在 mail、database、broadcast、vonage 和 slack 频道上发送。</p>
<pre><code>/**
 * 获取通知发送频道
 * @param  mixed  $notifiable
 * @return array
 */
public function via($notifiable)
{
    return $notifiable-&gt;prefers_sms ? ['vonage'] : ['mail', 'database'];
}</code></pre>
<h2>数据库通知</h2>
<p>开始之前，您需要创建一个数据库表来保存您的通知</p>
<pre><code>php artisan notifications:table

php artisan migrate</code></pre>
<h3>格式化数据库通知</h3>
<p>如果通知支持存储在数据库表中，则应在通知类上定义 <code>toDatabase</code> 或 <code>toArray</code> 方法。这个方法将接收一个 <code>$notifiable</code> 实体并且应该返回一个普通的 <code>PHP</code> 数组。 返回的数组将被编码为 <code>JSON</code> 并存储在 <code>notifications</code> 表的 <code>data</code> 列中。让我们看一个示例 <code>toArray</code> 方法：</p>
<pre><code>public function toArray($notifiable)
{
    return [
        'invoice_id' =&gt; $this-&gt;invoice-&gt;id,
        'amount' =&gt; $this-&gt;invoice-&gt;amount,
    ];
}</code></pre>
<p><code>toDatabase</code> 对比 <code>toArray</code></p>
<p><code>broadcast</code> 通道也使用 <code>toArray</code> 方法来确定将哪些数据广播到 <code>JavaScript</code> 驱动的前端。如果您想为 <code>database</code> 和 <code>broadcast</code> 通道提供两种不同的数组表示，您应该定义一个 <code>toDatabase</code> 方法而不是 <code>toArray</code> 方法。</p>
<h3>访问通知</h3>
<p>默认情况下，通知将按 <code>created_at</code> 时间戳排序，最近的通知位于集合的开头：</p>
<pre><code>$user = App\Models\User::find(1);

foreach ($user-&gt;notifications as $notification) {
    echo $notification-&gt;type;
}</code></pre>
<p>如果您只想检索「未读」通知，可以使用 <code>unreadNotifications</code> </p>
<pre><code>$user = App\Models\User::find(1);

foreach ($user-&gt;unreadNotifications as $notification) {
    echo $notification-&gt;type;
}</code></pre>
<h3>将通知标记为已读</h3>
<pre><code>//直接在通知集合上使用 markAsRead 方法，而不是循环遍历每个通知：
$user-&gt;unreadNotifications-&gt;markAsRead();

//您还可以使用批量更新查询将所有通知标记为已读，而无需从数据库中检索它们：
$user = App\Models\User::find(1);
$user-&gt;unreadNotifications()-&gt;update(['read_at' =&gt; now()]);

//删除所有通知
$user-&gt;notifications()-&gt;delete();</code></pre>
<h2>自定义通知模板</h2>
<p>默认的模板通常已经够用了，下面我们简单修改一下</p>
<p>一些必要操作</p>
<pre><code>php artisan notifications:table
php artisan migrate

//创建通知
php artisan make:notification PasswordChangeMsg

php artisan vendor:publish --tag=laravel-mail
php artisan vendor:publish --tag=laravel-notifications</code></pre>
<p>主要代码</p>
<pre><code>    public function toMail(object $notifiable): MailMessage
    {
        return (new MailMessage)
            -&gt;level('info')
            -&gt;subject('您的密码已被重置')
            -&gt;greeting('您好！')
            -&gt;salutation("祝\n\n万事如意")
            -&gt;line('您的密码已被重置')
            -&gt;lines(["登录名：$this-&gt;username", "密码：$this-&gt;password"])
            -&gt;action('访问这里登录', url($this-&gt;url))
            -&gt;line('此邮件由系统自动发出，请勿回复！');
    }</code></pre>
<h3>通知模板修改</h3>
<p><code>resources/views/vendor/notifications/email.blade.php</code></p>
<pre><code>@lang(
    "如果你在点击 \":actionText\" 按钮遇到麻烦，复制并粘贴下面的URL\n".
    '进入你的网页浏览器:',
    [
        'actionText' =&gt; $actionText,
    ]
)</code></pre>
<h3>顶部修改</h3>
<p><code>/resources/views/vendor/mail/html/header.blade.php</code></p>
<p>默认的只有<code>APP_NAME</code>等于<code>Laravel</code>时才显示图片，我们改一下</p>
<pre><code>&lt;a href="{{ $url }}" style="display: inline-block;"&gt;
@if (trim($slot) === '写代码的崔哥')
&lt;img src="https://www.cuiwei.net/images/logo.jpg" class="logo" alt="写代码的崔哥 Logo"&gt;
@else
{{ $slot }}
@endif
&lt;/a&gt;</code></pre>
<h3>底部修改</h3>
<p><code>/resources/views/vendor/mail/html/message.blade.php</code></p>
<pre><code>&lt;x-mail::footer&gt;
© {{ date('Y') }} {{ config('app.name') }}. @lang('版权所有')
&lt;/x-mail::footer&gt;</code></pre>
<h3>最后，发送通知（邮件）</h3>
<pre><code>User::query()-&gt;find(1)-&gt;notify(new PasswordChangeMsg('cw', '1234', 'http://blog.cw.net/admin'));</code></pre>
<h2>参考</h2>
<p><a href="https://learnku.com/docs/laravel/10.x/notifications/14870">https://learnku.com/docs/laravel/10.x/notifications/14870</a></p></div>]]></description>
            <guid isPermaLink="false">Laravel 消息通知</guid>
        </item>
        <item>
            <title><![CDATA[Laravel 发送邮件]]></title>
            <description><![CDATA[<link rel="stylesheet" href="https://www.cuiwei.net/static/css/github-markdown.min.css?t=20260430" type="text/css" media="screen" /><div class="markdown-body"><h2>简单使用</h2>
<h3>配置</h3>
<p>以阿里企业邮为例，修改<code>.env</code>文件</p>
<pre><code>MAIL_MAILER=smtp
MAIL_HOST=smtp.mxhichina.com
MAIL_PORT=25
MAIL_USERNAME=notifications-noreply@a.com
MAIL_PASSWORD=123
MAIL_ENCRYPTION=tls
MAIL_FROM_ADDRESS=notifications-noreply@a.com
MAIL_FROM_NAME=notifications-noreply</code></pre>
<blockquote>
<p>关于<code>MAIL_ENCRYPTION</code>参数，我查看了源码，它的值只有<code>tls</code>和其他，tls即ssl加密，源码如下：</p>
</blockquote>
<pre><code>    public function setEncryption($encryption)
    {
        $encryption = strtolower($encryption ?? '');
        if ('tls' == $encryption) {
            $this-&gt;params['protocol'] = 'tcp';
            $this-&gt;params['tls'] = true;
        } else {
            $this-&gt;params['protocol'] = $encryption;
            $this-&gt;params['tls'] = false;
        }

        return $this;
    }</code></pre>
<h3>发送邮件</h3>
<p>发送文本邮件</p>
<pre><code>        Mail::raw('邮件内容。。', function (\Illuminate\Mail\Message $message){
//            $message-&gt;to('1@qq.com');//无主题
            $message-&gt;subject('测试一下。。')-&gt;to('1@qq.com');
        });</code></pre>
<p>发送富文本邮件</p>
<pre><code>        Mail::send('emails.test',['name'=&gt;'张三'],function (\Illuminate\Mail\Message $message){
            $message-&gt;subject('测试一下。。')-&gt;to('1@qq.com');
        })</code></pre>
<p>视图文件 <code>resources/views/emails/test.blade.php</code></p>
<h2>生成Mailables</h2>
<p>Laravel 更推荐使用<code>mailable</code>类来发送邮件</p>
<p>创建<code>RegisterSuccess</code></p>
<pre><code>root@php-fpm:/var/www/laravel-demo# php artisan make:mail RegisterSuccess</code></pre>
<p>如上，生成了一个mailable 类<code>app/Mail/RegisterSuccess.php</code>，请注意所有可邮寄类的配置都是在 <code>build</code> 方法中完成的。</p>
<h3>配置视图</h3>
<pre><code>    public function build()
    {
        return $this-&gt;view('emails.register_success');
    }</code></pre>
<p>视图文件<code>resources/views/emails/register_success.blade.php</code></p>
<p>纯文本邮件</p>
<p>你可以使用 text 方法来定义一个纯文本格式的邮件。和 view 方法一样， 该 text 方法接受一个模板名，模板名指定了在渲染邮件内容时你想使用的模板。你既可以定义纯文本格式亦可定义 HTML 格式：</p>
<pre><code>/**
 * 构建消息.
 *
 * @return $this
 */
public function build()
{
    return $this-&gt;view('emails.register_success')
                -&gt;text('emails.register_success_plain');
}</code></pre>
<h3>视图数据</h3>
<p>有两种方法传递数据到视图中。</p>
<p>第一种，通过 Public 属性</p>
<p>你在 mailable 类中定义的所有 public 的属性都将自动传递到视图中。</p>
<pre><code>    public User $user;
    public function __construct(User $user)
    {
        //
        $this-&gt;user=$user;
    }
    public function build()
    {
        return $this-&gt;view('emails.register_success')-&gt;with(['name' =&gt; 'abc']);
    }

//视图文件
&lt;body&gt;
test2..&lt;?=$name?&gt;
--
&lt;?=$user-&gt;name?&gt;

&lt;/body&gt;
&lt;/html&gt;</code></pre>
<p>第二种，通过 with 方法</p>
<pre><code>    public function build()
    {
        return $this-&gt;view('emails.register_success')-&gt;with(['name' =&gt; 'abc']);
    }</code></pre>
<h2>Markdown 格式邮件</h2>
<p>Markdown 格式邮件允许你可以使用 mailable 中的预构建模板和 邮件通知 组件。由于消息是用 Markdown 编写，Laravel 能够渲染出美观的、响应式的 HTML 模板消息，同时还能自动生成纯文本副本。</p>
<p>生成 Markdown 邮件</p>
<pre><code>root@php-fpm:/var/www/laravel-demo# php artisan make:mail RegisterSuccess2 --markdown=emails.register_success2

//

    public function build()
    {
        return $this-&gt;markdown('mail.register-success2', [
                    'url' =&gt; $this-&gt;orderUrl,
                ]);
    }</code></pre>
<p>Markdown mailable 类整合了 Markdown 语法和 Blade 组件，让你能够非常方便的使用 Laravel 预置的 UI 组件来构建邮件消息</p>
<p>常用组件：按钮组件，面板组件，表格组件，当然你也可以自定义组件</p>
<p>可以将所有 Markdown 邮件组件导出到自己的应用，用作自定义组件的模板。若要导出组件，使用 laravel-mail 资产标签的 vendor:publish Artisan 命令：</p>
<pre><code>php artisan vendor:publish --tag=laravel-mail</code></pre>
<h2>发送邮件</h2>
<p>若要发送邮件，使用 Mail 门面 的方法。该 to 方法接受 邮件地址、用户实例或用户集合。如果传递一个对象或者对象集合，mailer 在设置收件人时将自动使用它们的 email 和 name 属性，因此请确保对象的这些属性可用。一旦指定了收件人，就可以将 mailable 类实例传递给 send 方法：</p>
<pre><code>$user=User::query()-&gt;first();
//Mail::to('1@qq.com')-&gt;send(new RegisterSuccess($user));
Mail::to($user)-&gt;send(new RegisterSuccess($user));
</code></pre>
<h2>渲染邮件</h2>
<p>有时您可能希望捕获邮件的 HTML 内容而不发送它。为此，可以调用邮件类的 render 方法。此方法将以字符串形式返回邮件类的渲染内容:</p>
<pre><code>use App\Mail\InvoicePaid;
use App\Models\Invoice;

$invoice = Invoice::find(1);

return (new InvoicePaid($invoice))-&gt;render();</code></pre>
<p>在浏览器中预览邮件</p>
<pre><code>Route::get('/mailable', function () {
    $invoice = App\Models\Invoice::find(1);

    return new App\Mail\InvoicePaid($invoice);
});</code></pre>
<h2>参考</h2>
<p><a href="https://mp.weixin.qq.com/s/xWKrOMIFh_ZTTEuYJUwiAQ">https://mp.weixin.qq.com/s/xWKrOMIFh_ZTTEuYJUwiAQ</a></p>
<p><a href="https://learnku.com/docs/laravel/9.x/mail/12233">https://learnku.com/docs/laravel/9.x/mail/12233</a></p></div>]]></description>
            <guid isPermaLink="false">Laravel 发送邮件</guid>
        </item>
        <item>
            <title><![CDATA[Laravel 事件]]></title>
            <description><![CDATA[<link rel="stylesheet" href="https://www.cuiwei.net/static/css/github-markdown.min.css?t=20260430" type="text/css" media="screen" /><div class="markdown-body"><h2>常用命令</h2>
<pre><code>#显示系统注册的事件和监听器的列表
php artisan event:list</code></pre>
<h2>生成事件和监听器</h2>
<h3>一、 手动</h3>
<p>1、生成单个事件和监听器</p>
<pre><code>php artisan make:event PublishArticlesEvent

php artisan make:listener PublishArticlesListener --event=PublishArticlesEvent</code></pre>
<p>2、手动注册事件和监听器</p>
<pre><code>root@php-fpm:/var/www/laravel-demo# cat app/Providers/EventServiceProvider.php 
/**
 * 系统中的事件和监听器的对应关系。
 *
 * @var array
 */
protected $listen = [
    PublishArticlesEvent::class =&gt; [
        PublishArticlesListener::class,
    ],
];</code></pre>
<h3>二、自动</h3>
<p>生成 <code>EventServiceProvider</code> 中列出的、尚不存在的任何事件或侦听器</p>
<p>如下，<code>PublishArticlesEvent</code>和<code>PublishArticlesListener</code>是不存在的</p>
<pre><code>root@php-fpm:/var/www/laravel-demo# cat app/Providers/EventServiceProvider.php 

&lt;?php
namespace App\Providers;

use App\Events\PublishArticlesEvent;
use App\Listeners\PublishArticlesListener;

class EventServiceProvider extends ServiceProvider
{
    protected $listen = [
        PublishArticlesEvent::class =&gt; [
            PublishArticlesListener::class,
        ],
    ];
}</code></pre>
<p>执行</p>
<pre><code>php artisan event:generate</code></pre>
<p>执行成功会自动创建<code>app/Events/PublishArticlesEvent.php</code>和<code>app/Listeners/PublishArticlesListener.php</code></p>
<p>然后再修改一下</p>
<pre><code>vi app/Events/PublishArticlesEvent.php
class PublishArticlesEvent
{
    use Dispatchable, InteractsWithSockets, SerializesModels;

    public Article $article;
    public function __construct(Article $article)
    {
        $this-&gt;article=$article;
    }
}

vi app/Listeners/PublishArticlesListener.php
class PublishArticlesListener
{
    public function __construct()
    {
        //
    }
    public function handle(PublishArticlesEvent $event)
    {
        Log::info("article..");
        Log::info($event-&gt;article);
    }
</code></pre>
<h2>调度事件</h2>
<p>即触发事件，在web应用的控制器中，或控制台命令中都可以调用</p>
<pre><code>$article=Article::query()-&gt;first();
//调度事件
PublishArticlesEvent::dispatch($article);</code></pre>
<h2>参考</h2>
<p><a href="https://learnku.com/docs/laravel/9.x/events/12228">https://learnku.com/docs/laravel/9.x/events/12228</a></p></div>]]></description>
            <guid isPermaLink="false">Laravel 事件</guid>
        </item>
        <item>
            <title><![CDATA[Laravel 任务调度]]></title>
            <description><![CDATA[<link rel="stylesheet" href="https://www.cuiwei.net/static/css/github-markdown.min.css?t=20260430" type="text/css" media="screen" /><div class="markdown-body"><p>过去想给一个脚本创建计划任务，得登陆服务器执行<code>crontab -e</code>，或编辑<code>/etc/crontab</code>，每加一个脚本都得重复此步骤。</p>
<p>现在有了任务调度，你只需在服务器上配置一条</p>
<pre><code>* * * * * cd /你的项目路径 &amp;&amp; php artisan schedule:run &gt;&gt; /dev/null 2&gt;&amp;1</code></pre>
<p>后面你再加多少脚本都无需到服务器处理</p>
<h2>定义调度</h2>
<p>你可以在 <code>App\Console\Kernel</code> 类的 <code>schedule</code> 方法中定义所有的调度任务。</p>
<pre><code>class Kernel extends ConsoleKernel
{
    protected function schedule(Schedule $schedule)
    {
        //在test环境每分钟执行一次文章发布命令，不重叠的（如果一个任务执行缓慢，即使到下一个时间点了，也要等上一个时间点到任务执行完再执行，避免重叠）
         $schedule-&gt;command('article:publish')-&gt;everyMinute()-&gt;withoutOverlapping()-&gt;environments(['testing']);
    }
...</code></pre>
<pre><code>#闭包调度
$schedule-&gt;call(function () {
            DB::table('recent_users')-&gt;delete();
        })-&gt;daily();

#除了调用闭包这种方式来调度外，你还可以调用 `可调用对象`. 可调用对象是简单的 PHP 类，包含一个 `__invoke` 方法：
$schedule-&gt;call(new DeleteRecentUsers)-&gt;daily();</code></pre>
<p>查看已有的计划任务</p>
<pre><code>php artisan schedule:list</code></pre>
<p>Artisan 命令调度</p>
<pre><code>use App\Console\Commands\SendEmailsCommand;

$schedule-&gt;command('emails:send Taylor --force')-&gt;daily();

$schedule-&gt;command(SendEmailsCommand::class, ['Taylor', '--force'])-&gt;daily();</code></pre>
<p>队列任务调度</p>
<pre><code>use App\Jobs\Heartbeat;

$schedule-&gt;job(new Heartbeat)-&gt;everyFiveMinutes();

// 分发任务到「heartbeats」队列及「sqs」连接...
$schedule-&gt;job(new Heartbeat, 'heartbeats', 'sqs')-&gt;everyFiveMinutes();</code></pre>
<p>Shell 命令调度</p>
<pre><code>$schedule-&gt;exec('node /home/forge/script.js')-&gt;daily();</code></pre>
<p>调度频率选项</p>
<pre><code>-&gt;daily();  每天 00:00 执行一次任务</code></pre>
<h2>运行调度程序</h2>
<pre><code>* * * * * cd /你的项目路径 &amp;&amp; php artisan schedule:run &gt;&gt; /dev/null 2&gt;&amp;1</code></pre>
<p>本地运行调度程序</p>
<pre><code>php artisan schedule:work</code></pre>
<h2>任务输出</h2>
<pre><code>$schedule-&gt;command('emails:send')
         -&gt;daily()
         -&gt;appendOutputTo($filePath);</code></pre>
<h2>任务钩子</h2>
<pre><code>use Illuminate\Support\Stringable;

$schedule-&gt;command('emails:send')
         -&gt;daily()
         -&gt;onSuccess(function (Stringable $output) {
             // 任务执行成功。。。
         })
         -&gt;onFailure(function (Stringable $output) {
             // 任务执行失败。。。
         });</code></pre>
<p>Pinging 网址</p>
<pre><code>$schedule-&gt;command('emails:send')
         -&gt;daily()
         -&gt;pingOnSuccess($successUrl)
         -&gt;pingOnFailure($failureUrl);</code></pre>
<h2>参考</h2>
<p><a href="https://learnku.com/docs/laravel/9.x/scheduling/12238">https://learnku.com/docs/laravel/9.x/scheduling/12238</a></p></div>]]></description>
            <guid isPermaLink="false">Laravel 任务调度</guid>
        </item>
        <item>
            <title><![CDATA[Laravel 队列]]></title>
            <description><![CDATA[<link rel="stylesheet" href="https://www.cuiwei.net/static/css/github-markdown.min.css?t=20260430" type="text/css" media="screen" /><div class="markdown-body"><h2>创建任务</h2>
<p>生成任务类</p>
<pre><code>root@php-fpm:/var/www/laravel-demo# php artisan make:job PublishArticles
Job created successfully.
</code></pre>
<p>编辑一下</p>
<pre><code>class PublishArticles implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

    public Article $article;
    public function __construct(Article $article)
    {
        $this-&gt;article=$article;
    }
    public function handle()
    {
        Log::info($this-&gt;article);
        //todo 发布文章
    }
}</code></pre>
<p>调度任务，在web应用的控制器中，或控制台命令中都可以调用</p>
<pre><code>$article=Article::query()-&gt;first();
\App\Jobs\PublishArticles::dispatch($article);</code></pre>
<h2>运行队列工作者</h2>
<pre><code>#效率高，代码更新时必须重启队列
php artisan queue:work

#效率低，代码更新时不必重启队列
php artisan queue:listen

php artisan queue:work redis

php artisan queue:work redis --queue=emails

php artisan queue:work --once

php artisan queue:work --max-jobs=1000

php artisan queue:work --stop-when-empty

# 处理进程一小时，然后退出...
php artisan queue:work --max-time=3600

php artisan queue:work --sleep=3</code></pre>
<p>由于队列任务是长期存在的进程，因此如果不重新启动，他们不会注意到代码的更改。因此，使用队列任务部署应用程序的最简单方法是在部署过程中重新启动任务。您可以通过发出 <code>queue:restart</code> 命令优雅地重新启动所有进程：</p>
<pre><code>php artisan queue:restart</code></pre>
<h2>队列驱动</h2>
<h3>null</h3>
<p>丢弃排队任务</p>
<pre><code>QUEUE_CONNECTION=null</code></pre>
<h3>sync</h3>
<p>立即执行任务的同步驱动程序(用于本地开发期间)</p>
<pre><code>QUEUE_CONNECTION=sync</code></pre>
<h3>redis</h3>
<pre><code>composer require predis/predis</code></pre>
<pre><code>QUEUE_CONNECTION=redis</code></pre>
<h3>database</h3>
<pre><code>php artisan queue:table

php artisan migrate</code></pre>
<pre><code>QUEUE_CONNECTION=database</code></pre>
<h3>beanstalkd</h3>
<pre><code>composer require pda/pheanstalk</code></pre>
<pre><code>QUEUE_CONNECTION=beanstalkd</code></pre>
<h2>处理失败的工作</h2>
<p>创建 <code>failed_jobs</code> 表的迁移通常已经存在于新的 Laravel 应用程序中。但是，如果您的应用程序不包含此表的迁移，您可以使用 <code>queue:failed-table</code> 命令来创建迁移：</p>
<pre><code>php artisan queue:failed-table

php artisan migrate</code></pre>
<h3>失败重试</h3>
<pre><code>#重试3次
#如果您没有为 --tries 选项指定值，则作业将仅尝试一次或与任务类的 $tries 属性指定的次数相同：
php artisan queue:work redis --tries=3

#重试3次，每次都3秒后重试
php artisan queue:work redis --tries=3 --backoff=3

#重试任务前等待的秒数。
public $backoff = 3;

#计算重试任务之前要等待的秒数。
public function backoff()
{
    return 3;
}

#计算重试任务之前要等待的秒数。
#第一次重试的重试延迟为 1 秒，第二次重试为 5 秒，第三次重试为 10 秒：
public function backoff()
{
    return [1, 5, 10];
}</code></pre>
<h3>任务失败后发送告警</h3>
<pre><code>class ProcessPodcast implements ShouldQueue
{
    use InteractsWithQueue, Queueable, SerializesModels;

    protected $podcast;
    public function __construct(Podcast $podcast)
    {
        $this-&gt;podcast = $podcast;
    }
    public function handle(AudioProcessor $processor)
    {
        // 处理上传的播客...
    }
    public function failed(Throwable $exception)
    {
        // 向用户发送失败通知等...
    }
}</code></pre>
<p>手动重试</p>
<pre><code>#查看失败的任务（failed_jobs表）
php artisan queue:failed

#任务 ID 可用于重试失败的任务
php artisan queue:retry ce7bb17c-cdd8-41f0-a8ec-7b4fef4e5ece

#如有必要，可以向命令传递多个 ID:
php artisan queue:retry ce7bb17c-cdd8-41f0-a8ec-7b4fef4e5ece 91401d2c-0784-4f43-824c-34f94a33c24d

#还可以重试指定队列的所有失败任务:
php artisan queue:retry --queue=name

#重试所有失败任务，可以执行 queue:retry 命令，并将 all 作为 ID 传递:
php artisan queue:retry all

#如果要删除指定的失败任务，可以使用 queue:forget 命令:
php artisan queue:forget 91401d2c-0784-4f43-824c-34f94a33c24d

#删除 failed_jobs 表中所有失败任务，可以使用 queue:flush 命令:
php artisan queue:flush

#删除失败的任务2（和queue:flush有何不同？）
php artisan queue:prune-failed
php artisan queue:prune-failed --hours=48</code></pre>
<p>忽略缺失的模型</p>
<pre><code>/**
 * 如果任务的模型不存在，则删除该任务。
 *
 * @var bool
 */
public $deleteWhenMissingModels = true;</code></pre>
<p>丢弃失败的任务而不存储它们</p>
<pre><code>QUEUE_FAILED_DRIVER=null</code></pre>
<h3>从队列中清除任务</h3>
<pre><code>php artisan queue:clear

php artisan queue:clear redis --queue=emails</code></pre>
<blockquote>
<p>注意：从队列中清除任务仅适用于 SQS、Redis 和数据库队列驱动程序。 此外，SQS 消息删除过程最多需要 60 秒，因此在你清除队列后 60 秒内发送到 SQS 队列的任务也可能会被删除。</p>
</blockquote>
<h3>监控你的队列 [新特性]</h3>
<pre><code>php artisan queue:monitor redis:default,redis:deployments --max=100</code></pre>
<h2>总结</h2>
<pre><code>//向队列发布任务
        $article = \App\Models\Admin\Article::query()-&gt;first();
        \App\Jobs\PublishArticles::dispatch($article)-&gt;onQueue('article')-&gt;delay(now()-&gt;addMinute());

//处理任务
    /**
     * 任务可尝试次数.
     *
     * @var int
     */
    public $tries = 5;

    public function handle()
    {
        Log::info($this-&gt;article);
        Log::info('$this-&gt;attempts()');
        Log::info($this-&gt;attempts());
        if ($this-&gt;attempts() &gt; 3) {
            $this-&gt;release(10);
        }

        //直接失败，不会重试
//        $this-&gt;fail();

        //1. 成功：不需要返回值
        //2. 如果纯粹想重试，可以release,如果release次数超过最大次数，则任务会失败

        //手动删除
        //$this-&gt;delete();
        //导致失败，会重试
//        throw new \Exception('测试异常');
    }</code></pre>
<h2>参考</h2>
<p><a href="https://learnku.com/docs/laravel/9.x/queues/12236">https://learnku.com/docs/laravel/9.x/queues/12236</a></p></div>]]></description>
            <guid isPermaLink="false">Laravel 队列</guid>
        </item>
        <item>
            <title><![CDATA[Laravel 编写控制台命令]]></title>
            <description><![CDATA[<link rel="stylesheet" href="https://www.cuiwei.net/static/css/github-markdown.min.css?t=20260430" type="text/css" media="screen" /><div class="markdown-body"><p>Artisan 是 Laravel 附带的命令行接口。</p>
<pre><code>#查看所有可用的 Artisan 命令
php artisan list

#查看命令帮助
php artisan help migrate</code></pre>
<h2>Tinker 命令 (REPL)</h2>
<p>Laravel Tinker 是为 Laravel 提供的强大的 REPL（交互式解释器），由 PsySH 提供支持。</p>
<p>所有 Laravel 应用都默认包含了 Tinker。如果你之前已经将 Tinker 从应用中删除，可以使用 Composer 进行手动安装：</p>
<pre><code>composer require laravel/tinker</code></pre>
<p>通过运行 Artisan 命令 tinker 进入 Tinker 环境。</p>
<pre><code>php artisan tinker</code></pre>
<p>你可以通过 vendor:publish 命令发布 Tinker 配置文件：</p>
<pre><code>
root@php-fpm:/var/www/laravel-demo# php artisan vendor:publish --provider="Laravel\Tinker\TinkerServiceProvider"
Copied File [/vendor/laravel/tinker/config/tinker.php] To [/config/tinker.php]
Publishing complete.</code></pre>
<h2>编写命令</h2>
<p>即控制台应用。</p>
<p>除 Artisan 提供的命令外，你也可以编写自己的自定义命令。命令在多数情况下位于 app/Console/Commands 目录中。</p>
<h3>生成命令</h3>
<pre><code>root@php-fpm:/var/www/laravel-demo# php artisan make:command PublishArticles
Console command created successfully.

root@php-fpm:/var/www/laravel-demo# cat app/Console/Commands/PublishArticles.php 

&lt;?php

class PublishArticles extends Command
{
    protected $signature = 'article:publish {article}';

    protected $description = '发布文章';

    public function __construct()
    {
        parent::__construct();
    }
    public function handle()
    {
        echo $this-&gt;argument('article').PHP_EOL;
    }
}
</code></pre>
<h3>执行命令</h3>
<pre><code>root@php-fpm:/var/www/laravel-demo# php artisan article:publish cw
cw</code></pre>
<h2>定义输入期望</h2>
<p>在编写控制台命令时，通常是通过参数和选项来收集用户输入的。</p>
<h3>参数</h3>
<p>用户提供的所有参数和选项都用花括号括起来。</p>
<pre><code>#必须的参数
protected $signature = 'article:publish {article}';

#可选参数...
'article:publish {article?}'

#带有默认值的可选参数...
'article:publish {article=foo}'</code></pre>
<h3>选项</h3>
<p>选项类似于参数，是用户输入的另一种形式。在命令行中指定选项的时候，它们以两个短横线 (–) 作为前缀。这有两种类型的选项：接收值和不接受值。不接收值的选项就像是一个布尔「开关」。我们来看一下这种类型的选项的示例：</p>
<pre><code>#不接收值的选项就像是一个布尔「开关」
protected $signature = 'article:publish {article} {--queue}';

#带值的选项。如果用户需要为一个选项指定一个值，则需要在选项名称的末尾追加一个 = 号：
protected $signature = 'article:publish {article} {--queue=}';

#在选项名称后指定其默认值
'article:publish {article} {--queue=default}'

#选项简写
'article:publish {article} {--Q|queue}'</code></pre>
<h3>输入数组</h3>
<pre><code>#指定了一个数组参数的例子：
'article:publish {article*}'

root@php-fpm:/var/www/laravel-demo# php artisan article:publish cw cw2
["cw","cw2"]

#定义零个或多个参数
'article:publish {article?*}'

#选项数组
'article:publish {--id=*}'

root@php-fpm:/var/www/laravel-demo# php artisan article:publish --id=1 --id=2
["1","2"]
</code></pre>
<h3>输入说明</h3>
<pre><code>protected $signature = 'article:publish
                        {article : The ID of the article}
                        {--queue : Whether the job should be queued}';</code></pre>
<h2>命令 I/O</h2>
<h3>接收参数</h3>
<pre><code>$articleId = $this-&gt;argument('article');
$arguments = $this-&gt;arguments();

// 检索一个指定的选项...
$queueName = $this-&gt;option('queue');

// 检索所有选项做为数组...
$options = $this-&gt;options();</code></pre>
<h3>交互式输入</h3>
<pre><code>#ask 方法将询问用户指定的问题来接收用户输入，然后用户输入将会传到你的命令中：
$name = $this-&gt;ask('What is your name?');

root@php-fpm:/var/www/laravel-demo# php artisan article:publish

 What is your name?:
 &gt; cw

cw

#请求确认
$password = $this-&gt;secret('What is the password?');
if ($this-&gt;confirm('Do you wish to continue?')) {
    //
}

#自动补全
$name = $this-&gt;anticipate('What is your name?', ['Taylor', 'Dayle']);

root@php-fpm:/var/www/laravel-demo# php artisan article:publish

 What is your name?:
 &gt; Taylor
#根据输入词给出响应的提示
$name = $this-&gt;anticipate('What is your address?', function ($input) {
    //拿$input查数据库？
    // 返回自动完成配置...
});

#多选择问题
root@php-fpm:/var/www/laravel-demo# php artisan article:publish

 What is your name? [Taylor]:
  [0] Taylor
  [1] Dayle
 &gt; 1

Dayle

此外， choice 方法接受第四和第五可选参数 ，用于确定选择有效响应的最大尝试次数以及是否允许多次选择：
</code></pre>
<h3>文字输出</h3>
<pre><code>root@php-fpm:/var/www/laravel-demo# php artisan article:publish
The command was successful!

$this-&gt;error('Something went wrong!');
$this-&gt;line('Display this on the screen');
// 输出单行空白...
$this-&gt;newLine();

// 输出三行空白...
$this-&gt;newLine(3);

#表格
root@php-fpm:/var/www/laravel-demo# php artisan article:publish
+------+-----------+
| Name | Email     |
+------+-----------+
| cw   | 1@qq.com  |
| cw2  | 11@qq.com |
+------+-----------+

#进度条
$articles = $this-&gt;withProgressBar(Article::all(), function ($article) {
    echo json_encode($article, JSON_UNESCAPED_UNICODE).PHP_EOL;
});

root@php-fpm:/var/www/laravel-demo# php artisan article:publish
 0/2 [░░░░░░░░░░░░░░░░░░░░░░░░░░░░]   0%{"id":1,"category_id":1,"title":"aa","content":"aaaaa","views":0,"create_time":0}
{"id":2,"category_id":1,"title":"bb","content":"bbbbb","views":0,"create_time":0}
 2/2 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%
</code></pre>
<h2>注册命令</h2>
<p>您的所有控制台命令都在您的应用程序的 App\Console\Kernel 类中注册</p>
<pre><code>protected function commands()
{
    $this-&gt;load(__DIR__.'/Commands');
    $this-&gt;load(__DIR__.'/../Domain/Orders/Commands');

    // ...
}</code></pre>
<h2>以编程方式执行命令</h2>
<p>从路由或控制器执行 Artisan 命令。您可以使用 Artisan 外观上的 call 方法来完成此操作</p>
<pre><code>use Illuminate\Support\Facades\Artisan;

Route::post('/article/{article}/mail', function ($article) {
    $exitCode = Artisan::call('article:publish', [
        'article' =&gt; $article, '--queue' =&gt; 'default'
    ]);

    //
});</code></pre>
<p>或者，您可以将整个 Artisan 命令作为字符串传递给 call 方法：</p>
<pre><code>Artisan::call('article:publish 1 --queue=default');</code></pre>
<p>传递参数</p>
<pre><code>#传递数组值
use Illuminate\Support\Facades\Artisan;

Route::post('/mail', function () {
    $exitCode = Artisan::call('article:publish', [
        '--id' =&gt; [5, 13]
    ]);
});

#传递布尔值
$exitCode = Artisan::call('migrate:refresh', [
    '--force' =&gt; true,
]);</code></pre>
<p>队列 Artisan 命令</p>
<pre><code>use Illuminate\Support\Facades\Artisan;

Route::post('/article/{article}/mail', function ($article) {
    Artisan::queue('article:publish', [
        'article' =&gt; $article, '--queue' =&gt; 'default'
    ]);

    //
});</code></pre>
<p>从一个命令调用另一个命令</p>
<pre><code>/**
 * 执行控制台命令。
 *
 * @return mixed
 */
public function handle()
{
    $this-&gt;call('article:publish', [
        'article' =&gt; 1, '--queue' =&gt; 'default'
    ]);

    //
}</code></pre>
<h2>Stub 定制</h2>
<p>Artisan 控制台的 make 命令用于创建各种类。类似模板文件，如果想修改他们，需要先发布资源</p>
<pre><code>php artisan stub:publish</code></pre>
<p>已发布的 stub 将存放于你的应用根目录下的 stubs 目录中。</p>
<h2>参考</h2>
<p><a href="https://learnku.com/docs/laravel/9.x/artisan/12222">https://learnku.com/docs/laravel/9.x/artisan/12222</a></p></div>]]></description>
            <guid isPermaLink="false">Laravel 编写控制台命令</guid>
        </item>
        <item>
            <title><![CDATA[docker-compose快速部署JumpServer]]></title>
            <description><![CDATA[<link rel="stylesheet" href="https://www.cuiwei.net/static/css/github-markdown.min.css?t=20260430" type="text/css" media="screen" /><div class="markdown-body"><p>JumpServer 是运维必备的开源跳板机(堡垒机)系统</p>
<p>包含组件</p>
<table>
<thead>
<tr>
<th>组件项目</th>
<th>描述</th>
</tr>
</thead>
<tbody>
<tr>
<td><a href="https://github.com/jumpserver/lina">Lina</a></td>
<td>JumpServer Web UI 项目</td>
</tr>
<tr>
<td><a href="https://github.com/jumpserver/luna">Luna</a></td>
<td>JumpServer Web Terminal 项目</td>
</tr>
<tr>
<td><a href="https://github.com/jumpserver/koko">KoKo</a></td>
<td>Koko 是 Go 版本的 coco，重构了 coco 的 SSH/SFTP 服务和 Web Terminal 服务。</td>
</tr>
<tr>
<td><a href="https://github.com/jumpserver/lion-release">Lion</a></td>
<td>Lion 使用了 Apache 软件基金会的开源项目 Guacamole，JumpServer 使用 Golang 和 Vue 重构了 Guacamole 实现 RDP/VNC 协议跳板机功能。</td>
</tr>
<tr>
<td><a href="https://github.com/jumpserver/magnus-release">Magnus</a></td>
<td>JumpServer 数据库代理 Connector 项目</td>
</tr>
</tbody>
</table>
<p>知识点</p>
<p>定义docker-compose.yml中的变量</p>
<p>使用<code>.env</code>文件</p>
<h2>Github</h2>
<p><a href="https://github.com/chudaozhe/docker-jumpserver">docker-compose.yml文件</a></p>
<h2>参考</h2>
<p><a href="https://docs.jumpserver.org/zh/master/admin-guide/quick_start/#23">https://docs.jumpserver.org/zh/master/admin-guide/quick_start/#23</a></p></div>]]></description>
            <guid isPermaLink="false">docker-compose快速部署JumpServer</guid>
        </item>
        <item>
            <title><![CDATA[使用STS临时访问凭证访问OSS]]></title>
            <description><![CDATA[<link rel="stylesheet" href="https://www.cuiwei.net/static/css/github-markdown.min.css?t=20260430" type="text/css" media="screen" /><div class="markdown-body"><p>现在越来越多的项目使用oss存储文件，为了减轻服务器带宽的压力，通常会选择让前端直接把文件传到oss，但是为了不暴露密钥，通常会通过STS服务给前端颁发一个临时访问凭证。前端可使用临时访问凭证在规定时间内访问您的OSS资源。</p>
<p>下面看下如何快速配置sts服务：</p>
<p><img src="https://www.cuiwei.net/data/upload/2023-08-05/169116996058492.jpg" alt="WX202301292350402x.png" /></p>
<p><img src="https://www.cuiwei.net/data/upload/2023-08-05/169116998435057.jpg" alt="WX202301292358582x.png" /></p>
<p><img src="https://www.cuiwei.net/data/upload/2023-08-05/169116993288012.jpg" alt="FA381B5FF6014751BFE2752C06A347F4.png" /></p>
<p>最后记得复制 <code>RoleArn</code>，类似：<code>acs:ram::13530326330670:role/aliyunosstokengeneratorrole</code></p>
<h2>参考</h2>
<p><a href="https://help.aliyun.com/document_detail/100624.html">https://help.aliyun.com/document_detail/100624.html</a></p></div>]]></description>
            <guid isPermaLink="false">使用STS临时访问凭证访问OSS</guid>
        </item>
        <item>
            <title><![CDATA[聊天机器人模型 - ChatGPT]]></title>
            <description><![CDATA[<link rel="stylesheet" href="https://www.cuiwei.net/static/css/github-markdown.min.css?t=20260430" type="text/css" media="screen" /><div class="markdown-body"><h2>申请账号</h2>
<p>网上有教程，其中一个问题是不支持国内手机号，这就需要借助<a href="https://sms-activate.org">短信接码平台</a></p>
<p>还有一个问题，正常情况：注册成功<code>openai</code>会赠送18美金，可以用于api调用。但如果你用的这个手机号被其他人用过了，这18美金就没有了。我第一次就遇到这个问题，只能换个E-mail重新注册</p>
<h2>PHP类库</h2>
<h3>openai-php/client</h3>
<p>要求 <code>PHP 8.1+</code></p>
<pre><code>composer require openai-php/client</code></pre>
<p>简单使用</p>
<pre><code>$client = OpenAI::client('YOUR_API_KEY');

$result = $client-&gt;completions()-&gt;create([
    'model' =&gt; 'text-davinci-003',
    'prompt' =&gt; 'PHP is',
]);

echo $result['choices'][0]['text']; // an open-source, widely-used, server-side scripting language.</code></pre>
<h3>orhanerday/open-ai</h3>
<p>要求 <code>PHP 7.4+</code></p>
<pre><code>composer require orhanerday/open-ai</code></pre>
<p>简单使用</p>
<pre><code>&lt;?php

require __DIR__ . '/vendor/autoload.php'; // remove this line if you use a PHP Framework.

use Orhanerday\OpenAi\OpenAi;

$open_ai_key = getenv('OPENAI_API_KEY');
$open_ai = new OpenAi($open_ai_key);

$complete = $open_ai-&gt;completion([
    'model' =&gt; 'davinci',
    'prompt' =&gt; 'Hello',
    'temperature' =&gt; 0.9,
    'max_tokens' =&gt; 150,
    'frequency_penalty' =&gt; 0,
    'presence_penalty' =&gt; 0.6,
]);

var_dump($complete);</code></pre>
<h2>参考</h2>
<p><a href="https://beta.openai.com/examples/default-chat?lang=json">https://beta.openai.com/examples/default-chat?lang=json</a></p>
<h2>最后</h2>
<p>话不多说，点击页面右下角按钮即可与<code>ChatGPT</code>对话</p></div>]]></description>
            <guid isPermaLink="false">聊天机器人模型 - ChatGPT</guid>
        </item>
        <item>
            <title><![CDATA[PHPUnit 的使用]]></title>
            <description><![CDATA[<link rel="stylesheet" href="https://www.cuiwei.net/static/css/github-markdown.min.css?t=20260430" type="text/css" media="screen" /><div class="markdown-body"><h2>安装</h2>
<p>PHP Archive (PHAR)</p>
<pre><code>wget -O phpunit https://phar.phpunit.de/phpunit-9.phar
chmod +x phpunit
root@php-fpm:/var/www/html# ./phpunit --version
PHPUnit 9.5.27 by Sebastian Bergmann and contributors.</code></pre>
<p>或者，Composer</p>
<pre><code>composer require --dev phpunit/phpunit ^9
root@php-fpm:/var/www/laravel-demo# ./vendor/bin/phpunit --version
PHPUnit 9.5.24 #StandWithUkraine
</code></pre>
<h3>配置文件</h3>
<p>如果 <code>phpunit.xml</code> 或 <code>phpunit.xml.dist</code>（按此顺序）存在于当前工作目录并且未使用 <code>--configuration</code>，将自动从此文件中读取配置。</p>
<h2>执行测试</h2>
<p>执行全部测试</p>
<pre><code>phpunit</code></pre>
<p>执行某个测试</p>
<pre><code>    /**
     * @group home
     */
    public function testHome()
    {
        dump(123);
        $this-&gt;assertTrue(true);
    }

//直接用方法名
root@php-fpm:/var/www/laravel-demo# phpunit --filter testHome

//指定组名
root@php-fpm:/var/www/laravel-demo# phpunit --group home

--filter 'TestNamespace\\TestCaseClass::testMethod'
--filter 'TestNamespace\\TestCaseClass'
--filter TestNamespace
--filter TestCaseClase
--filter testMethod
</code></pre>
<h3>api 测试</h3>
<p>如果只是断言两个变量，就太没意思了，下面看下api测试</p>
<pre><code>$response = $this-&gt;get('/api/user/1/config');
        $response-&gt;dump();
        $response-&gt;assertStatus(200);

        $keys = [
            'aa'     =&gt; 'integer',
            'bb'      =&gt; 'string',
            'cc'          =&gt; 'string',
            'dd'        =&gt; 'string',
            'ee' =&gt; 'string',
        ];
        $data = optional(json_decode($response-&gt;getContent()))-&gt;data;
        $this-&gt;seeObjectContainsJson($keys, $data-&gt;j[0]);</code></pre>
<p>注意，上面的<code>get</code>用法是laravel特有的</p>
<h2>参考</h2>
<p><a href="https://phpunit.de/getting-started/phpunit-9.html">https://phpunit.de/getting-started/phpunit-9.html</a></p>
<p><a href="https://phpunit.readthedocs.io/zh_CN/latest/index.html">https://phpunit.readthedocs.io/zh_CN/latest/index.html</a></p></div>]]></description>
            <guid isPermaLink="false">PHPUnit 的使用</guid>
        </item>
        <item>
            <title><![CDATA[PHP PHPStan 的使用]]></title>
            <description><![CDATA[<link rel="stylesheet" href="https://www.cuiwei.net/static/css/github-markdown.min.css?t=20260430" type="text/css" media="screen" /><div class="markdown-body"><p>安装</p>
<pre><code>composer require --dev phpstan/phpstan</code></pre>
<p>修改composer.json</p>
<pre><code>
    "scripts": {
        ...
        "stan": [
            " php -d memory_limit=-1 vendor/bin/phpstan analyse app routes database config tests"
        ]
    },</code></pre>
<p>在项目根目录添加<code>phpstan.neon</code>配置文件</p>
<pre><code>root@php-fpm:/var/www/laravel-demo# vi phpstan.neon

内容可参考 https://phpstan.org/user-guide/getting-started</code></pre>
<h2>使用</h2>
<pre><code>root@php-fpm:/var/www/laravel-demo# composer stan</code></pre></div>]]></description>
            <guid isPermaLink="false">PHP PHPStan 的使用</guid>
        </item>
        <item>
            <title><![CDATA[Laravel 数据库交互 - 查询构造器]]></title>
            <description><![CDATA[<link rel="stylesheet" href="https://www.cuiwei.net/static/css/github-markdown.min.css?t=20260430" type="text/css" media="screen" /><div class="markdown-body"><h2>获取结果</h2>
<p>从表中检索所有行</p>
<pre><code>        $articles = DB::table('article')-&gt;get();
        foreach ($articles as $article) {
            echo $article-&gt;title.PHP_EOL;
        }</code></pre>
<p>从表中检索单行或单列</p>
<pre><code>        #通过id字段获取一行
        $article = DB::table('article')-&gt;find(3);
        #获取一行
        $article = DB::table('article')-&gt;where('title', '339911y')-&gt;first();
        echo $article-&gt;content.PHP_EOL;
        #从纪录中提取单个值
        echo DB::table('article')-&gt;where('title', '339911y')-&gt;value('content').PHP_EOL;</code></pre>
<p>获取某一列的值</p>
<pre><code>        $titles = DB::table('article')-&gt;pluck('title');
        foreach ($titles as $title) {
            echo $title.PHP_EOL;
        }
        #从表中检索单行或单列
        $regions = DB::connection('mysql2')-&gt;table('regions')-&gt;pluck('name', 'code');

        foreach ($regions as $code =&gt; $name) {
            echo $code.' =&gt; '.$name.PHP_EOL;
        }</code></pre>
<p>分块结果</p>
<pre><code>        #以一次 1000 条记录的块为单位检索整个 regions 表。
        DB::connection('mysql2')-&gt;table('regions')-&gt;orderBy('id')-&gt;chunk(1000, function ($regions) {
            foreach ($regions as $region) {
                echo $region-&gt;code.' =&gt; '.$region-&gt;name.PHP_EOL;
            }
            //您可以通过从闭包中返回 false 来停止处理其余的块
            //return false;
        });

        #如果您打算在分块时更新检索到的记录，最好使用 chunkById 方法
        DB::connection('mysql2')-&gt;table('regions')-&gt;chunkById(100, function ($regions) {
            foreach ($regions as $region) {
//                echo $region-&gt;code.' =&gt; '.$region-&gt;name.PHP_EOL;
                DB::table('users')-&gt;where('id', $region-&gt;id)-&gt;update(['views' =&gt; 1]);
            }
        });</code></pre>
<p>Lazily 流式传输结果</p>
<pre><code>        DB::table('article')-&gt;orderBy('id')-&gt;lazy()-&gt;each(function ($article) {
            echo $article-&gt;title.PHP_EOL;
        });

        #如果您打算在迭代它们时更新检索到的记录，最好使用 lazyById 或 lazyByIdDesc 方法。
        DB::table('users')-&gt;where('active', false)-&gt;lazyById()-&gt;each(function ($user) {
            DB::table('users')
                -&gt;where('id', $user-&gt;id)
                -&gt;update(['active' =&gt; true]);
        });</code></pre>
<p>聚合函数</p>
<pre><code>        $users = DB::table('users')-&gt;count();
        $price = DB::table('orders')-&gt;max('price');
        $price = DB::table('orders')-&gt;where('finalized', 1)-&gt;avg('price');</code></pre>
<p>判断记录是否存在</p>
<pre><code>if (DB::table('orders')-&gt;where('finalized', 1)-&gt;exists()) {
    // ...
}

if (DB::table('orders')-&gt;where('finalized', 1)-&gt;doesntExist()) {
    // ...
}</code></pre>
<h2>Select 语句</h2>
<pre><code>        #筛选字段
        $users = DB::table('users')-&gt;select('name', 'email as user_email')-&gt;get();
        #去重
        $users = DB::table('users')-&gt;distinct()-&gt;get();

        #addSelect
        $query = DB::table('users')-&gt;select('name');
        $users = $query-&gt;addSelect('age')-&gt;get();
</code></pre>
<h2>原生表达式</h2>
<pre><code>        $users = DB::table('users')
            -&gt;select(DB::raw('count(*) as user_count, status'))
            -&gt;where('status', '&lt;&gt;', 1)
            -&gt;groupBy('status')
            -&gt;get();
        #可以使用以下方法代替 DB::raw
        #selectRaw
        #whereRaw / orWhereRaw
        #havingRaw / orHavingRaw
        #orderByRaw
        #groupByRaw</code></pre>
<h2>Joins</h2>
<pre><code>        #Inner Join 语句
        $users = DB::table('users')
            -&gt;join('contacts', 'users.id', '=', 'contacts.user_id')
            -&gt;join('orders', 'users.id', '=', 'orders.user_id')
            -&gt;select('users.*', 'contacts.phone', 'orders.price')
            -&gt;get();

        #Left Join / Right Join 语句
        $users = DB::table('users')
            -&gt;leftJoin('posts', 'users.id', '=', 'posts.user_id')
            -&gt;get();

        $users = DB::table('users')
            -&gt;rightJoin('posts', 'users.id', '=', 'posts.user_id')
            -&gt;get();</code></pre>
<h2>Where 语句</h2>
<pre><code>        $users = DB::table('users')
            -&gt;where('votes', '=', 100)
            -&gt;where('age', '&gt;', 35)
            -&gt;get();

        $users = DB::table('users')
            -&gt;where('votes', '&gt;=', 100)
            -&gt;get();

        $users = DB::table('users')
            -&gt;where('votes', '&lt;&gt;', 100)
            -&gt;get();

        $users = DB::table('users')
            -&gt;where('name', 'like', 'T%')
            -&gt;get();

        $users = DB::table('users')-&gt;where([
            ['status', '=', '1'],
            ['subscribed', '&lt;&gt;', '1'],
        ])-&gt;get();

        #Or Where 语句
        $users = DB::table('users')
            -&gt;where('votes', '&gt;', 100)
            -&gt;orWhere('name', 'John')
            -&gt;get();

        #JSON Where 语句
        $users = DB::table('users')
            -&gt;where('preferences-&gt;dining-&gt;meal', 'salad')
            -&gt;get();

        #whereBetween / orWhereBetween
        $users = DB::table('users')
            -&gt;whereBetween('votes', [1, 100])
            -&gt;get();

        #whereNotBetween / orWhereNotBetween
        $users = DB::table('users')
            -&gt;whereNotBetween('votes', [1, 100])
            -&gt;get();

        #whereIn / whereNotIn / orWhereIn / orWhereNotIn
        $users = DB::table('users')
            -&gt;whereIn('id', [1, 2, 3])
            -&gt;get();
        $users = DB::table('users')
            -&gt;whereNotIn('id', [1, 2, 3])
            -&gt;get();

        #whereNull / whereNotNull / orWhereNull / orWhereNotNull
        $users = DB::table('users')
            -&gt;whereNull('updated_at')
            -&gt;get();
        $users = DB::table('users')
            -&gt;whereNotNull('updated_at')
            -&gt;get();

        #whereDate / whereMonth / whereDay / whereYear / whereTime
        $users = DB::table('users')
            -&gt;whereDate('created_at', '2016-12-31')
            -&gt;get();
        $users = DB::table('users')
            -&gt;whereMonth('created_at', '12')
            -&gt;get();
        $users = DB::table('users')
            -&gt;whereDay('created_at', '31')
            -&gt;get();
        $users = DB::table('users')
            -&gt;whereYear('created_at', '2016')
            -&gt;get();
        $users = DB::table('users')
            -&gt;whereTime('created_at', '=', '11:20:45')
            -&gt;get();

        #whereColumn / orWhereColumn
        $users = DB::table('users')
            -&gt;whereColumn('first_name', 'last_name')
            -&gt;get();
        $users = DB::table('users')
            -&gt;whereColumn('updated_at', '&gt;', 'created_at')
            -&gt;get();
        $users = DB::table('users')
            -&gt;whereColumn([
                ['first_name', '=', 'last_name'],
                ['updated_at', '&gt;', 'created_at'],
            ])-&gt;get();

        #逻辑分组
        $users = DB::table('users')
            -&gt;where('name', '=', 'John')
            -&gt;where(function ($query) {
                $query-&gt;where('votes', '&gt;', 100)
                    -&gt;orWhere('title', '=', 'Admin');
            })
            -&gt;get();

        #子查询 Where 语句
        $users = User::where(function ($query) {
            $query-&gt;select('type')
                -&gt;from('membership')
                -&gt;whereColumn('membership.user_id', 'users.id')
                -&gt;orderByDesc('membership.start_date')
                -&gt;limit(1);
        }, 'Pro')-&gt;get();</code></pre>
<h2>Ordering, Grouping, Limit &amp; Offset</h2>
<pre><code>        #排序
        $users = DB::table('users')
            -&gt;orderBy('name', 'desc')
            -&gt;get();
        $users = DB::table('users')
            -&gt;orderBy('name', 'desc')
            -&gt;orderBy('email', 'asc')
            -&gt;get();
        #latest 和 oldest 方法可以方便让你把结果根据日期排序。查询结果默认根据数据表的 created_at 字段进行排序 。或者，你可以传一个你想要排序的列名
        $user = DB::table('users')
            -&gt;latest()
            -&gt;first();

        #随机排序
        $randomUser = DB::table('users')
            -&gt;inRandomOrder()
            -&gt;first();

        #groupBy 和 having 方法
        $users = DB::table('users')
            -&gt;groupBy('account_id')
            -&gt;having('account_id', '&gt;', 100)
            -&gt;get();
        $report = DB::table('orders')
            -&gt;selectRaw('count(id) as number_of_orders, customer_id')
            -&gt;groupBy('customer_id')
            -&gt;havingBetween('number_of_orders', [5, 15])
            -&gt;get();
        $users = DB::table('users')
            -&gt;groupBy('first_name', 'status')
            -&gt;having('account_id', '&gt;', 100)
            -&gt;get();

        #Limit 和 Offset
        $users = DB::table('users')-&gt;skip(10)-&gt;take(5)-&gt;get();
        #或者，你可以使用 limit 和 offset 方法。这些方法在功能上等同于 take 和 skip 方法，如下:
        $users = DB::table('users')-&gt;offset(10)-&gt;limit(5)-&gt;get();
</code></pre>
<h2>条件语句 when</h2>
<pre><code>        #when 方法只有当第一个参数为 true 的时候才执行给定的闭包
        $role=1;
        $users = DB::table('users')
            -&gt;when($role, function ($query, $role) {
                return $query-&gt;where('role_id', $role);
            })
            -&gt;get();

        #只有当第一个参数的计算结果为 false 时，这个闭包才会执行
        $sortByVotes = 0;
        $users = DB::table('users')
            -&gt;when($sortByVotes, function ($query, $sortByVotes) {
                return $query-&gt;orderBy('votes');
            }, function ($query) {
                return $query-&gt;orderBy('name');
            })
            -&gt;get();</code></pre>
<h2>插入语句</h2>
<pre><code>
        DB::table('users')-&gt;insert([
            'email' =&gt; 'kayla@example.com',
            'votes' =&gt; 0
        ]);
        DB::table('users')-&gt;insert([
            ['email' =&gt; 'picard@example.com', 'votes' =&gt; 0],
            ['email' =&gt; 'janeway@example.com', 'votes' =&gt; 0],
        ]);
        #自增 IDs
        $id = DB::table('users')-&gt;insertGetId(
            ['email' =&gt; 'john@example.com', 'votes' =&gt; 0]
        );</code></pre>
<h2>Update 语句</h2>
<pre><code>        $affected = DB::table('users')
            -&gt;where('id', 1)
            -&gt;update(['votes' =&gt; 1]);
        DB::table('users')
            -&gt;updateOrInsert(
                ['email' =&gt; 'john@example.com', 'name' =&gt; 'John'],
                ['votes' =&gt; '2']
            );
        #更新 JSON 字段
        $affected = DB::table('users')
            -&gt;where('id', 1)
            -&gt;update(['options-&gt;enabled' =&gt; true]);

        #自增与自减
        DB::table('users')-&gt;increment('votes');
        DB::table('users')-&gt;increment('votes', 5);
        DB::table('users')-&gt;decrement('votes');
        DB::table('users')-&gt;decrement('votes', 5);</code></pre>
<h2>删除语句</h2>
<pre><code>        $deleted = DB::table('users')-&gt;where('votes', '&gt;', 100)-&gt;delete();
</code></pre>
<h2>悲观锁</h2>
<pre><code>        DB::table('users')
            -&gt;where('votes', '&gt;', 100)
            -&gt;sharedLock()
            -&gt;get();
        DB::table('users')
            -&gt;where('votes', '&gt;', 100)
            -&gt;lockForUpdate()
            -&gt;get();</code></pre>
<h2>调试</h2>
<pre><code>DB::table('users')-&gt;where('votes', '&gt;', 100)-&gt;dd();

DB::table('users')-&gt;where('votes', '&gt;', 100)-&gt;dump();</code></pre>
<h2>参考</h2>
<p><a href="https://learnku.com/docs/laravel/9.x/queries/12246">https://learnku.com/docs/laravel/9.x/queries/12246</a></p></div>]]></description>
            <guid isPermaLink="false">Laravel 数据库交互 - 查询构造器</guid>
        </item>
        <item>
            <title><![CDATA[Laravel 数据库交互 - 原生 SQL]]></title>
            <description><![CDATA[<link rel="stylesheet" href="https://www.cuiwei.net/static/css/github-markdown.min.css?t=20260430" type="text/css" media="screen" /><div class="markdown-body"><pre><code>        return DB::connection('mysql')-&gt;select('SELECT * FROM `article` WHERE `category_id` = ?', [1]);
        return DB::select('SELECT * FROM `article` WHERE `category_id` = ?', [1]);
        #使用命名绑定
        #除了使用 ? 表示参数绑定外，你还可以使用命名绑定的形式来执行一个查询：
        return DB::select('select * from `article` where `id` = :id', ['id' =&gt; 2]);

        return DB::insert('insert into `article` (`title`, `content`) values (?, ?)', ['aa', 'aaa']);
        return DB::update('update `article` set `views` = 100 where id = ?', [66]);
        return DB::delete('delete from `article` where id = ?', [66]);

        #自动提交
        return DB::transaction(function () {
            DB::select('select * from `article` where `id` = :id', ['id' =&gt; 65]);
            DB::update('update `article` set `views` = 100 where id = ?', [65]);
        });
        #手动提交
        DB::beginTransaction();
        DB::rollBack();
        DB::commit();

        #处理死锁
        DB::transaction(function () {
            DB::update('update users set votes = 1');
            DB::delete('delete from posts');
        }, 5);</code></pre>
<p><a href="https://learnku.com/docs/laravel/9.x/database/12245">https://learnku.com/docs/laravel/9.x/database/12245</a></p></div>]]></description>
            <guid isPermaLink="false">Laravel 数据库交互 - 原生 SQL</guid>
        </item>
        <item>
            <title><![CDATA[Laravel 迁移文件migrations 和 数据填充seeders]]></title>
            <description><![CDATA[<link rel="stylesheet" href="https://www.cuiwei.net/static/css/github-markdown.min.css?t=20260430" type="text/css" media="screen" /><div class="markdown-body"><h2>迁移文件 migration</h2>
<pre><code>#创建一个迁移
php artisan make:migration create_store_categories_table

#运行所有未完成的迁移
php artisan migrate

#如果要查看到目前为止已运行哪些迁移
php artisan migrate:status

#如果您希望查看迁移将执行的 SQL 语句而不实际运行它们
php artisan migrate --pretend

#某些迁移操作具有破坏性，这意味着它们可能会导致数据丢失。为了防止您对生产数据库运行这些命令，在执行命令之前，系统将提示您进行确认。若要强制命令在没有提示的情况下运行，请使用以下标志：
php artisan migrate --force

#要回滚最新的迁移操作，您可以使用rollback Artisan 命令。此命令回滚最后一批迁移，其中可能包括多个迁移文件：
php artisan migrate:rollback

#通过向rollback命令提供step选项，可以回滚有限数量的迁移。例如，以下命令将回滚最近5次迁移:
php artisan migrate:rollback --step=5

# migrate:reset命令将回滚应用程序的所有迁移:
php artisan migrate:reset

#refresh命令将回滚所有迁移，然后执行migrate命令。这个命令有效地重新创建您的整个数据库:
php artisan migrate:refresh

# 刷新数据库并运行所有数据库seeds...
php artisan migrate:refresh --seed

#通过向refresh命令提供step选项，可以回滚并重新迁移有限数量的迁移。例如，下面的命令将回滚并重新迁移最近的五次迁移:
php artisan migrate:refresh --step=5

#migrate:fresh命令将删除数据库中的所有表，然后执行migrate命令:
php artisan migrate:fresh
php artisan migrate:fresh --seed

#执行某个迁移/回滚某个迁移，官方是不支持的，只能采用特殊办法了
php artisan migrate --path=database/migrations/temp/
php artisan migrate:rollback --path=database/migrations/temp/

php artisan migrate --path=database/migrations/2023_01_16_203228_create_promoters_table.php
php artisan migrate:rollback --path=database/migrations/2023_01_16_203228_create_promoters_table.php --step=100
</code></pre>
<h2>数据填充 seed</h2>
<pre><code>#创建一个seed，表名为users
php artisan make:seeder UsersTableSeeder
#再创建一个seed，表名为users2
php artisan make:seeder Users2TableSeeder

#执行一个seed
php artisan db:seed --class=UsersTableSeeder

#执行所有seed，会执行DatabaseSeeder.php
php artisan db:seed

#您还可以使用migrate:fresh命令和——seed选项来为数据库播种，这将删除所有的表并重新运行所有的迁移。此命令对于完全重新构建数据库非常有用。——seeder选项可用于指定要运行的特定种子
php artisan migrate:fresh --seed
php artisan migrate:fresh --seed --seeder=UserSeeder

#某些播种操作可能会导致您更改或丢失数据。为了防止对生产数据库运行播种命令，在生产环境中执行播种命令之前，将提示您进行确认。要强制种子程序在没有提示的情况下运行，使用——force标志
php artisan db:seed --force</code></pre>
<p><code>cat database/seeders/DatabaseSeeder.php</code></p>
<pre><code>&lt;?php

namespace Database\Seeders;

use Illuminate\Database\Seeder;

class DatabaseSeeder extends Seeder
{
    /**
     * Seed the application's database.
     *
     * @return void
     */
    public function run()
    {
        // \App\Models\User::factory(10)-&gt;create();
//        $this-&gt;call(UsersTableSeeder::class);
//        $this-&gt;call(Users2TableSeeder::class);
        $this-&gt;call([
            UsersTableSeeder::class,
            Users2TableSeeder::class,
        ]);
    }
}
</code></pre>
<h2>参考</h2>
<p><a href="https://laravel.com/docs/9.x/migrations">https://laravel.com/docs/9.x/migrations</a></p>
<p><a href="https://laravel.com/docs/9.x/seeding">https://laravel.com/docs/9.x/seeding</a></p></div>]]></description>
            <guid isPermaLink="false">Laravel 迁移文件migrations 和 数据填充seeders</guid>
        </item>
        <item>
            <title><![CDATA[Laravel 中的 redis]]></title>
            <description><![CDATA[<link rel="stylesheet" href="https://www.cuiwei.net/static/css/github-markdown.min.css?t=20260430" type="text/css" media="screen" /><div class="markdown-body"><p>Redis 在 Laravel 中有两个角色，缓存和数据库</p>
<h2>数据库</h2>
<p>配置文件 <code>config/database.php</code></p>
<p>作为数据库使用，有两个<code>REDIS_CLIENT</code>可选，默认是<code>phpredis</code>（php的redis扩展），可选<code>predis</code>（纯php的三方库），两者区别，详见：<a href="https://www.cnblogs.com/afeige/p/14385588.html">https://www.cnblogs.com/afeige/p/14385588.html</a></p>
<p>Redis 门面的使用（Illuminate\Support\Facades\Redis）</p>
<p>下面以<code>phpredis</code>扩展为例</p>
<pre><code>Redis::set('a', 1); //没提示，和connection('default') 一样
Redis::connection()-&gt;client()-&gt;set('b',1);//和connection('default') 一样
Redis::connection('default')-&gt;client()-&gt;set('c',1);
Redis::connection('cache')-&gt;client()-&gt;set('d',1);
app('redis.connection')-&gt;set('e', 1); //没提示，和connection('default') 一样</code></pre>
<blockquote>
<p>Laravel 的 <code>config/app.php</code> 配置文件包含了 <code>aliases</code> 数组，该数组可用于定义通过框架注册的所有类别名。方便起见，<code>Laravel</code> 提供了一份包含了所有 <code>facade</code> 的别名入口；不过，<code>Redis</code> 别名不能在这里使用，因为这与 <code>phpredis</code> 扩展提供的 Redis 类名冲突。如果正在使用 Predis 客户端并确实想要用这个别名，你可以在 config/app.php 配置文件中取消对此别名的注释。</p>
</blockquote>
<h2>缓存</h2>
<p>配置文件 <code>config/cache.php</code></p>
<pre><code>    'default' =&gt; env('CACHE_DRIVER', 'file'),</code></pre>
<p>默认是file缓存，可以改为redis</p>
<pre><code>Cache::set('foo1', 1);
Cache::store('redis')-&gt;set('foo1', 1, 600);//10分钟后过期</code></pre></div>]]></description>
            <guid isPermaLink="false">Laravel 中的 redis</guid>
        </item>
        <item>
            <title><![CDATA[Laravel 记录SQL日志]]></title>
            <description><![CDATA[<link rel="stylesheet" href="https://www.cuiwei.net/static/css/github-markdown.min.css?t=20260430" type="text/css" media="screen" /><div class="markdown-body"><p>Laravel 默认只在sql语法错误时提示完整的sql日志，但实际情况接口慢，筛选条件和预期不符等，都需要看到sql语句，通过sql语句判断问题所在</p>
<p>下面介绍实现方式</p>
<h3>第一步</h3>
<p>修改 AppServiceProvider.php</p>
<pre><code>vi app/Providers/AppServiceProvider.php

&lt;?php

namespace App\Providers;

use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\ServiceProvider;

class AppServiceProvider extends ServiceProvider
{
    /**
     * Register any application services.
     *
     * @return void
     */
    public function register()
    {
    }

    /**
     * Bootstrap any application services.
     *
     * @return void
     */
    public function boot()
    {
        //https://learnku.com/docs/laravel/9.x/database/12245#97d96c
        $this-&gt;registerSqlDebug();
    }

    protected function registerSqlDebug()
    {
        if (config('logging.enable_log_sql', false)) {
            $print = false;
            if ($this-&gt;app-&gt;environment('local') &amp;&amp; env('IS_UNIT')) {
                $print = true;
            }

            DB::listen(function ($query) use ($print) {
                $sql = $query-&gt;sql;
                foreach ($query-&gt;bindings as $binding) {
                    $value = is_numeric($binding) ? $binding : "'{$binding}'";
                    $sql   = preg_replace('/\?/', (string) $value, $sql, 1);
                }
                $sql              = sprintf('【%s】 %s', $this-&gt;format_duration($query-&gt;time / 1000), $sql);
                Log::channel('sql')-&gt;debug($sql);
                if ($print) {
                    dump($sql);
                }
            });

        }
    }

    private function format_duration($seconds): string
    {
        if ($seconds &lt; 0.001) {
            return round($seconds * 1000000) . 'μs';
        } elseif ($seconds &lt; 1) {
            return round($seconds * 1000, 2) . 'ms';
        }

        return round($seconds, 2) . 's';
    }
}
</code></pre>
<h3>第二步</h3>
<p>修改 config/logging.php</p>
<p>增加sql日志开关</p>
<pre><code>    /**
     * 开启sql日志
     */
    'enable_log_sql' =&gt; env('LOG_SQL_ENABLED', true),</code></pre>
<p>日志默认输出到 <code>storage/logs/laravel.log</code>文件，为了区分开，增加以下配置</p>
<pre><code>        'sql'           =&gt; [
            'driver' =&gt; 'daily',
            'path'   =&gt; storage_path('logs/debug/sql.log'),
            'level'  =&gt; env('LOG_LEVEL', 'debug'),
            'days'   =&gt; 14,
        ],</code></pre></div>]]></description>
            <guid isPermaLink="false">Laravel 记录SQL日志</guid>
        </item>
        <item>
            <title><![CDATA[Laravel 以服务提供者的方式使用第三方扩展包]]></title>
            <description><![CDATA[<link rel="stylesheet" href="https://www.cuiwei.net/static/css/github-markdown.min.css?t=20260430" type="text/css" media="screen" /><div class="markdown-body"><p>下面以使用<code>腾讯地图webservices的php封装</code>为例</p>
<p>先安装</p>
<pre><code>composer require chudaozhe/tencent-map-api -vvv</code></pre>
<h2>先看下以普通方式使用</h2>
<pre><code>$key = '';//腾讯地图key
$secret_key = '';//SecretKey (SK)：在腾讯位置服务控制台 &gt; Key配置中，勾选WebServiceAPI的 SN校验时自动生成的随机字串，用于计算签名（sig）
$app = new \DeathSatan\TencentMapApi\Application($key, $secret_key);

//地址转经纬度
$data=$app-&gt;api()-&gt;addressResolution('北京市');
var_dump($data);</code></pre>
<h2>再看下以服务提供者的方式使用</h2>
<h3>第一步</h3>
<p>通过通过artisan命令创建TencentMapServiceProvider</p>
<p>或者手动创建也行</p>
<pre><code>root@php-fpm:/var/www/laravel-demo# php artisan make:provider TencentMapServiceProvider</code></pre>
<p>执行成功会生成一个文件：<code>app/Providers/TencentMapServiceProvider.php</code></p>
<h3>第二步</h3>
<p>添加一个配置文件</p>
<pre><code>vi config/tencentmap.php
&lt;?php
declare(strict_types=1);
return [
    //开发密钥（Key）
    'key'            =&gt; env('TENCENT_MAP_KEY', 'aaaa...'),
    //SecretKey (SK)
    'secret_key'     =&gt; env('TENCENT_MAP_SECRET_KEY', 'bbbb...'),

];
</code></pre>
<h3>第三步</h3>
<p>接着修改<code>app/Providers/TencentMapServiceProvider.php</code>文件中的<code>register</code>方法</p>
<pre><code>    public function register(): void
    {
        $this-&gt;app-&gt;singleton(Application::class, function ($app) {
            return new Application(config('tencentmap.key'), config('tencentmap.secret_key'));
        });

        $this-&gt;app-&gt;alias(Application::class, 'tencentmap');
    }</code></pre>
<h3>第四步</h3>
<p>注册服务</p>
<pre><code>vi config/app.php

    'providers'       =&gt; [
...
        App\Providers\TencentMapServiceProvider::class,

    ],</code></pre>
<h3>第五步</h3>
<p>使用，这里以控制器为例</p>
<pre><code>use DeathSatan\TencentMapApi\Application;
class Other extends Controller{
    protected Application $svc;

    public function __construct()
    {
        $this-&gt;svc = app('tencentmap');
    }

    public function test()
    {
        try {
            $data = $this-&gt;svc-&gt;api()-&gt;addressResolution('北京市')-&gt;toArray();
            var_dump($data);
        } catch (GuzzleException $e) {
            throw new Exception($e-&gt;getMessage(), 500);
        }
    }
}
</code></pre>
<h2>参考</h2>
<p><a href="https://segmentfault.com/a/1190000016824040">https://segmentfault.com/a/1190000016824040</a></p>
<p><a href="https://learnku.com/docs/laravel/9.x/providers/12207">https://learnku.com/docs/laravel/9.x/providers/12207</a></p></div>]]></description>
            <guid isPermaLink="false">Laravel 以服务提供者的方式使用第三方扩展包</guid>
        </item>
        <item>
            <title><![CDATA[Laravel 调试工具 - Telescope]]></title>
            <description><![CDATA[<link rel="stylesheet" href="https://www.cuiwei.net/static/css/github-markdown.min.css?t=20260430" type="text/css" media="screen" /><div class="markdown-body"><p>Laravel Telescope 是Laravel框架的一个优雅的调试助手。Telescope 提供对进入应用程序的请求、异常、日志条目、数据库查询、排队作业、邮件、通知、缓存操作、计划任务、变量转储等的洞察。Telescope 是您本地Laravel开发环境的绝佳伴侣。</p>
<p>安装</p>
<pre><code>#仅本地安装，不推荐用在生产环境
composer require laravel/telescope --dev
php artisan telescope:install
php artisan migrate</code></pre>
<p>访问仪表盘</p>
<p><a href="http://laravel.cw.net/telescope">http://laravel.cw.net/telescope</a></p>
<p><img src="https://www.cuiwei.net/data/upload/2022-12-31/167241999243124.jpg" alt="WX202212310105372x.jpg" /></p>
<h2>参考</h2>
<p><a href="https://laravel.com/docs/9.x/telescope">https://laravel.com/docs/9.x/telescope</a></p></div>]]></description>
            <guid isPermaLink="false">Laravel 调试工具 - Telescope</guid>
        </item>
        <item>
            <title><![CDATA[Laravel 队列管理工具 - Horizon]]></title>
            <description><![CDATA[<link rel="stylesheet" href="https://www.cuiwei.net/static/css/github-markdown.min.css?t=20260430" type="text/css" media="screen" /><div class="markdown-body"><p>安装</p>
<pre><code>#先安装pcntl扩展（我用的docker环境）
root@php-fpm:/var/www/laravel-demo# docker-php-ext-install pcntl

#再执行composer
root@php-fpm:/var/www/laravel-demo# composer require laravel/horizon</code></pre>
<p>安装 Horizon 后，使用 Artisan 命令发布其资源：<code>horizon:install</code></p>
<pre><code>#执行成功会创建config/horizon.php配置文件
php artisan horizon:install</code></pre>
<p>启动服务</p>
<pre><code>php artisan horizon</code></pre>
<p>访问服务</p>
<p><a href="http://laravel.cw.net/horizon/dashboard">http://laravel.cw.net/horizon/dashboard</a></p>
<p><img src="https://www.cuiwei.net/data/upload/2022-12-31/167241869697638.jpg" alt="WX202212310042582x.jpg" /></p>
<h2>参考</h2>
<p><a href="http://www.qg10086.com/book/laravel9/12268.html">http://www.qg10086.com/book/laravel9/12268.html</a></p>
<p><a href="https://laravel.com/docs/9.x/horizon#installation">https://laravel.com/docs/9.x/horizon#installation</a></p></div>]]></description>
            <guid isPermaLink="false">Laravel 队列管理工具 - Horizon</guid>
        </item>
        <item>
            <title><![CDATA[PHP CS Fixer 的使用]]></title>
            <description><![CDATA[<link rel="stylesheet" href="https://www.cuiwei.net/static/css/github-markdown.min.css?t=20260430" type="text/css" media="screen" /><div class="markdown-body"><p>安装</p>
<pre><code>composer require --dev friendsofphp/php-cs-fixer
</code></pre>
<p>修改composer.json</p>
<pre><code>    "scripts": {
...
        "cs-diff": [
            "vendor/bin/php-cs-fixer fix --verbose --diff --dry-run"
        ],
        "cs-fix": [
            "vendor/bin/php-cs-fixer fix --verbose --diff"
        ]
    },</code></pre>
<p>在项目根目录添加<code>.php-cs-fixer.php</code>配置文件</p>
<pre><code>root@php-fpm:/var/www/laravel-demo# vi .php-cs-fixer.php

...

内容可参考 https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/blob/master/.php-cs-fixer.dist.php</code></pre>
<h2>使用</h2>
<pre><code>root@php-fpm:/var/www/laravel-demo# composer cs-fix</code></pre>
<p>执行成功会生成<code>.php-cs-fixer.cache</code>文件，把它加到<code>.gitignore</code>，因为不需要提交到git</p>
<h2>参考</h2>
<p><a href="https://cs.symfony.com/">https://cs.symfony.com/</a></p></div>]]></description>
            <guid isPermaLink="false">PHP CS Fixer 的使用</guid>
        </item>
        <item>
            <title><![CDATA[elasticsearch地理位置查询]]></title>
            <description><![CDATA[<link rel="stylesheet" href="https://www.cuiwei.net/static/css/github-markdown.min.css?t=20260430" type="text/css" media="screen" /><div class="markdown-body"><p>Elasticsearch支持两种类型的地理数据：支持lat/lon对的geo_point字段和支持点、线、圆圈、多边形、多多边形等的geo_shape字段。</p>
<p>下面只介绍geo_point</p>
<p>创建名称为geo的索引</p>
<pre><code>curl --location --request PUT 'localhost:9200/geo' \
--header 'Content-Type: application/json' \
--data-raw '{
  "settings": {
    "number_of_replicas": 3,
    "number_of_shards": 5
  },
   "mappings": {
    "properties": {
      "name":{
        "type": "text"
      },
      "location":{
        "type": "geo_point"
      }
    }
  }
}'</code></pre>
<p>添加测试数据</p>
<pre><code>curl --location --request PUT 'localhost:9200/geo/_doc/2' \
--header 'Content-Type: application/json' \
--data-raw '{
  "name":"海淀公园",
  "location":
  {
    "lon":116.302509,
    "lat":39.991152
  }
}'

curl --location --request PUT 'localhost:9200/geo/_doc/1' \
--header 'Content-Type: application/json' \
--data-raw '{
  "name":"天安门",
  "location":
  {
    "lon":116.403981,
    "lat":39.914492
  }
}'

curl --location --request PUT 'localhost:9200/geo/_doc/3' \
--header 'Content-Type: application/json' \
--data-raw '{
  "name":"北京动物园",
  "location":
  {
    "lon":116.343184,
    "lat":39.947468
  }
}'
</code></pre>
<p>geo_point支持三种类型的查询</p>
<ul>
<li>geo_distance</li>
<li>geo_bounding_box</li>
<li>geo_polygon</li>
</ul>
<h3>geo_distance:直线距离检索，如给定点A，要求返回地图上距离点A三千米的商家</h3>
<p>查找索引内距离北京站(116.433733,39.908404)3000米内的点</p>
<p>涉及的参数如下</p>
<ul>
<li>location:确定一个点；</li>
<li>distance:确定一个半径，单位米</li>
<li>distance_type:确定一个图形的类型，一般是圆形,arc</li>
</ul>
<pre><code>curl --location --request GET 'localhost:9200/geo/_search' \
--header 'Content-Type: application/json' \
--data-raw '{
  "query": {
    "geo_distance": {
      "location": {
        "lon":116.433733
        ,"lat":39.908404
      },
      "distance":3000,
      "distance_type":"arc"
    }
  }
}'</code></pre>
<h3>geo_bounding_box:以两个点确定一个矩形，获取在矩形内的全部数据</h3>
<p>查找索引内位于中央民族大学(116.326943,39.95499)以及京站(116.433733,39.908404)矩形的点</p>
<p>涉及的参数如下</p>
<ul>
<li>top_left: 左上角的矩形起始点经纬度；</li>
<li>bottom_right: 右下角的矩形结束点经纬度</li>
</ul>
<pre><code>curl --location --request GET 'localhost:9200/geo/_search' \
--header 'Content-Type: application/json' \
--data-raw '{
  "query": {
    "geo_bounding_box": {
      "location": {
        "top_left": {
          "lon": 116.326943,
          "lat": 39.95499
        },
        "bottom_right": {
          "lon": 116.433446,
          "lat": 39.908737
        }
      }
    }
  }
}'</code></pre>
<h3>geo_polygon：以多个点，确定多边形，获取多边形内的全部数据</h3>
<p>查找索引内位于西苑桥(116.300209,40.003423)，巴沟山水园(116.29561,39.976004)以及北京科技大学(116.364528,39.996348)三角形内的点</p>
<p>涉及的参数如下</p>
<ul>
<li>points：是个数组，存储多变形定点的经纬度，每个点用大括号包起来</li>
</ul>
<pre><code>curl --location --request GET 'localhost:9200/geo/_search' \
--header 'Content-Type: application/json' \
--data-raw '{
  "query": {
    "geo_polygon": {
      "location": {
        "points": [
          {
            "lon": 116.29561,
            "lat": 39.976004
          },
          {
            "lon": 116.364528,
            "lat": 39.996348
          },
          {
            "lon": 116.300209,
            "lat": 40.003423
          }
        ]
      }
    }
  }
}'</code></pre>
<h2>地理位置排序</h2>
<p>检索结果可以按与指定点的距离排序，当可以按距离排序时， 按距离打分 通常是一个更好的解决方案。但是要计算当前距离,所以还是使用这个排序。搜索示例:</p>
<pre><code>{
  "query": {
    "geo_polygon": {
      "location": {
        "points": [
          {
            "lon": 116.29561,
            "lat": 39.976004
          },
          {
            "lon": 116.364528,
            "lat": 39.996348
          },
          {
            "lon": 116.300209,
            "lat": 40.003423
          }
        ]
      }
    }
  },
  "sort": [
    {
      "_geo_distance": {
        "location": { 
          "lat":  40.715,
          "lon": -73.998
        },
        "order": "asc",
        "unit": "km", 
        "distance_type": "plane" 
      }
    }
  ]
}</code></pre>
<p>解读以下: (注意看sort对象)</p>
<ul>
<li>计算每个文档中 location 字段与指定的 lat/lon 点间的距离。</li>
<li>将距离以 km 为单位写入到每个返回结果的 sort 键中。</li>
<li>使用快速但精度略差的 plane 计算方式。</li>
</ul>
<p>结果</p>
<pre><code>{
    "took": 33,
    "timed_out": false,
    "_shards": {
        "total": 5,
        "successful": 5,
        "skipped": 0,
        "failed": 0
    },
    "hits": {
        "total": {
            "value": 1,
            "relation": "eq"
        },
        "max_score": null,
        "hits": [
            {
                "_index": "geo",
                "_type": "_doc",
                "_id": "2",
                "_score": null,
                "_source": {
                    "name": "海淀公园",
                    "location": {
                        "lon": 116.302509,
                        "lat": 39.991152
                    }
                },
                "sort": [
                    16125.943696542714
                ]
            }
        ]
    }
}</code></pre>
<h2>参考</h2>
<p><a href="https://www.elastic.co/guide/en/elasticsearch/reference/7.17/geo-queries.html">https://www.elastic.co/guide/en/elasticsearch/reference/7.17/geo-queries.html</a></p>
<p><a href="https://blog.csdn.net/wuxintdrh/article/details/115367301">https://blog.csdn.net/wuxintdrh/article/details/115367301</a></p>
<p><a href="https://www.cnblogs.com/johnvwan/p/15644841.html">https://www.cnblogs.com/johnvwan/p/15644841.html</a></p></div>]]></description>
            <guid isPermaLink="false">elasticsearch地理位置查询</guid>
        </item>
        <item>
            <title><![CDATA[基于redis的geo类型实现“附近的xx”功能]]></title>
            <description><![CDATA[<link rel="stylesheet" href="https://www.cuiwei.net/static/css/github-markdown.min.css?t=20260430" type="text/css" media="screen" /><div class="markdown-body"><p>这里假设要实现的是“附近的地铁站”功能，key为list，member为地铁站id</p>
<p>首先，你需要在redis里维护一个geo的列表（本质上是sorted set），当每个地铁站的经纬度有更新时，就使用</p>
<pre><code>#地铁站id=1
#顺便提一下，相同key，member，不同经纬度，GEOADD会自动更新的
127.0.0.1:6379&gt; GEOADD list CH 13.361389 38.115556 "1"</code></pre>
<h2>列表接口的实现</h2>
<p>客户端会传一个经纬度，页码，每页条数，关键词（搜索后排序先不考虑）</p>
<p>生成模拟数据</p>
<pre><code>$redis = new Redis();
$redis-&gt;connect("docker-redis", 6379);

$addrs=[
    ['name'=&gt;'1北京四惠地铁站', 'id'=&gt;1, 'long'=&gt;116.495676, 'lat'=&gt;39.908789],
    ['name'=&gt;'2北京大望路地铁站', 'id'=&gt;2, 'long'=&gt;116.475835, 'lat'=&gt;39.908278],
    ['name'=&gt;'3北京国贸地铁站', 'id'=&gt;3, 'long'=&gt;116.459729, 'lat'=&gt;39.908432],
    ['name'=&gt;'4北京永安里地铁站', 'id'=&gt;4, 'long'=&gt;116.450334, 'lat'=&gt;39.908478],
    ['name'=&gt;'5北京建国门地铁站', 'id'=&gt;5, 'long'=&gt;116.434768, 'lat'=&gt;39.908587],
    ['name'=&gt;'6北京东单地铁站', 'id'=&gt;6, 'long'=&gt;116.418504, 'lat'=&gt;39.908366],
    ['name'=&gt;'7北京王府井地铁站', 'id'=&gt;7, 'long'=&gt;116.411565, 'lat'=&gt;39.908106],
    ['name'=&gt;'8北京西单地铁站', 'id'=&gt;8, 'long'=&gt;116.376302, 'lat'=&gt;39.907194],
    ['name'=&gt;'9北京复兴门地铁站', 'id'=&gt;9, 'long'=&gt;116.357757, 'lat'=&gt;39.90715],
    ['name'=&gt;'10北京南礼士路地铁站', 'id'=&gt;10, 'long'=&gt;116.352589, 'lat'=&gt;39.907247],
    ['name'=&gt;'11北京木樨地地铁站', 'id'=&gt;11, 'long'=&gt;116.337475, 'lat'=&gt;39.907471],
    ['name'=&gt;'12北京军事博物馆地铁站', 'id'=&gt;12, 'long'=&gt;116.321411, 'lat'=&gt;39.90744],
];
$args=[];
foreach ($addrs as $v){
    $args[]=$v['long'];
    $args[]=$v['lat'];
    $args[]=$v['id'];
}
$ok=$redis-&gt;geoAdd('list',
...$args
);
</code></pre>
<p>查询</p>
<pre><code>$r = $redis-&gt;geoRadiusByMember('list', 1, 1800, 'km', [
    'count' =&gt; 100,
//    'store'=&gt;'list2',
    'storedist'=&gt;'list3',
    'asc',
//    'WITHCOORD',
//    'WITHDIST',
//    'WITHHASH'
]);
$page=$_GET['page'];
$max=$_GET['max'];
$start = ($page-1) * $max;
$r=$redis-&gt;zRange('list3', $start, ($start + $max) - 1, true);//分数升序，取全部
var_dump(array_keys($r));

//todo:cw where id in(1,2)

$j=[
    ['name'=&gt;'1北京四惠地铁站', 'id'=&gt;1, 'long'=&gt;116.495676, 'lat'=&gt;39.908789],
    ['name'=&gt;'2北京大望路地铁站', 'id'=&gt;2, 'long'=&gt;116.475835, 'lat'=&gt;39.908278],
    ['name'=&gt;'3北京国贸地铁站', 'id'=&gt;3, 'long'=&gt;116.459729, 'lat'=&gt;39.908432],
];
$arr=[];
foreach ($r as $id=&gt;$v){
    foreach ($j as $k=&gt;$item){
        if ($item['id']==$id) $j[$k]['dist']=round($v, 2);
    }
}
$dist=array_column($j, 'dist');
array_multisort($dist, SORT_DESC, $j);
var_dump($j);</code></pre></div>]]></description>
            <guid isPermaLink="false">基于redis的geo类型实现“附近的xx”功能</guid>
        </item>
        <item>
            <title><![CDATA[docker-compose快速部署jira]]></title>
            <description><![CDATA[<link rel="stylesheet" href="https://www.cuiwei.net/static/css/github-markdown.min.css?t=20260430" type="text/css" media="screen" /><div class="markdown-body"><p>上篇总结了<a href="https://www.cuiwei.net/p/1258785919">Confluence的安装和激活</a>，下面再看下Jira</p>
<h2>各版本的区别</h2>
<blockquote>
<p>在Jira7.X，Atlassian将根据特定的JIRA应用场景，将原来的JIRA分为了三个不同的版本 ：</p>
<p>JIRA Core可以认为是原生态的JIRA功能，提供最基本的项目、版本、组件、任务的相关管理；向所有用户提供完整功能。</p>
<p>JIRA Software是JIRA Core+Agile插件的融合体，允许用户在开展最基本的项目、版本、组件、任务的相关管理的同时，采用目前比较流行的敏捷开发模式（支持Scrum和Kanba）进行工作的管理。</p>
<p>JIRA Service Desk是JIRA Core+Service Desk插件的整合体，允许用户在开展最基本的项目、版本、组件、任务的相关管理的同时，它提供了另外一类以客户服务为特定场景工作模式。比如用户请求及反馈的处理，SLA的追求；同时针对客户化的工单请求，提供更为友好的界面；并在请求代理的的处理界面上进行了更为直观的展现形式。</p>
<p>Jira Service Desk 现在是 Jira Service Management 的一部分。Jira Service Management 包含 Jira Service Desk 的所有功能以及更丰富的 ITSM 功能</p>
</blockquote>
<p><a href="https://doc.devpod.cn/jsm/jira-service-management-17105048.html">https://doc.devpod.cn/jsm/jira-service-management-17105048.html</a></p>
<p><a href="https://community.atlassian.com/t5/Jira-Core-Server-questions/JIRA-core-vs-JIRA-Software/qaq-p/1967432">https://community.atlassian.com/t5/Jira-Core-Server-questions/JIRA-core-vs-JIRA-Software/qaq-p/1967432</a></p>
<h2>版本选择</h2>
<ul>
<li>Jira：JIRA Core 9.4.0，长期支持版本</li>
<li>数据库：mysql:8.0.26</li>
</ul>
<h2>配置mysql</h2>
<p><a href="https://dev.mysql.com/downloads/connector/j/">https://dev.mysql.com/downloads/connector/j/</a></p>
<p><img src="https://www.cuiwei.net/data/upload/2022-12-03/167008261126917.jpg" alt="WX202212032348332x.jpg" /></p>
<p>解压后得到<code>mysql-connector-j-8.0.31.jar</code></p>
<p>修改my.cnf，调整事务隔离级别</p>
<pre><code>[mysqld]
...
transaction-isolation=READ-COMMITTED
...</code></pre>
<p>创建数据库<code>jira</code></p>
<p><img src="https://www.cuiwei.net/data/upload/2022-12-03/167008275781597.jpg" alt="WX202212032210392x.png" /></p>
<h2>激活</h2>
<p><code>atlassian-agent.jar</code>如何获取请看上一篇</p>
<h3>构建镜像</h3>
<p>为了方便，我们借鉴一个Dockerfile</p>
<pre><code>FROM atlassian/jira-core:9.4.0-jdk11

USER root

# 将代理破解包加入容器
COPY "atlassian-agent.jar" /opt/atlassian/jira/

# 设置启动加载代理包
RUN echo '\nexport JAVA_OPTS="-javaagent:/opt/atlassian/jira/atlassian-agent.jar ${JAVA_OPTS}"' &gt;&gt; /opt/atlassian/jira/bin/setenv.sh</code></pre>
<p>构建</p>
<pre><code>cuiwei@weideMacBook-Pro jira % docker build -t jira-core:9.4.0-jdk11-1.0 .</code></pre>
<h2>docker-compose</h2>
<pre><code>version: '3'

networks:
  web-network:

services:

  jira-core:
    image: jira-core:9.4.0-jdk11-1.0
    container_name: jira-core
    hostname: jira-core
    ports:
      - "8081:8080"
    restart: always
    tty: true
    volumes:
      - ./jira/data:/var/atlassian/application-data/jira
      - ./jira/mysql-connector-j-8.0.31.jar:/opt/atlassian/jira/atlassian-jira/WEB-INF/lib/mysql-connector-j-8.0.31.jar
    networks:
      - web-network

  docker-mysql:
    image: mysql:8.0.26
    hostname: mysql
    restart: always
    tty: true
    volumes:
      - ./mysql/my.cnf:/etc/my.cnf
      - ./mysql/data:/var/lib/mysql
    environment:
      - "MYSQL_ALLOW_EMPTY_PASSWORD=yes"
    ports:
      - 3306:3306
    networks:
      - web-network</code></pre>
<p>启动服务</p>
<pre><code>docker-compose up -d</code></pre>
<h2>网页配置</h2>
<p>服务启动后就可以访问了，<a href="http://localhost:8081">http://localhost:8081</a></p>
<p><img src="https://www.cuiwei.net/data/upload/2022-12-04/167012912896242.jpg" alt="WX202212041244352x.png" /></p>
<p>有了上面的Server ID，就可以生成license key了</p>
<pre><code>root@jira-core:/var/atlassian/application-data/jira# cd /opt/atlassian/jira
root@jira-core:/opt/atlassian/jira# java -jar atlassian-agent.jar -d -m test@test.com -n BAT -p jc -o http://localhost:8081 -s BO73-0000-0000-KN6F

#注意，填写的license key不能有换行符</code></pre>
<p>配置mysql连接信息</p>
<p><img src="https://www.cuiwei.net/data/upload/2022-12-03/167008317393914.jpg" alt="WX202212032229592x.jpg" /></p>
<p>后面按照提示走就可以了</p>
<h2>参考</h2>
<p><a href="https://www.cnblogs.com/zxl1024320609/p/16549311.html">https://www.cnblogs.com/zxl1024320609/p/16549311.html</a></p>
<p><a href="https://www.atlassian.com/zh/software/jira/core/download">https://www.atlassian.com/zh/software/jira/core/download</a></p>
<p><a href="https://hub.docker.com/r/atlassian/jira-core">https://hub.docker.com/r/atlassian/jira-core</a></p>
<p><a href="https://confluence.atlassian.com/adminjiraserver/connecting-jira-applications-to-a-database-938846850.html">https://confluence.atlassian.com/adminjiraserver/connecting-jira-applications-to-a-database-938846850.html</a></p>
<p><a href="https://confluence.atlassian.com/adminjiraserver/running-the-setup-wizard-938846872.html">https://confluence.atlassian.com/adminjiraserver/running-the-setup-wizard-938846872.html</a></p></div>]]></description>
            <guid isPermaLink="false">docker-compose快速部署jira</guid>
        </item>
        <item>
            <title><![CDATA[docker-compose快速部署confluence]]></title>
            <description><![CDATA[<link rel="stylesheet" href="https://www.cuiwei.net/static/css/github-markdown.min.css?t=20260430" type="text/css" media="screen" /><div class="markdown-body"><p>Confluence 是什么这里就不多说了</p>
<h2>版本选择</h2>
<ul>
<li>confluence：confluence-server:7.19.4-jdk11，长期支持版本</li>
<li>数据库：mariadb:10.5.12</li>
</ul>
<h2>配置mysql</h2>
<blockquote>
<p>由于许可限制，MySQL和Oracle的驱动程序没有与Confluence捆绑在一起。
Confluence目前正在使用5.1.48驱动程序进行测试。
您无法将最新的驱动程序（8.x）与Confluence和MySQL 5.7一起使用。</p>
</blockquote>
<p>需要手动下载</p>
<p><a href="https://dev.mysql.com/downloads/connector/j/5.1.html">https://dev.mysql.com/downloads/connector/j/5.1.html</a></p>
<p><img src="https://www.cuiwei.net/data/upload/2022-12-03/167005547725524.jpg" alt="WX202212031616382x.jpg" /></p>
<p>解压后得到<code>mysql-connector-java-5.1.48.jar</code></p>
<p>修改my.cnf，调整事务隔离级别</p>
<pre><code>[mysqld]
...
transaction-isolation=READ-COMMITTED
...</code></pre>
<p>创建数据库<code>confluence</code></p>
<p><img src="https://www.cuiwei.net/data/upload/2022-12-03/167005632853138.jpg" alt="WX202212030030192x.png" /></p>
<h2>激活</h2>
<p>激活用的是<code>atlassian-agent-v1.3.1</code>，下载链接 <a href="https://zhile.io/2018/12/20/atlassian-license-crack.html">https://zhile.io/2018/12/20/atlassian-license-crack.html</a></p>
<p>主要分两步</p>
<h3>第一步，配置<code>Agent</code></h3>
<ol>
<li>
<p>下载得到<code>atlassian-agent.jar</code>，放到合适的位置，比如：<code>/opt/atlassian/confluence/</code></p>
</li>
<li>
<p>设置环境变量<code>JAVA_OPTS</code></p>
</li>
</ol>
<p>你可以把：<code>export JAVA_OPTS="-javaagent:/path/to/atlassian-agent.jar ${JAVA_OPTS}"</code>这样的命令放到<code>.bashrc</code>或<code>.bash_profile</code>这样的文件内</p>
<h3>第二步，使⽤<code>KeyGen</code></h3>
<pre><code>java -jar atlassian-agent.jar -d -m test@test.com -n BAT -p conf -o http://localhost:8090 -s ServerID</code></pre>
<blockquote>
<p>-p参数很重要，查询方法<code>java -jar atlassian-agent.jar</code></p>
</blockquote>
<h3>构建镜像</h3>
<p>为了方便，我们借鉴一个Dockerfile</p>
<pre><code>FROM atlassian/confluence-server:7.19.4-jdk11

USER root

# 将代理破解包加入容器
COPY "atlassian-agent.jar" /opt/atlassian/confluence/

# 设置启动加载代理包
RUN echo '\nexport CATALINA_OPTS="-javaagent:/opt/atlassian/confluence/atlassian-agent.jar ${CATALINA_OPTS}"' &gt;&gt; /opt/atlassian/confluence/bin/setenv.sh</code></pre>
<p>构建</p>
<pre><code>cuiwei@weideMacBook-Pro confluence % docker build -t confluence-server:7.19.4-jdk11-1.0 .</code></pre>
<h2>docker-compose</h2>
<pre><code>version: '3'

# 使用外部网络
# docker network create server_web-network
networks:
  server_web-network:
    external: true

services:

  confluence:
    image: confluence-server:7.19.4-jdk11-1.0
    container_name: confluence
    hostname: confluence
    ports:
      - "8090:8090"
      - "8091:8091"
    restart: always
    tty: true
    volumes:
      - ./confluence/data:/var/atlassian/application-data/confluence
      - ./confluence/mysql-connector-java-5.1.48.jar:/opt/atlassian/confluence/confluence/WEB-INF/lib/mysql-connector-java-5.1.48.jar
    networks:
      - server_web-network</code></pre>
<p>启动服务</p>
<pre><code>docker-compose up -d</code></pre>
<h2>网页配置</h2>
<p>服务启动后就可以访问了，<a href="http://localhost:8090">http://localhost:8090</a></p>
<p><img src="https://www.cuiwei.net/data/upload/2022-12-03/167005914640838.jpg" alt="WX202212031718442x.png" /></p>
<p>有了上面的Server ID，就可以生成license key了</p>
<pre><code>root@confluence:/var/atlassian/application-data/confluence# cd /opt/atlassian/confluence/
#生成license key了
root@confluence:/opt/atlassian/confluence# java -jar atlassian-agent.jar -d -m test@test.com -n BAT -p conf -o http://localhost:8090 -s BLAD-0000-0000-MDJ1

#注意，填写的license key不能有换行符</code></pre>
<p>配置mysql连接信息</p>
<p><img src="https://www.cuiwei.net/data/upload/2022-12-03/167005952258523.jpg" alt="WX202212030029422x.png" /></p>
<p>后面按照提示走就可以了</p>
<h2>重置管理员密码</h2>
<pre><code>vi /opt/atlassian/confluence/bin/setenv.sh

CATALINA_OPTS="-Datlassian.recovery.password=12345678"</code></pre>
<p>然后重启confluence，你就可以使用<code>recovery_admin</code>和密码<code>12345678</code>登录了</p>
<p>登录成功后就可以重置admin的密码了</p>
<p>最后移除recovery的配置并重启</p>
<h2>参考</h2>
<p><a href="https://www.atlassian.com/zh/software/confluence/download-archives">https://www.atlassian.com/zh/software/confluence/download-archives</a></p>
<p><a href="https://confluence.atlassian.com/conf719/database-jdbc-drivers-1157467546.html">https://confluence.atlassian.com/conf719/database-jdbc-drivers-1157467546.html</a></p>
<p><a href="https://confluence.atlassian.com/doc/database-setup-for-mysql-128747.html">https://confluence.atlassian.com/doc/database-setup-for-mysql-128747.html</a></p>
<p><a href="https://hub.docker.com/r/atlassian/confluence-server">https://hub.docker.com/r/atlassian/confluence-server</a></p>
<p><a href="https://soulteary.com/2019/03/30/construct-confluence-with-docker.html">https://soulteary.com/2019/03/30/construct-confluence-with-docker.html</a></p>
<p><a href="https://www.cnblogs.com/hahaha111122222/p/13809276.html">https://www.cnblogs.com/hahaha111122222/p/13809276.html</a></p></div>]]></description>
            <guid isPermaLink="false">docker-compose快速部署confluence</guid>
        </item>
        <item>
            <title><![CDATA[ssh配置内网穿透]]></title>
            <description><![CDATA[<link rel="stylesheet" href="https://www.cuiwei.net/static/css/github-markdown.min.css?t=20260430" type="text/css" media="screen" /><div class="markdown-body"><p>上篇文章介绍了通过frp实现内网穿透 <a href="https://www.cuiwei.net/p/1427429539">https://www.cuiwei.net/p/1427429539</a></p>
<p>ssh是系统自带的，无需安装就能实现ssh服务的代理</p>
<p>模拟场景</p>
<ul>
<li>一台公网服务器(Linux，47.98.227.00)</li>
<li>一台虚拟机(Linux，相当于内网服务器)</li>
<li>ssh服务测试：通过宿主机或其他电脑访问虚拟机</li>
</ul>
<h2>公网服务器</h2>
<p>将<code>GatewayPorts</code>改为<code>yes</code>，然后重启sshd服务</p>
<pre><code>[root@iZbp1430s16l9piu268n8rZ voice]# vi /etc/ssh/sshd_config 
#GatewayPorts no
GatewayPorts yes

[root@iZbp1430s16l9piu268n8rZ voice]# systemctl restart sshd</code></pre>
<h2>虚拟机</h2>
<p>执行</p>
<pre><code>ssh -Nf -R 6999:localhost:22 root@47.98.227.00</code></pre>
<p>如上，可能会提示你输入密码，想实现免密码，将虚拟机的public key添加的公网服务器即可。<code>[root@iZbp1430s16l9piu268n8rZ voice]# echo '虚拟机的public key'&gt;&gt; ~/.ssh/authorized_keys</code></p>
<p>如果要实现自动重连，可以了解一下<code>autossh</code></p>
<h2>测试一下</h2>
<p>通过宿主机或其他电脑执行，不出意外就能连到虚拟机了</p>
<pre><code>ssh -p 6999 root@47.98.227.00</code></pre>
<h2>提示</h2>
<p>端口<code>6999</code>要在公网服务器开放</p></div>]]></description>
            <guid isPermaLink="false">ssh配置内网穿透</guid>
        </item>
        <item>
            <title><![CDATA[frp配置内网穿透]]></title>
            <description><![CDATA[<link rel="stylesheet" href="https://www.cuiwei.net/static/css/github-markdown.min.css?t=20260430" type="text/css" media="screen" /><div class="markdown-body"><p>情况是这样的，公司有台内网服务器，有一天公司要求部分人员在家办公。一般来讲，在家办公的同事想连内网服务器是不可能的。为了解决这个问题<code>内网穿透</code>就该了解一下了</p>
<p>frp 是一个专注于内网穿透的高性能的反向代理应用，支持 TCP、UDP、HTTP、HTTPS 等多种协议。可以将内网服务以安全、便捷的方式通过具有公网 IP 节点的中转暴露到公网。</p>
<p>frp有很多功能，这里只介绍ssh和web服务</p>
<p>模拟场景</p>
<ul>
<li>一台公网服务器(Linux，47.98.227.00)</li>
<li>一台虚拟机(Linux，相当于内网服务器)</li>
<li>ssh服务测试：宿主机通过frp访问虚拟机；公网服务器通过frp访问虚拟机</li>
<li>web服务测试：通过公网IP+端口号访问内网服务</li>
</ul>
<h2>ssh服务</h2>
<h3>公网服务器</h3>
<pre><code>[root@iZbp1430s16l9piu268n8rZ data]# wget https://github.com/fatedier/frp/releases/download/v0.45.0/frp_0.45.0_linux_amd64.tar.gz
[root@iZbp1430s16l9piu268n8rZ data]# tar -xvzf frp_0.45.0_linux_amd64.tar.gz 
[root@iZbp1430s16l9piu268n8rZ data]# mv frp_0.45.0_linux_amd64 frp
[root@iZbp1430s16l9piu268n8rZ data]# cd frp/
[root@iZbp1430s16l9piu268n8rZ frp]# ls
frpc  frpc_full.ini  frpc.ini  frps  frps_full.ini  frps.ini  LICENSE
[root@iZbp1430s16l9piu268n8rZ frp]# cat frps.ini
[common]
bind_port = 7000
#默认frps.ini不用修改，直接启动服务
[root@iZbp1430s16l9piu268n8rZ frp]# ./frps -c ./frps.ini</code></pre>
<h3>虚拟机</h3>
<pre><code>[root@nfsFileSystem vagrant]# wget https://github.com/fatedier/frp/releases/download/v0.45.0/frp_0.45.0_linux_amd64.tar.gz
[root@nfsFileSystem vagrant]# tar -xvzf frp_0.45.0_linux_amd64.tar.gz 
[root@nfsFileSystem vagrant]# mv frp_0.45.0_linux_amd64 frp
[root@nfsFileSystem vagrant]# cd frp/
[root@nfsFileSystem frp]# ls
frpc  frpc_full.ini  frpc.ini  frps  frps_full.ini  frps.ini  LICENSE
[root@nfsFileSystem frp]# cat frpc.ini 
[common]
server_addr = 47.98.227.00
server_port = 7000

[ssh]
type = tcp
local_ip = 127.0.0.1
local_port = 22
remote_port = 6000
#启动客户端
[root@nfsFileSystem frp]# ./frpc -c ./frpc.ini</code></pre>
<p>确认客户端，服务端都起来了，开始测试</p>
<h3>宿主机通过frp访问虚拟机</h3>
<p>实现免密码登陆，将宿主机的public key添加的虚拟机<code>[root@nfsFileSystem frp]# echo '宿主机的public key'&gt;&gt; ~/.ssh/authorized_keys</code></p>
<pre><code>cuiwei@weideMacBook-Pro nfsFileSystem % ssh -oPort=6000 root@47.98.227.00
Last login: Thu Dec  1 13:51:32 2022
[root@nfsFileSystem ~]# ls
[root@nfsFileSystem ~]# ls /vagrant/
Vagrantfile</code></pre>
<h3>公网服务器通过frp访问虚拟机</h3>
<p>实现免密码登陆，将公网服务器的public key添加的虚拟机<code>[root@nfsFileSystem frp]# echo '公网服务器的public key'&gt;&gt; ~/.ssh/authorized_keys</code></p>
<pre><code>[root@iZbp1430s16l9piu268n8rZ voice]# ssh -oPort=6000 root@47.98.227.00
Last login: Thu Dec  1 13:59:05 2022 from 127.0.0.1
[root@nfsFileSystem ~]# ls
[root@nfsFileSystem ~]# ls /vagrant/
Vagrantfile</code></pre>
<h2>web服务</h2>
<h3>公网服务器</h3>
<p>在<code>frps.ini</code>追加以下配置</p>
<pre><code>vhost_http_port = 8999</code></pre>
<p>然后，重新启动服务端</p>
<pre><code>[root@iZbp1430s16l9piu268n8rZ frp]# ./frps -c ./frps.ini</code></pre>
<h3>虚拟机</h3>
<p>准备一个web服务，确保可以通过<code>localhost:8080</code>访问</p>
<p>在<code>frpc.ini</code>追加以下配置</p>
<pre><code>[web]
type = http
local_port = 8080
custom_domains = frp.cw.net</code></pre>
<p>然后，重新启动客户端</p>
<pre><code>[root@nfsFileSystem frp]# ./frpc -c ./frpc.ini</code></pre>
<h3>测试一下</h3>
<p>访问 <a href="http://frp.cw.net:8999/">http://frp.cw.net:8999/</a></p>
<p>不出意外看到的就是虚拟机8080端口提供的服务</p>
<h2>仪表盘</h2>
<p>在<code>frps.ini</code>追加以下配置</p>
<pre><code>dashboard_port = 7500
# dashboard's username and password are both optional
dashboard_user = admin
dashboard_pwd = admin</code></pre>
<p>然后，重新启动服务端</p>
<pre><code>[root@iZbp1430s16l9piu268n8rZ frp]# ./frps -c ./frps.ini</code></pre>
<h3>测试一下</h3>
<p>访问 <a href="http://47.98.227.00:7500/">http://47.98.227.00:7500/</a></p>
<p><img src="https://www.cuiwei.net/data/upload/2022-12-01/166990993617173.jpg" alt="WX202212012350182x.png" /></p>
<h2>提示</h2>
<p>上面提到的端口7000，6000，8999，7500都需要在公网服务器放开</p>
<p>优化：上面的客户端，服务端的启动方式可以改为后台进程，交给 Supervisor 管理。可参考 <a href="https://www.cuiwei.net/p/1109683129">https://www.cuiwei.net/p/1109683129</a></p>
<h2>参考</h2>
<p><a href="https://github.com/fatedier/frp">https://github.com/fatedier/frp</a></p></div>]]></description>
            <guid isPermaLink="false">frp配置内网穿透</guid>
        </item>
        <item>
            <title><![CDATA[Jenkins自动构建vue项目]]></title>
            <description><![CDATA[<link rel="stylesheet" href="https://www.cuiwei.net/static/css/github-markdown.min.css?t=20260430" type="text/css" media="screen" /><div class="markdown-body"><p>Jenkins安装 请参考：<a href="https://www.cuiwei.net/p/1392307197">https://www.cuiwei.net/p/1392307197</a></p>
<h2>ssh连接gitee</h2>
<pre><code>#生成公钥，私钥
root@edfd04c7ec00:/# ssh-keygen -t rsa -C "jenkins"

公钥配到gitee：https://gitee.com/profile/sshkeys

私钥配到Jenkins：Dashboard -&gt; 系统管理 -&gt; 凭据 -&gt; 系统 -&gt; 全局凭据 (unrestricted)</code></pre>
<p>如上配置完，在拉取项目(git@gitee.com:chudaozhe/enterprise-admin.git)时，可能报错</p>
<pre><code>returned status code 128:
stdout: 
stderr: No ECDSA host key is known for gitee.com and you have requested strict checking.
Host key verification failed.
fatal: Could not read from remote repository.</code></pre>
<p><img src="https://www.cuiwei.net/data/upload/2022-11-28/166964931022711.jpg" alt="WX202211282328012x.png" /></p>
<p>这时，配置一下<code>Git Host Key Verification Configuration</code>就可以了</p>
<p><img src="https://www.cuiwei.net/data/upload/2022-12-01/166982895973439.jpg" alt="WX202212010121382x.png" /></p>
<h2>构建vue项目</h2>
<h3>配置nodejs</h3>
<p>Dashboard -&gt; 系统管理 -&gt; 插件管理 -&gt; 可选插件</p>
<p>搜索“nodejs”，安装即可</p>
<p>接着，Dashboard -&gt; 系统管理 -&gt; 全局工具配置</p>
<p>配置<code>https://mirrors.aliyun.com/nodejs-release/</code></p>
<p><img src="https://www.cuiwei.net/data/upload/2022-11-28/166964977799737.jpg" alt="WX202211282335352x.jpg" /></p>
<p>接着，测试一下</p>
<p><img src="https://www.cuiwei.net/data/upload/2022-11-28/166965035781807.jpg" alt="WX202211282345182x.jpg" /></p>
<p>成功了</p>
<p><img src="https://www.cuiwei.net/data/upload/2022-11-28/166965047969130.jpg" alt="WX202211282347202x.png" /></p>
<p>如上，是成功了，但因为没有加到环境变量，所以直接执行<code>node -v</code>是不行的</p>
<pre><code>[root@nfsFileSystem vagrant]# node -v
bash: node: command not found
[root@nfsFileSystem vagrant]# /var/lib/jenkins/tools/jenkins.plugins.nodejs.tools.NodeJSInstallation/nodejs19/bin/node -v
v19.2.0</code></pre>
<h3>clone并构建vue项目</h3>
<p><img src="https://www.cuiwei.net/data/upload/2022-11-28/166965096575610.jpg" alt="WX202211282354372x.png" /></p>
<p><img src="https://www.cuiwei.net/data/upload/2022-11-28/166965097956453.jpg" alt="WX202211282355072x.png" /></p>
<p>配置shell脚本</p>
<pre><code>npm install --registry=https://registry.npm.taobao.org
npm run build</code></pre>
<p><img src="https://www.cuiwei.net/data/upload/2022-11-28/166965099131363.jpg" alt="WX202211282355262x.png" /></p>
<p>构建成功，会多一个<code>dist</code>目录</p>
<p><img src="https://www.cuiwei.net/data/upload/2022-11-28/166965112236288.jpg" alt="WX202211282357422x.jpg" /></p>
<p>日志</p>
<pre><code>First time build. Skipping changelog.
Unpacking https://mirrors.aliyun.com/nodejs-release/v19.2.0/node-v19.2.0-linux-x64.tar.gz to /var/lib/jenkins/tools/jenkins.plugins.nodejs.tools.NodeJSInstallation/nodejs19 on Jenkins
[vue_admin] $ /bin/sh -xe /tmp/jenkins9582283174751732397.sh
+ npm install --registry=https://registry.npm.taobao.org
npm WARN deprecated source-map-url@0.4.1: See https://github.com/lydell/source-map-url#deprecated
...
added 1019 packages in 50s
+ npm run build

&gt; vue_admin@1.0.0 build
&gt; node build/build.js

(node:5656) Warning: Accessing non-existent property 'cat' of module exports inside circular dependency
(Use `node --trace-warnings ...` to show where the warning was created)
(node:5656) Warning: Accessing non-existent property 'cd' of module exports inside circular dependency
...
Starting to optimize CSS...
Processing static/css/app.67d5653a5ceee5df23c290c1efb8acd9.css...
Processed static/css/app.67d5653a5ceee5df23c290c1efb8acd9.css, before: 176458, after: 173025, ratio: 98.05%
Hash: ab5f3662432c354b153a
Version: webpack 2.7.0
Time: 30863ms
                                                  Asset       Size  Chunks                    Chunk Names
                 static/fonts/element-icons.b02bdc1.ttf    13.2 kB          [emitted]         
               static/js/vendor.a11188574e8c22a95fd9.js    1.19 MB       0  [emitted]  [big]  vendor
                  static/js/app.842d7b541ae50b8e1e0d.js     159 kB       1  [emitted]         app
             static/js/manifest.2496bc72a04420c949f3.js    1.51 kB       2  [emitted]         manifest
    static/css/app.67d5653a5ceee5df23c290c1efb8acd9.css     173 kB       1  [emitted]         app
           static/js/vendor.a11188574e8c22a95fd9.js.map    8.27 MB       0  [emitted]         vendor
              static/js/app.842d7b541ae50b8e1e0d.js.map     806 kB       1  [emitted]         app
static/css/app.67d5653a5ceee5df23c290c1efb8acd9.css.map     226 kB       1  [emitted]         app
         static/js/manifest.2496bc72a04420c949f3.js.map    14.7 kB       2  [emitted]         manifest
                                             index.html  492 bytes          [emitted]         
                                          static/xx.png    2.63 kB          [emitted]         
                                static/simditor-html.js    3.47 kB          [emitted]         
                                static/beautify-html.js    66.8 kB          [emitted]         

  Build complete.

  Tip: built files are meant to be served over an HTTP server.
  Opening index.html over file:// won't work.

Finished: SUCCESS</code></pre>
<h2>参考</h2>
<p><a href="https://blog.csdn.net/qq_39905409/article/details/122449380">https://blog.csdn.net/qq_39905409/article/details/122449380</a></p></div>]]></description>
            <guid isPermaLink="false">Jenkins自动构建vue项目</guid>
        </item>
        <item>
            <title><![CDATA[Jenkins自动构建docker镜像，并推送到阿里云]]></title>
            <description><![CDATA[<link rel="stylesheet" href="https://www.cuiwei.net/static/css/github-markdown.min.css?t=20260430" type="text/css" media="screen" /><div class="markdown-body"><p>这篇主要介绍使用Jenkins自动从git仓库拉取代码并构建镜像，最后推送到阿里云</p>
<p>Jenkins安装 请参考：<a href="https://www.cuiwei.net/p/1392307197">https://www.cuiwei.net/p/1392307197</a></p>
<p>ssh连接gitee 请参考：<a href="https://www.cuiwei.net/p/1475072228">https://www.cuiwei.net/p/1475072228</a></p>
<h2>准备</h2>
<ol>
<li>配置证书</li>
</ol>
<p><img src="https://www.cuiwei.net/data/upload/2022-12-01/166987858522185.jpg" alt="WX202212011448592x.jpg" /></p>
<ol start="2">
<li>了解两个插件：<code>docker-build-step</code> 和 <code>Version Number</code>，其中<code>Version Number</code>不是必须的</li>
</ol>
<h3>Version Number</h3>
<p>支持生成更复杂的版本信息。</p>
<p>在 “某任务” - “配置” - “构建环境” 找到 “Create a formatted version number”。这里配置 增加变量<code>BUILD_VERSION</code>，格式为 <code>${BUILD_DATE_FORMATTED, "yyyy-MM-dd"}.${BUILD_NUMBER}</code>。</p>
<p><img src="https://www.cuiwei.net/data/upload/2022-12-01/166987244481789.jpg" alt="WX202212011326532x.png" /></p>
<p>构建镜像时使用这个变量<code>BUILD_VERSION</code></p>
<p><img src="https://www.cuiwei.net/data/upload/2022-12-01/166987307142897.jpg" alt="WX202212011332232x.png" /></p>
<p>看结果，默认的镜像名称是<code>BUILD_NUMBER</code>，现在已经变成<code>${BUILD_DATE_FORMATTED, "yyyy-MM-dd"}.${BUILD_NUMBER}</code></p>
<p><img src="https://www.cuiwei.net/data/upload/2022-12-01/166987314842072.jpg" alt="WX202212011336482x.png" /></p>
<h3>docker-build-step</h3>
<p>Set Docker URL
In Jenkins global configuration, you need to specify Docker REST API URL.</p>
<p>Jenkins -&gt; Manage Jenkins -&gt; Configure System -&gt; Docker Builder</p>
<ul>
<li>Configure Docker server REST API URL
<ul>
<li>For Linux nodes, set the local socket <code>unix:///var/run/docker.sock</code></li>
<li>For other nodes, you may need to set something like <code>tcp://127.0.0.1:2375</code></li>
</ul></li>
<li>Test the connection.</li>
</ul>
<p><img src="https://www.cuiwei.net//data/upload/2022-11-29/166973012265968.jpg" alt="WX20221129-001444@2x.png" /></p>
<h2>具体操作</h2>
<p>拉取master的代码 -&gt; Creat/build image -&gt; Tag image -&gt; Push image -&gt; Remove image</p>
<p><img src="https://www.cuiwei.net/data/upload/2022-12-01/166987663487555.jpg" alt="WX202212011429202x.png" /></p>
<p><img src="https://www.cuiwei.net/data/upload/2022-12-01/166987665653353.jpg" alt="WX202212011429412x.png" /></p>
<p><img src="https://www.cuiwei.net/data/upload/2022-12-01/166987667370771.jpg" alt="WX202212011430592x.png" /></p>
<p><img src="https://www.cuiwei.net/data/upload/2022-12-01/166987668882459.jpg" alt="WX202212011436462x.png" />
日志</p>
<pre><code>[Docker] INFO: Step 1/1 : FROM busybox:latest
[Docker] INFO: 

[Docker] INFO:  ---&gt; 9d5226e6ce3f

[Docker] INFO: Successfully built 9d5226e6ce3f

[Docker] INFO: Successfully tagged 2022-12-01.28:latest

[Docker] INFO: Build image id:9d5226e6ce3f
[Docker] INFO: start tagging image 2022-12-01.28:latest in registry.cn-hangzhou.aliyuncs.com/cuiw/test as 28
[Docker] INFO: Tagged image 2022-12-01.28:latest in registry.cn-hangzhou.aliyuncs.com/cuiw/test as 28
[Docker] INFO: Pushing image registry.cn-hangzhou.aliyuncs.com/cuiw/test:28
[Docker] INFO: Done pushing image registry.cn-hangzhou.aliyuncs.com/cuiw/test:28
[Docker] INFO: Removed image 2022-12-01.28:latest
Finished: SUCCESS</code></pre>
<h2>常见问题</h2>
<h3>问题1</h3>
<pre><code>[Docker] ERROR: Failed to create docker image: connect(..) failed: Permission denied: /var/run/docker.sock</code></pre>
<p>依次执行以下命令，然后重启就好了</p>
<pre><code>gpasswd -a jenkins docker
newgrp docker</code></pre>
<h3>问题2</h3>
<pre><code>[Docker] ERROR: Failed to exec start:null
[Docker] ERROR: Failed to create docker image: null
ERROR: Build step failed with exception
java.util.NoSuchElementException
    at java.base/java.util.ArrayDeque.removeFirst(ArrayDeque.java:363)
...</code></pre>
<p>这个问题很严重，折腾我好几天。有时行，有时不行（很多时候都不行），暂时无解！</p>
<h2>参考</h2>
<p><a href="https://www.jianshu.com/p/5c3e6f2c5d15">https://www.jianshu.com/p/5c3e6f2c5d15</a></p>
<p><a href="https://blog.csdn.net/qq_39905409/article/details/122449380">https://blog.csdn.net/qq_39905409/article/details/122449380</a></p>
<p><a href="https://plugins.jenkins.io/docker-build-step/">https://plugins.jenkins.io/docker-build-step/</a></p>
<p><a href="https://plugins.jenkins.io/versionnumber/">https://plugins.jenkins.io/versionnumber/</a></p></div>]]></description>
            <guid isPermaLink="false">Jenkins自动构建docker镜像，并推送到阿里云</guid>
        </item>
        <item>
            <title><![CDATA[使用git钩子实现自动部署]]></title>
            <description><![CDATA[<link rel="stylesheet" href="https://www.cuiwei.net/static/css/github-markdown.min.css?t=20260430" type="text/css" media="screen" /><div class="markdown-body"><p>git有很多钩子，分为客户端钩子和服务端钩子</p>
<p>下面主要使用了服务端钩子：post-receive</p>
<blockquote>
<p>当 push 动作已经完成的时候会被触发，可以用此 hook 来 push notification 等，比如发邮件，通知持续构建服务器等。</p>
</blockquote>
<h2>准备</h2>
<p>先创建一个仓库</p>
<pre><code>#先切到git用户
su git
mkdir -p ~/blog.git
cd ~/blog.git
git --bare init</code></pre>
<h2>配置钩子</h2>
<pre><code>
#进到git仓库目录
cd ~/blog.git/hooks
#配置test分支自动部署，其中-f参数：强制移动指针，忽略本地变化，使用git log是看不到提交记录的
vi post-receive
#!/bin/sh
git --work-tree=/data/www/blog checkout test -f

#给可执行权限
chmod +x post-receive</code></pre>
<p>注意：
项目目录得有git:git权限，git控制之外的<code>文件/目录</code>可以是其他权限</p>
<pre><code>chmod -R git:git /data/www/blog</code></pre>
<h2>参考</h2>
<p><a href="https://www.jianshu.com/p/e4db2050305f">https://www.jianshu.com/p/e4db2050305f</a></p></div>]]></description>
            <guid isPermaLink="false">使用git钩子实现自动部署</guid>
        </item>
        <item>
            <title><![CDATA[Jenkins的两种安装方式]]></title>
            <description><![CDATA[<link rel="stylesheet" href="https://www.cuiwei.net/static/css/github-markdown.min.css?t=20260430" type="text/css" media="screen" /><div class="markdown-body"><h2>普通方式</h2>
<p>yum安装</p>
<pre><code>  sudo wget -O /etc/yum.repos.d/jenkins.repo https://pkg.jenkins.io/redhat-stable/jenkins.repo
  sudo rpm --import https://pkg.jenkins.io/redhat-stable/jenkins.io.key
  yum install fontconfig java-11-openjdk
  yum install jenkins</code></pre>
<p>常用命令</p>
<pre><code>systemctl enable jenkins
systemctl start jenkins
systemctl status jenkins</code></pre>
<p><a href="https://pkg.jenkins.io/redhat-stable/">https://pkg.jenkins.io/redhat-stable/</a></p>
<p><a href="https://www.jenkins.io/doc/book/installing/linux/#red-hat-centos">https://www.jenkins.io/doc/book/installing/linux/#red-hat-centos</a></p>
<h2>docker-compose.yml</h2>
<pre><code>version: '3.1'
services:
  jenkins:
    image: jenkins/jenkins:2.361.4-lts-jdk11
    volumes:
      - ./data/jenkins/:/var/jenkins_home
      - /var/run/docker.sock:/var/run/docker.sock
      - /usr/bin/docker:/bin/docker
      - .ssh:/root/.ssh
    ports:
      - "8088:8080"
#    容器内获取宿主机的root权限
#    privileged: true
    user: root #要不 docker pull 会没权限
    restart: always
    container_name: jenkins
#    environment:
#      JAVA_OPTS: '-Djava.util.logging.config.file=/var/jenkins_home/log.properties'</code></pre>
<p>如上，为了在容器内使用<code>docker</code>命令，所以额外挂载了</p>
<pre><code>      - /var/run/docker.sock:/var/run/docker.sock
      - /usr/bin/docker:/bin/docker</code></pre>
<p>但是，这种只在Linux下可用，Mac下我没成功，所以下面的介绍都是基于<code>CentOS</code>虚拟机的：虚拟机里安装了docker，docker-compose</p>
<pre><code>[root@nfsFileSystem vagrant]# cd docker-jenkins/
#启动容器
[root@nfsFileSystem docker-jenkins]# docker-compose up -d</code></pre></div>]]></description>
            <guid isPermaLink="false">Jenkins的两种安装方式</guid>
        </item>
        <item>
            <title><![CDATA[git常用操作]]></title>
            <description><![CDATA[<link rel="stylesheet" href="https://www.cuiwei.net/static/css/github-markdown.min.css?t=20260430" type="text/css" media="screen" /><div class="markdown-body"><h2>配置</h2>
<pre><code>//文件名需要区分大小写。windows不区分，所以需要本地设置
git config core.ignorecase false
</code></pre>
<h2>分支</h2>
<pre><code>//部署test分支
git checkout test &amp;&amp; git pull

//本地分支列表
git branch

//删除test分支
git branch -d test
//强制删除test分支
git branch -D test

//新建test2分支，并切换到test2分支
git checkout -b test2
//相当于
git branch test2 &amp;&amp; git checkout test2
</code></pre>
<h2>打标签</h2>
<p><a href="https://blog.csdn.net/jinking01/article/details/121363836">https://blog.csdn.net/jinking01/article/details/121363836</a></p>
<pre><code>#查看已有的tag
git tag

#打tag
git tag -a v1.1 -m "选择签名校验的方式"

#推送到远程
git push -u origin v1.1

#删除tag
git tag -d v1.1
</code></pre>
<h2>git stash和git stash pop</h2>
<p>应用场景：你正在dev分支写代码，测试人员在test分支发现了严重问题，要求马上处理。这时你本地有未提交的代码，直接切test分支可能会有冲突。这时就该<code>git stash</code>出场了</p>
<pre><code>git stash //将修改存储到暂存区，工作区会删除这些修改
git checkout test
//。。。修复bug
//修复完成，提交修改
//切换到dev，并恢复暂存的代码
git checkout dev
git stash pop

https://blog.csdn.net/qq_36898043/article/details/79431168</code></pre>
<h2>git checkout</h2>
<p><a href="https://blog.csdn.net/wangdawei_/article/details/124567178">https://blog.csdn.net/wangdawei_/article/details/124567178</a></p>
<h3>切换本地分支</h3>
<p>切换到test分支</p>
<pre><code>git checkout test</code></pre>
<h3>切换远程分支</h3>
<p>//从<code>origin/rbac</code>新建rbac分支，并切换到rbac分支</p>
<pre><code>git checkout -b rbac origin/rbac</code></pre>
<h3>放弃修改</h3>
<p>放弃所有工作区的修改</p>
<pre><code>git checkout .</code></pre>
<p><img src="https://www.cuiwei.net/data/upload/2022-12-20/167150314079427.jpg" alt="9a05425794c04bd8a74c486ec41f69ab.png" /></p>
<p>放弃对指定文件的修改</p>
<pre><code>git checkout -- filename</code></pre>
<p><img src="https://www.cuiwei.net/data/upload/2022-12-20/167150347060567.jpg" alt="f72b61d6496145f18770156206ce7af2.png" /></p>
<p>放弃工作区和暂存区的所有修改</p>
<pre><code>git checkout -f</code></pre>
<p><img src="https://www.cuiwei.net/data/upload/2022-12-20/167150385720951.jpg" alt="4362f9c3908c4ccda4cfa80ab22f0920.png" /></p>
<h2>git diff</h2>
<p>查看修改了哪些文件（commit之前）</p>
<pre><code>#显示新增、修改、删除的文件清单。
git diff --name-status

#仅在提交信息后显示已修改的文件清单。
git diff --name-only

#显示commit1以来的修改
git diff --name-only commit1

#显示两次commit之间的修改
git diff --name-only commit1 commit2

#例如
cuiwei@weideMacBook-Pro aaa % git diff --name-only
application/config.php</code></pre>
<h3>将修改的文件复制出来，并保留原来的目录结构</h3>
<pre><code>&lt;?php
$from=__DIR__.'/';
$to=__DIR__.'/update/';
$path =&lt;&lt;&lt;_END
application/config.php
_END;

$path_arr=explode("\n", $path);

foreach ($path_arr as $path){
    if (empty($path)) continue;
    $file=$from.$path;
    $to_file=$to.$path;
    is_dir(dirname($to_file)) || mkdir(dirname($to_file), 0777, true);
    copy($file, $to_file);
    echo $to_file.PHP_EOL;
}
echo 'ok'.PHP_EOL;</code></pre>
<p><a href="https://blog.csdn.net/liuxiao723846/article/details/109689069">https://blog.csdn.net/liuxiao723846/article/details/109689069</a></p></div>]]></description>
            <guid isPermaLink="false">git常用操作</guid>
        </item>
        <item>
            <title><![CDATA[PhpStorm 配置 Xdebug 3，及常见问题]]></title>
            <description><![CDATA[<link rel="stylesheet" href="https://www.cuiwei.net/static/css/github-markdown.min.css?t=20260430" type="text/css" media="screen" /><div class="markdown-body"><p>做PHP开发也很多年了，Xdebug也配过很多次，但总觉得不是刚需，感觉有没有都行；另一方面它配置复杂（新方法很简单），每个项目都得配一遍（新方法也避免不了），并且在公司配完，回到家还得修改hosts 中的ip（新方法不需要）。种种原因，使用Xdebug的习惯并没有延续下来。直到我近日接触了yii2中的事件，比如：</p>
<pre><code>$config = [
    'components' =&gt; [
...
    ],
    'on beforeRequest' =&gt; function($event) {
        \yii\base\Event::on(\yii\db\BaseActiveRecord::class,
            \yii\db\BaseActiveRecord::EVENT_AFTER_FIND, ['common\models\Operate', 'RecordOperateInfo']);
    },
]</code></pre>
<p>假如要调试<code>common\models\Operate</code>中的<code>RecordOperateInfo</code>，如果没有Xdebug，你可能会先写一个临时控制器，再调一下这个model方法，当然也行。但有了Xdebug就方便很多了，你可以直接在model方法中下断点。</p>
<p>下面看下如何配置</p>
<h2>配置</h2>
<p>这里我的PHP环境是基于docker的，非docker的也大同小异</p>
<h3>修改php.ini</h3>
<pre><code>[xdebug]
zend_extension = xdebug.so
xdebug.mode=debug
xdebug.client_host=host.docker.internal
;xdebug.discover_client_host=yes
xdebug.client_port=9003
xdebug.start_with_request=yes</code></pre>
<p>注意，你的php环境用的<code>docker-compose.yml</code>可能需要修改一下，允许容器内通过localhost访问宿主机（是可能，未验证！）</p>
<pre><code>  docker-php-fpm:
    image: php:1.1-work
    hostname: php-fpm
    extra_hosts: #允许容器内通过localhost访问宿主机
      - host.docker.internal:host-gateway
    networks:
      - web-network</code></pre>
<h2>使用</h2>
<p>PhpStorm 要配置的很少，直接开始</p>
<p>第一步</p>
<p><img src="https://www.cuiwei.net/data/upload/2022-11-12/166825911296965.jpg" alt="1111.jpg" /></p>
<p>第二步</p>
<p>访问接口，触发事件。这时你会看到如下弹窗</p>
<p><img src="https://www.cuiwei.net/data/upload/2022-11-12/166825916377408.jpg" alt="2222.jpg" /></p>
<p>第三步</p>
<p>打开 PhpStorm 的设置，如下</p>
<p><img src="https://www.cuiwei.net/data/upload/2022-11-12/166825924225221.jpg" alt="3333.jpg" /></p>
<p>第四步</p>
<p>再次访问接口，就成功了</p>
<p><img src="https://www.cuiwei.net/data/upload/2022-11-12/166825932263984.jpg" alt="4444.jpg" /></p>
<h2>CLI模式</h2>
<p>平时开发中cli模式也是用的比较多的。当你配置完上面那些，准备进入容器，调试控制台脚本时</p>
<pre><code>cuiwei@weideMacBook-Pro yii-demo % docker exec -it e31afbaf8164d7b2abf2a38f5fe22477fe681ef6b663797150cb56bdbaf1441f /bin/sh
# bash
root@php-fpm:/var/www/html# cd ../yii-demo
root@php-fpm:/var/www/yii-demo# php test.php</code></pre>
<p>会看到如下错误：</p>
<p><img src="https://www.cuiwei.net/data/upload/2022-11-23/166921695793082.jpg" alt="WX202211232321142x.jpg" /></p>
<p>这时你只需设置一下<code>PHP_IDE_CONFIG</code>环境变量即可，其他都是多余</p>
<pre><code>root@php-fpm:/var/www/yii-demo# export PHP_IDE_CONFIG="serverName=yii.cw.net" </code></pre>
<h2>常见问题</h2>
<p>如果不成功，第一个需要确认的是9003端口是否可用</p>
<p>注意，先开启监听，如下</p>
<p><img src="https://www.cuiwei.net/data/upload/2022-11-12/166825983364912.jpg" alt="5555.jpg" /></p>
<p>然后在本机和php容器内分别执行</p>
<pre><code>cuiwei@weideMacBook-Pro ~ % telnet 127.0.0.1 9003
root@php-fpm:/var/www/html# telnet host.docker.internal 9003</code></pre>
<h3>问题1</h3>
<p>通常我们配置nginx的时候，想通过ip访问，会这么配</p>
<pre><code>server {
    listen      8080;
    server_name _;
    index index.php index.html index.htm default.html;
    root        /var/www;
}</code></pre>
<p>这种想用xdebug调试就不行了（没深究，可能有其他方式），需要把<code>server_name</code>配置成具体的，比如<code>localhost</code></p>
<h3>问题2</h3>
<p>有时候我们会通过代理的方式让多个项目共用一个域名，比如：前缀为<code>xx.com/api/user</code>的反向代理到8090端口，前缀为<code>xx.com/api/admin</code>的反向代理到8091端口。这种想访问某个接口（xx.com/api/user/1）来调试的也不行，xdebug只会识别出端口号为8090的server_name，这时 <code>server_name</code> 的值肯定为 <code>_</code>。所以这种情况就先别用代理了</p>
<h3>问题3</h3>
<p>如下图，也是常遇到的，总的来说就是程序没执行到下断点的位置</p>
<p><img src="https://www.cuiwei.net/data/upload/2022-11-23/166921792498935.jpg" alt="WX202211232337432x.png" /></p>
<h2>总结</h2>
<p>这种配置方式还是挺简单的，每个项目只需配置一下容器内的根目录即可。</p>
<p>注意，上文提到的<code>yii.cw.net</code>是我通过修改hosts自定义的域名</p>
<pre><code>cuiwei@weideMacBook-Pro ~ % cat /etc/hosts
127.0.0.1 yii.cw.net</code></pre>
<p>这样，不管你怎么切换网络都不影响</p></div>]]></description>
            <guid isPermaLink="false">PhpStorm 配置 Xdebug 3，及常见问题</guid>
        </item>
        <item>
            <title><![CDATA[yii2-queue队列的使用]]></title>
            <description><![CDATA[<link rel="stylesheet" href="https://www.cuiwei.net/static/css/github-markdown.min.css?t=20260430" type="text/css" media="screen" /><div class="markdown-body"><h2>安装</h2>
<pre><code>composer require yiisoft/yii2-queue</code></pre>
<p>配置</p>
<pre><code>//cat config/console.php
return [
    'bootstrap' =&gt; [
        'queue',
    ],
    'components' =&gt; [
        'redis' =&gt; [
            'class' =&gt; 'yii\redis\Connection',
            'hostname' =&gt; 'docker-redis',
            'port' =&gt; 6379,
            'database' =&gt; 0,
            'retries' =&gt; 1,
        ],
//        'queue' =&gt; [
//            'class' =&gt; \yii\queue\file\Queue::class,
//        ],
        'queue' =&gt; [
            'class' =&gt; \yii\queue\redis\Queue::class,
            'redis' =&gt; 'redis', // Redis connection component or its config
            'channel' =&gt; 'queue', // Queue channel key
        ],
    ],
];</code></pre>
<h2>创建任务</h2>
<p>任务1</p>
<pre><code>&lt;?php
namespace app\job;
use yii\base\BaseObject;

class Download extends BaseObject implements \yii\queue\JobInterface {
    public $url;
    public $file;

    public function execute($queue) {
        file_put_contents($this-&gt;file, file_get_contents($this-&gt;url));
    }
}</code></pre>
<p>任务2</p>
<pre><code>&lt;?php
namespace app\job;

class SendEmail extends \yii\base\Component implements \yii\queue\JobInterface {
    public $url;
    public $title;

    public function execute($queue) {
        file_put_contents($this-&gt;url,"email") ;
    }
}</code></pre>
<p>将任务添加到队列，等待执行</p>
<pre><code>        \Yii::$app-&gt;queue-&gt;push(new Download([
            'url' =&gt; 'https://www.baidu.com/img/PCtm_d9c8750bed0b3c7d089fa7d55720d6cf.png',
            'file' =&gt; \Yii::getAlias("@app/runtime/down/bd.jpg"),
        ]));

        //delay单位为秒
        $rs = \Yii::$app-&gt;queue-&gt;delay(10)-&gt;push(new SendEmail([
            'url' =&gt; \Yii::getAlias("@app/runtime/mail/aaa"),
            'title'=&gt;123,
        ]));
        var_dump($rs);</code></pre>
<h2>执行任务</h2>
<p>将队列里的任务执行一遍，适合做计划任务</p>
<pre><code>./yii queue/run</code></pre>
<p>创建一个守护进程，实时监听队列，有新任务就执行</p>
<pre><code>./yii queue/listen //实时监听
./yii queue/listen 5 //每隔5s监听一次队列</code></pre>
<p>其他命令</p>
<pre><code>yii queue/info 查看队列状态
yii queue/clear 清空队列
yii queue/remove [id] 移除某个任务</code></pre>
<h2>参考</h2>
<p><a href="https://github.com/yiisoft/yii2-queue/blob/master/docs/guide/driver-redis.md">https://github.com/yiisoft/yii2-queue/blob/master/docs/guide/driver-redis.md</a></p></div>]]></description>
            <guid isPermaLink="false">yii2-queue队列的使用</guid>
        </item>
        <item>
            <title><![CDATA[go 检测数据竞争 race]]></title>
            <description><![CDATA[<link rel="stylesheet" href="https://www.cuiwei.net/static/css/github-markdown.min.css?t=20260430" type="text/css" media="screen" /><div class="markdown-body"><p>Go（从v1.1开始）具有内置的数据竞争检测器，可以使用它来检测潜在的数据竞争。</p>
<p>使用：</p>
<p>运行时检查竟态的命令：<code>go run -race main.go</code></p>
<p>构建时检查竟态的命令：<code>go build -race main.go</code></p>
<p>测试时检查竟态的命令：<code>go test -race main.go</code></p>
<p>总结一下，其实就是race选项其实就是检测数据的安全性的，同时读写（而不是同时读，同时写），等情况。</p></div>]]></description>
            <guid isPermaLink="false">go 检测数据竞争 race</guid>
        </item>
        <item>
            <title><![CDATA[go 不一样的grpc入门示例]]></title>
            <description><![CDATA[<link rel="stylesheet" href="https://www.cuiwei.net/static/css/github-markdown.min.css?t=20260430" type="text/css" media="screen" /><div class="markdown-body"><p>之所以说不一样，是因为在这里你看不到<code>.proto</code>文件，当然也无需生成任何代码</p>
<p>之所以这么神奇是我用了<code>buf</code>，它托管了我的<code>.proto</code>文件，并且自动生成了相关代码，详情请移步：<a href="https://www.cuiwei.net/p/1679512807">https://www.cuiwei.net/p/1679512807</a></p>
<h2>服务端极简示例</h2>
<p>下面的示例提供了一个<code>AdminLogin</code>服务</p>
<p>server.go</p>
<pre><code>package main

import (
    "context"
    blogv1 "go.buf.build/grpc/go/cuiwei/blog/admin/v1"
    "google.golang.org/grpc"
    "net"
)

type blogService struct {
    *blogv1.UnimplementedBlogServiceServer
}

func (s *blogService) AdminLogin(ctx context.Context, in *blogv1.AdminLoginRequest) (*blogv1.AdminLoginResponse, error) {
    return &amp;blogv1.AdminLoginResponse{
        Id:       1,
        Username: "aa",
        Token:    "xx",
    }, nil
}

func main() {
    lis, _ := net.Listen("tcp", ":9087")
    gRPCServer := grpc.NewServer()
    blogv1.RegisterBlogServiceServer(gRPCServer, &amp;blogService{})
    gRPCServer.Serve(lis)
}</code></pre>
<h2>客户端极简示例</h2>
<p>下面的客户端示例也可以说是一个测试用例，执行<code>TestBlogServerLogin</code>方法可以验证<code>AdminLogin</code>服务的可用性</p>
<p>server_test.go</p>
<pre><code>package main

import (
    "context"
    "fmt"
    blogv1 "go.buf.build/grpc/go/cuiwei/blog/admin/v1"
    "google.golang.org/grpc"
    "google.golang.org/grpc/credentials/insecure"
    "testing"
)

var conn *grpc.ClientConn
var client blogv1.BlogServiceClient

func init() {
    //conn, err = grpc.Dial(":9087", grpc.WithInsecure()) //过时的
    conn, _ = grpc.Dial(":9087", grpc.WithTransportCredentials(insecure.NewCredentials()))
    client = blogv1.NewBlogServiceClient(conn)
}

func TestBlogServiceLogin(t *testing.T) {
    resp, err := client.AdminLogin(context.Background(), &amp;blogv1.AdminLoginRequest{
        Username: "admin22",
        Password: "123",
    })
    if err != nil {
        t.Error(err)
        return
    }
    fmt.Println(resp)
}</code></pre>
<h2>目录结构</h2>
<p>如上，服务端和客户端都是一个文件，实际项目中肯定不这样。如何拆分呢，这对新手来说可能是个问题。我这里抛砖引玉，下面只说服务端</p>
<p>将上文的<code>server.go</code>改名为<code>main.go</code>，并把里面的<code>&amp;blogService{}</code>移出去，移到两个目录，分别是<code>services</code>，<code>api</code>目录。<code>services/BlogService.go</code>定义了全部的服务，具体实现在<code>api</code>目录</p>
<p>将上文的<code>server_test.go</code>移到为<code>services/blog_service_test.go</code></p>
<p>最终等到的目录结构如下</p>
<pre><code>.
├── README.md
├── api
│   ├── admin.go
│   └── article.go
├── go.mod
├── go.sum
├── main.go
└── services
    ├── BlogService.go
    └── blog_service_test.go</code></pre>
<p>最后是关键文件的代码</p>
<p>main.go</p>
<pre><code>package main

import (
    "demo/grpc/services"
    blogv1 "go.buf.build/grpc/go/cuiwei/blog/admin/v1"
    "google.golang.org/grpc"
    "net"
)

func main() {
    lis, _ := net.Listen("tcp", ":9087")
    gRPCServer := grpc.NewServer()
    blogv1.RegisterBlogServiceServer(gRPCServer, services.BlogService)
    gRPCServer.Serve(lis)
}</code></pre>
<p>services/BlogService.go</p>
<pre><code>package services

import (
    "context"
    "demo/grpc/api"
    blogv1 "go.buf.build/grpc/go/cuiwei/blog/admin/v1"
)

var BlogService = &amp;blogService{}

//func NewBlogService() *blogService {
//  return &amp;blogService{}
//}

type blogService struct {
    *blogv1.UnimplementedBlogServiceServer //解决"missing mustEmbedUnimplementedBlogServiceServer method"的问题
}

//func (s *blogService) mustEmbedUnimplementedBlogServiceServer() {
//  //为了解决"missing mustEmbedUnimplementedBlogServiceServer method"的问题，但我测试时不好使
//}

func (s *blogService) AdminLogin(ctx context.Context, in *blogv1.AdminLoginRequest) (*blogv1.AdminLoginResponse, error) {
    return api.AdminLogin(ctx, in)
}

//这里只定义一个管理员的，其他的，像文章也都可以定义到这里，最终在api目录的实现再分开</code></pre>
<p>api/admin.go</p>
<pre><code>package api

import (
    "context"
    blogv1 "go.buf.build/grpc/go/cuiwei/blog/admin/v1"
)

func AdminLogin(ctx context.Context, in *blogv1.AdminLoginRequest) (*blogv1.AdminLoginResponse, error) {
    return &amp;blogv1.AdminLoginResponse{
        Id:       1,
        Username: "aa",
        Token:    "xx",
    }, nil
}</code></pre>
<p>看，是不是清晰多了～</p></div>]]></description>
            <guid isPermaLink="false">go 不一样的grpc入门示例</guid>
        </item>
        <item>
            <title><![CDATA[Android 对接 Google AdMob SDK]]></title>
            <description><![CDATA[<link rel="stylesheet" href="https://www.cuiwei.net/static/css/github-markdown.min.css?t=20260430" type="text/css" media="screen" /><div class="markdown-body"><p>首先，登陆 <a href="http://admob.google.com/，创建广告单元">http://admob.google.com/，创建广告单元</a></p>
<p><img src="https://www.cuiwei.net/data/upload/2022-10-29/166705189097670.jpg" alt="WX202210292156532x.jpg" /></p>
<p>无论你选择的是哪个类型的广告，第一步都需要初始化sdk，参考：
<a href="https://developers.google.com/admob/android/quick-start?hl=zh-CN#import_the_mobile_ads_sdk">Google 移动广告 SDK 指南</a></p>
<h2>开屏广告</h2>
<p><a href="https://developers.google.com/admob/android/app-open-ads?hl=zh-CN">https://developers.google.com/admob/android/app-open-ads?hl=zh-CN</a></p>
<p><a href="https://github.com/googleads/googleads-mobile-android-examples/tree/main/java/admob/AppOpenExample">https://github.com/googleads/googleads-mobile-android-examples/tree/main/java/admob/AppOpenExample</a></p>
<h2>自适应横幅广告</h2>
<p><a href="https://developers.google.com/admob/android/banner/adaptive?hl=zh-cn">https://developers.google.com/admob/android/banner/adaptive?hl=zh-cn</a></p>
<p><a href="https://github.com/googleads/googleads-mobile-android-examples/tree/main/java/admob/AdaptiveBannerExample">https://github.com/googleads/googleads-mobile-android-examples/tree/main/java/admob/AdaptiveBannerExample</a></p>
<h2>最后</h2>
<p>官方文档已经很详细了，我就不贴代码了</p>
<p>不同类型的广告，在GitHub上都对应提供了示例，很多代码都可以直接用</p></div>]]></description>
            <guid isPermaLink="false">Android 对接 Google AdMob SDK</guid>
        </item>
        <item>
            <title><![CDATA[Stack Overflow 离线版本的使用方法]]></title>
            <description><![CDATA[<link rel="stylesheet" href="https://www.cuiwei.net/static/css/github-markdown.min.css?t=20260430" type="text/css" media="screen" /><div class="markdown-body"><p><img src="https://www.cuiwei.net/data/upload/2022-10-28/166694849960635.jpg" alt="stackoverflowofflineblog2048x1075.png" /></p>
<p>近日，Stack Overflow 与 Kiwix 合作，推出一项名为「Overflow Offline」的新项目，以确保其数据集的最新版本可供需要的人轻松使用</p>
<h2>下载离线版本</h2>
<p>访问 <a href="https://library.kiwix.org/?lang=&amp;q=Stack">https://library.kiwix.org/?lang=&amp;q=Stack</a></p>
<p><img src="https://www.cuiwei.net/data/upload/2022-10-28/166694741860746.jpg" alt="WX202210281655502x.jpg" /></p>
<p>就是太大了，80G，让人望而却步</p>
<h2>下载Kiwix</h2>
<p><a href="https://www.kiwix.org/en/download/">https://www.kiwix.org/en/download/</a></p>
<p>全平台支持</p>
<h2>使用</h2>
<p><img src="https://www.cuiwei.net/data/upload/2022-10-28/166694761783358.jpg" alt="WX202210281659482x.png" /></p>
<h2>参考</h2>
<p><a href="https://stackoverflow.blog/2022/10/20/introducing-the-overflow-offline-project/">https://stackoverflow.blog/2022/10/20/introducing-the-overflow-offline-project/</a></p>
<p><a href="https://www.kiwix.org/en/look-at-us-we-took-stack-overflow-offline/">https://www.kiwix.org/en/look-at-us-we-took-stack-overflow-offline/</a></p>
<p><a href="https://github.com/openzim/sotoki">https://github.com/openzim/sotoki</a></p></div>]]></description>
            <guid isPermaLink="false">Stack Overflow 离线版本的使用方法</guid>
        </item>
        <item>
            <title><![CDATA[微服务链路追踪之Jaeger]]></title>
            <description><![CDATA[<link rel="stylesheet" href="https://www.cuiwei.net/static/css/github-markdown.min.css?t=20260430" type="text/css" media="screen" /><div class="markdown-body"><p>在一个微服务分布式架构的系统中，可能存在复杂的、深层的层层服务调用关系，大致如下图</p>
<p><img src="https://www.cuiwei.net/data/upload/2022-10-27/166686419194989.jpg" alt="20200104180741379.jpeg" /></p>
<p>如果某个环节出问题，在海量的日志中定位问题是很痛苦的，于是就有了调用链追踪系统，比较有名的是：Jaeger和Zipkin。本篇文章主要介绍Jaeger</p>
<p><img src="https://www.cuiwei.net/data/upload/2022-10-27/166686454355684.jpg" alt="cced6105da56535ce39ca800dec3f83e.png" /></p>
<h2>Jaeger的组成部分</h2>
<p><strong>Instrumentation SDKs：</strong> 集成到应用程序和框架中以捕获跟踪数据的库。 从历史上看，Jaeger 项目支持使用各种编程语言编写的自己的客户端库。 它们现在被弃用，取而代之的是 OpenTelemetry</p>
<p><strong>Jaeger Agent：</strong> Jaeger 代理是一个网络守护程序，用于侦听通过 UDP 从 Jaeger 客户端接收到的 span。 它收集成批的它们，然后将它们一起发送给收集器。 如果 SDK 被配置为将 span 直接发送到收集器，则不需要代理</p>
<p><strong>Jaeger Collector：</strong> Jaeger 收集器负责从 Jaeger 代理接收跟踪，执行验证和转换，并将它们保存到选定的存储后端</p>
<p><strong>Storage Backends：</strong> Jaeger 支持各种存储后端来存储跨度。 支持的存储后端有 In-Memory、Cassandra、Elasticsearch 和 Badger（用于单实例收集器部署）</p>
<p><strong>Jaeger Query：</strong> 这是一项服务，负责从 Jaeger 存储后端检索跟踪信息，并使其可供 Jaeger UI 访问。</p>
<p><strong>Jaeger UI：</strong> 一个 React 应用程序，可让您可视化跟踪并分析它们。 对于调试系统问题很有用。</p>
<p><strong>Ingester：</strong> 只有当我们使用 Kafka 作为收集器和存储后端之间的缓冲区时，ingester 才是相关的。 它负责从 Kafka 接收数据并将其摄取到存储后端。 更多信息可以在官方 Jaeger Tracing 文档中找到。</p>
<h2>在go-zero中使用</h2>
<p>在每个服务的配置文件中添加如下配置，其中<code>article-rpc</code>是服务名称</p>
<pre><code>Telemetry:
  Name: article-rpc
  Endpoint: http://localhost:14268/api/traces
  Sampler: 1.0
  Batcher: jaeger</code></pre>
<p>访问 Jaeger UI</p>
<p><a href="http://localhost:16686/">http://localhost:16686/</a></p>
<p><img src="https://www.cuiwei.net/data/upload/2022-10-27/166686546290329.jpg" alt="WX202210271808322x.jpg" /></p>
<p><img src="https://www.cuiwei.net/data/upload/2022-10-27/166686547965329.jpg" alt="WX202210271809192x.jpg" /></p>
<h2>参考</h2>
<p><a href="https://blog.csdn.net/zuiyijiangnan/article/details/103836060">https://blog.csdn.net/zuiyijiangnan/article/details/103836060</a></p></div>]]></description>
            <guid isPermaLink="false">微服务链路追踪之Jaeger</guid>
        </item>
        <item>
            <title><![CDATA[gRPC调试工具推荐]]></title>
            <description><![CDATA[<link rel="stylesheet" href="https://www.cuiwei.net/static/css/github-markdown.min.css?t=20260430" type="text/css" media="screen" /><div class="markdown-body"><p>在介绍工具之前先说一个情况，就是你用某个工具调试时，会发现，有的项目无需手动导入<code>.proto</code>文件，工具就能列出所有method，有些则不行。这是因为项目注册了反射，先看下怎么注册反射</p>
<p><img src="https://www.cuiwei.net/data/upload/2022-10-26/166676779643818.jpg" alt="carbon.png" /></p>
<p>就是这样，下面开始介绍工具</p>
<h2>Postman</h2>
<p>这个我觉得是最好的</p>
<p><img src="https://www.cuiwei.net/data/upload/2022-10-26/166676650693329.jpg" alt="WX202210261439072x.jpg" /></p>
<h2>gRPC UI</h2>
<p>gRPC的交互式Web用户界面，类似postman</p>
<p>安装</p>
<pre><code>go install github.com/fullstorydev/grpcui/cmd/grpcui@latest</code></pre>
<p>使用</p>
<pre><code># no TLS
cuiwei@weideMacBook-Pro ~ % grpcui -plaintext localhost:9087
gRPC Web UI available at http://127.0.0.1:61784/</code></pre>
<p><img src="https://www.cuiwei.net/data/upload/2022-10-26/166676441892548.jpg" alt="WX202210261405092x.jpg" /></p>
<h2>Evans</h2>
<p>Evans：更具表现力的通用gRPC客户端</p>
<p>安装</p>
<pre><code>go install github.com/ktr0731/evans@latest</code></pre>
<p><img src="https://www.cuiwei.net/data/upload/2022-10-26/166676981963865.jpg" alt="53423552e5ca88003a2411e99927fe7f3d5f867a.jpg" /></p>
<p>使用</p>
<pre><code>cuiwei@weideMacBook-Pro ~ % evans -r repl --host localhost -p 9087

  ______
 |  ____|
 | |__    __   __   __ _   _ __    ___
 |  __|   \ \ / /  / _. | | '_ \  / __|
 | |____   \ V /  | (_| | | | | | \__ \
 |______|   \_/    \__,_| |_| |_| |___/

 more expressive universal gRPC client

blog.v1.BlogService@localhost:9087&gt; show service
+-------------+---------------------+----------------------------+-----------------------------+
|   SERVICE   |         RPC         |        REQUEST TYPE        |        RESPONSE TYPE        |
+-------------+---------------------+----------------------------+-----------------------------+
| BlogService | AdminLogin          | AdminLoginRequest          | AdminLoginResponse          |
| BlogService | AdminFindpasswd     | AdminFindpasswdRequest     | AdminFindpasswdResponse     |
+-------------+---------------------+----------------------------+-----------------------------+

blog.v1.BlogService@localhost:9087&gt; call AdminDetail
admin_id (TYPE_INT64) =&gt; 5
{
  "avatar": "data/upload/avatar/5.jpg",
  "createTime": "1662222060",
  "email": "test@qq.com",
  "id": "5",
  "init": "1",
  "mobile": "18666666666",
  "nickname": "nick2..",
  "status": "1",
  "updateTime": "1664943345",
  "username": "admin22"
}</code></pre>
<h2>gRPCurl</h2>
<p>像cURL一样，但它属于gRPC：用于与gRPC服务器交互的命令行工具</p>
<p><a href="https://github.com/fullstorydev/grpcurl">https://github.com/fullstorydev/grpcurl</a></p>
<p>安装</p>
<pre><code>go install github.com/fullstorydev/grpcurl/cmd/grpcurl@latest</code></pre>
<p>使用</p>
<pre><code># no TLS
cuiwei@weideMacBook-Pro ~ % grpcurl -plaintext -d '{"to_aid": 5}' localhost:9087 blog.v1.BlogService/Admin2Detail
{
  "id": "5",
  "username": "admin22",
  "nickname": "nick2..",
  "email": "test@qq.com",
  "mobile": "18666666666",
  "avatar": "data/upload/avatar/5.jpg",
  "init": "1",
  "status": "1",
  "createTime": "1662222060",
  "updateTime": "1664943345"
}
</code></pre></div>]]></description>
            <guid isPermaLink="false">gRPC调试工具推荐</guid>
        </item>
        <item>
            <title><![CDATA[go-zero: not matching destination to scan]]></title>
            <description><![CDATA[<link rel="stylesheet" href="https://www.cuiwei.net/static/css/github-markdown.min.css?t=20260430" type="text/css" media="screen" /><div class="markdown-body"><p>在用go-zero写一个通过api调用grpc的一个功能</p>
<h2>问题还原</h2>
<p>错误的返回值</p>
<pre><code>func (m *customCwFlashModel) FindListByPage(ctx context.Context, page, max int64) (resp []*flash.AdminFlashDetailResponse, err error) {
    。。。
}</code></pre>
<p>这是一个查询flash列表的自定义模型，flash的定义有三处，分别是<code>model部分</code>、<code>api部分</code>和<code>grpc部分</code></p>
<h3>model部分</h3>
<p>这是go-zero通过sql自动生成的</p>
<pre><code>    CwFlash struct {
        Id         int64  `db:"id"`
        Title      string `db:"title"`
        Image      string `db:"image"`  // 图片
        Url        string `db:"url"`    // 链接
        Status     uint64 `db:"status"` // 是否显示，1是 0否
        Sort       uint64 `db:"sort"`   // 排序desc
        CreateTime uint64 `db:"create_time"`
        UpdateTime uint64 `db:"update_time"`
    }</code></pre>
<h3>api部分</h3>
<p>flash.api</p>
<pre><code>type (
    AdminFlashDetailRequest {
        FlashId int64 `path:"flash_id"`
    }

    AdminFlashDetailResponse {
        Id         int64    `json:"id"`
        Title      string `json:"title"`
        Image      string `json:"image"`
        Url        string `json:"url"`
        Status     int    `json:"status"`
        Sort       int    `json:"sort"`
        CreateTime int64  `json:"create_time"`
        UpdateTime int64  `json:"update_time"`
    }
)

type (
    AdminFlashListRequest {
        Page int64 `form:"page"`
        Max int64 `form:"max"`
    }

    AdminFlashListResponse {
        List []AdminFlashDetailResponse `json:"list"`
    }
)

service api {
    @handler FlashListHandler
    get /flash (AdminFlashListRequest) returns (AdminFlashListResponse)
}</code></pre>
<h3>grpc部分</h3>
<p>flash.proto</p>
<pre><code>message AdminFlashListRequest {
  int64 page = 1;
  int64 max = 2;
}

message AdminFlashListResponse {
  int64 count=1;
  repeated AdminFlashDetailResponse list=2;
}

message AdminFlashDetailResponse {
  int64 id=1;
  string title=2;
  string image=3;
  string url=4;
  int64  status=5;
  int64  sort=6;
  int64  create_time=7;
  int64  update_time=8;
}</code></pre>
<h2>问题解决</h2>
<p>经过一番折腾，最终发现返回值的类型用成了<code>grpc部分</code>定义的，其实应该使用<code>model部分</code>定义的，所以开头的代码应该修改为</p>
<pre><code>func (m *customCwFlashModel) FindListByPage(ctx context.Context, page, max int64) (resp []*CwFlash, err error) {
    。。。
}</code></pre></div>]]></description>
            <guid isPermaLink="false">go-zero: not matching destination to scan</guid>
        </item>
        <item>
            <title><![CDATA[go mutex的模式]]></title>
            <description><![CDATA[<link rel="stylesheet" href="https://www.cuiwei.net/static/css/github-markdown.min.css?t=20260430" type="text/css" media="screen" /><div class="markdown-body"><p>mutex 有<code>正常模式</code>和<code>饥饿模式</code></p>
<p>mutex是golang提供的基础并发原语，可以帮助我们处理多goruntine并发访问共享资源的问题。每个goruntine都要再获取到锁之后才能操作共享资源，完成操作释放锁，保证了共享资源的读写安全性。 但这种方式也可能带来一些问题：一些悲惨的goruntine一直获取不到锁，导致业务逻辑不能继续完整执行，这种问题被称为&quot;饥饿问题&quot;</p>
<h2>正常模式</h2>
<ul>
<li>当前的mutex只有一个goruntine来获取，那么没有竞争，直接返回。</li>
<li>新的goruntine进来，如果当前mutex已经被获取了，则该goruntine进入一个先入先出的waiter队列，在mutex被释放后，waiter按照先进先出的方式获取锁。该goruntine会处于自旋状态(不挂起，继续占有cpu)。</li>
<li>新的goruntine进来，mutex处于空闲状态，将参与竞争。新来的 goroutine 有先天的优势，它们正在 CPU 中运行，可能它们的数量还不少，所以，在高并发情况下，被唤醒的 waiter 可能比较悲剧地获取不到锁，这时，它会被插入到队列的前面。如果 waiter 获取不到锁的时间超过阈值 1 毫秒，那么，这个 Mutex 就进入到了饥饿模式。</li>
</ul>
<h2>饥饿模式</h2>
<p>在饥饿模式下，Mutex 的拥有者将直接把锁交给队列最前面的 waiter。新来的 goroutine 不会尝试获取锁，即使看起来锁没有被持有，它也不会去抢，也不会 spin（自旋），它会乖乖地加入到等待队列的尾部。 如果拥有 Mutex 的 waiter 发现下面两种情况的其中之一，它就会把这个 Mutex 转换成正常模式:</p>
<ul>
<li>此 waiter 已经是队列中的最后一个 waiter 了，没有其它的等待锁的 goroutine 了；</li>
<li>此 waiter 的等待时间小于 1 毫秒。</li>
</ul>
<h2>来源</h2>
<p><a href="https://www.cnblogs.com/tsxylhs/p/15042871.html">https://www.cnblogs.com/tsxylhs/p/15042871.html</a></p></div>]]></description>
            <guid isPermaLink="false">go mutex的模式</guid>
        </item>
        <item>
            <title><![CDATA[简述TCP三次握手和四次挥手]]></title>
            <description><![CDATA[<link rel="stylesheet" href="https://www.cuiwei.net/static/css/github-markdown.min.css?t=20260430" type="text/css" media="screen" /><div class="markdown-body"><h2>三次握手</h2>
<p>是指建立一个TCP连接时，需要客户端和服务端总共发送3个包以确认连接的建立。</p>
<p>可以想象两人用对讲机交谈。</p>
<pre><code>A：我准备好了你准备好了吗，收到请回答。
B：收到收到，我也准备好了，收到请回答。
A：收到收到</code></pre>
<h2>四步挥手</h2>
<pre><code>客户端：“兄弟，我这边没数据要传了，咱关闭连接吧。”
服务端：“收到，我看看我这边有木有数据了。”
服务端：“兄弟，我这边也没数据要传你了，咱可以关闭连接了。”
客户端：“好嘞。”</code></pre>
<p><img src="https://www.cuiwei.net/data/upload/2022-10-24/166660114835912.jpg" alt="699bc943363b4f258d57c029423aae69.png" /></p>
<h2>为什么建立连接是三次握手，而关闭连接却是四次挥手呢？</h2>
<p>这是因为服务端在LISTEN状态下，收到建立连接请求的SYN报文后，把ACK和SYN放在一个报文里发送给客户端。</p>
<p>而关闭连接时，当收到客户端的FIN报文时，仅仅表示客户端不再发送数据了但是还能接收数据，服务端也未必全部数据都发送给对方了，有可能还会发送一些数据给客户端后，再发送FIN报文给客户端来表示同意现在关闭连接，因此，己方ACK和FIN一般都会分开发送。</p>
<h2>来源</h2>
<p><a href="https://www.dandelioncloud.cn/article/details/1498121783957073921">https://www.dandelioncloud.cn/article/details/1498121783957073921</a></p></div>]]></description>
            <guid isPermaLink="false">简述TCP三次握手和四次挥手</guid>
        </item>
        <item>
            <title><![CDATA[gin 使用 Json Web Token(JWT)]]></title>
            <description><![CDATA[<link rel="stylesheet" href="https://www.cuiwei.net/static/css/github-markdown.min.css?t=20260430" type="text/css" media="screen" /><div class="markdown-body"><p>之前的token验证借助了redis，如果用jwt就不需要了</p>
<p>中间件</p>
<pre><code>cat middlewares/jwt.go

package middlewares

import (
    "enterprise-api/app/models"
    "enterprise-api/core"
    "github.com/gin-gonic/gin"
)

func JWTAuth(role string) gin.HandlerFunc {
    return func(c *gin.Context) {
        signToken := c.Request.Header.Get("Authorization")
        roleId, ok := c.Params.Get(role + "_id")
        if signToken == "" || !ok { //未传递user_id
            core.Error(c, 400, "无效的id")
            c.Abort()
            return
        }
        if role == "admin" || (role == "user" &amp;&amp; core.ToInt(roleId) &gt; 0) {
            myclaims, err := models.VerifyToken(signToken)
            if err != nil {
                core.Error(c, 401, "token校验失败")
                c.Abort()
                return
            }
            //c.Set("userid", myclaims.Id)
            if myclaims.Id == 0 || myclaims.Id != core.ToInt(roleId) {
                core.Error(c, 401, "token校验失败")
                c.Abort()
                return
            }
        }
        c.Next()
    }
}</code></pre>
<p>生成和校验token的方法</p>
<pre><code>cat models/jwt.go

package models

import (
    "enterprise-api/app/config"
    "github.com/golang-jwt/jwt"
)

type MyCustomClaims struct {
    Id       int    `json:"id"`
    Username string `json:"username"`
    jwt.StandardClaims
}

func CreateToken(claims MyCustomClaims) (string, error) {
    //使用HS256加密方式
    token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
    signToken, err := token.SignedString([]byte(config.GetConfig().JWTKey))
    if err != nil {
        return "", err
    }
    return signToken, nil

}

func VerifyToken(signToken string) (*MyCustomClaims, error) {
    var claims MyCustomClaims
    token, err := jwt.ParseWithClaims(signToken, &amp;claims, func(token *jwt.Token) (interface{}, error) {
        return []byte(config.GetConfig().JWTKey), nil
    })
    if token.Valid {
        return &amp;claims, nil
    } else {
        return nil, err
    }
}</code></pre>
<p>在登陆接口生成token</p>
<pre><code>claims := models.MyCustomClaims{
    Id:       admin.Id,
    Username: admin.Username,
    StandardClaims: jwt.StandardClaims{
        ExpiresAt: time.Now().Add(7 * 24 * time.Hour).Unix(), // 过期时间1星期
        Issuer:    admin.Username,                            // 签发人
    },
}
token, err := models.CreateToken(claims)</code></pre>
<p>在路由中使用</p>
<pre><code>...
userRouter.Use(middlewares.JWTAuth("user"))
{
...
}</code></pre>
<h2>参考</h2>
<p><a href="https://www.cnblogs.com/liuqingzheng/p/16154825.html">https://www.cnblogs.com/liuqingzheng/p/16154825.html</a></p></div>]]></description>
            <guid isPermaLink="false">gin 使用 Json Web Token(JWT)</guid>
        </item>
        <item>
            <title><![CDATA[grpc 实现统一格式的body响应]]></title>
            <description><![CDATA[<link rel="stylesheet" href="https://www.cuiwei.net/static/css/github-markdown.min.css?t=20260430" type="text/css" media="screen" /><div class="markdown-body"><p>我们写rest api时，统一响应大多是这样的</p>
<pre><code>{
 "err": 0,
 "msg": "参数错误",//有错误时非空
 "data" : {//没错误时才存在
        ...
 }
}</code></pre>
<p>在grpc项目中有些许差异</p>
<pre><code>func Admin2Resetpasswd(ctx context.Context, in *pb.Admin2ResetpasswdRequest) (*pb.Admin2ResetpasswdResponse, error) {
    if in.AdminId == in.ToAid {
        return nil, status.Error(codes.FailedPrecondition, "不能操作自己账号")
...</code></pre>
<p>grpc有十多种状态编码</p>
<pre><code>    `"OK"`: OK,
    `"CANCELLED"`:/* [sic] */ Canceled,
    `"UNKNOWN"`:             Unknown,
    `"INVALID_ARGUMENT"`:    InvalidArgument,
    `"DEADLINE_EXCEEDED"`:   DeadlineExceeded,
    `"NOT_FOUND"`:           NotFound,
    `"ALREADY_EXISTS"`:      AlreadyExists,
    `"PERMISSION_DENIED"`:   PermissionDenied,
    `"RESOURCE_EXHAUSTED"`:  ResourceExhausted,
    `"FAILED_PRECONDITION"`: FailedPrecondition,
    `"ABORTED"`:             Aborted,
    `"OUT_OF_RANGE"`:        OutOfRange,
    `"UNIMPLEMENTED"`:       Unimplemented,
    `"INTERNAL"`:            Internal,
    `"UNAVAILABLE"`:         Unavailable,
    `"DATA_LOSS"`:           DataLoss,
    `"UNAUTHENTICATED"`:     Unauthenticated,</code></pre>
<p>客户端解析</p>
<pre><code>ctx, _ := context.WithTimeout(context.Background(), time.Second)
r, err := c.SayHello(ctx, &amp;pb.HelloRequest{Name: "tony")})
if err != nil {
    errStatus := status.Convert(err)
    log.Printf("SayHello return error: code: %d, msg: %s\n", errStatus.Code(), errStatus.Message())
}
log.Printf("Greeting: %s", r.GetMessage())</code></pre>
<h2>参考</h2>
<p><a href="https://blog.csdn.net/bigwhite20xx/article/details/120499996">https://blog.csdn.net/bigwhite20xx/article/details/120499996</a></p></div>]]></description>
            <guid isPermaLink="false">grpc 实现统一格式的body响应</guid>
        </item>
        <item>
            <title><![CDATA[使用 buf 替代 protoc 生成和管理代码]]></title>
            <description><![CDATA[<link rel="stylesheet" href="https://www.cuiwei.net/static/css/github-markdown.min.css?t=20260430" type="text/css" media="screen" /><div class="markdown-body"><p>以go为例，下面是所需工具</p>
<table>
<thead>
<tr>
<th>工具</th>
<th>介绍</th>
<th>安装</th>
</tr>
</thead>
<tbody>
<tr>
<td><a href="https://github.com/protocolbuffers/protobuf">protobuf</a></td>
<td>protoc 可执行文件</td>
<td><a href="http://google.github.io/proto-lens/installing-protoc.html">Install</a></td>
</tr>
<tr>
<td><a href="https://github.com/golang/protobuf/tree/master/protoc-gen-go">protoc-gen-go</a></td>
<td>从 proto 文件，生成 .go 文件</td>
<td><a href="https://grpc.io/docs/languages/go/quickstart/">Install</a></td>
</tr>
<tr>
<td><a href="https://github.com/grpc/grpc-go">protoc-gen-go-grpc</a></td>
<td>从 proto 文件，生成 GRPC 相关的 .go 文件</td>
<td><a href="https://grpc.io/docs/languages/go/quickstart/">Install</a></td>
</tr>
<tr>
<td><a href="https://github.com/grpc-ecosystem/grpc-gateway">protoc-gen-grpc-gateway</a></td>
<td>从 proto 文件，生成 grpc-gateway 相关的 .go 文件</td>
<td><a href="https://github.com/grpc-ecosystem/grpc-gateway#installation">Install</a></td>
</tr>
<tr>
<td><a href="https://github.com/grpc-ecosystem/grpc-gateway">protoc-gen-openapiv2</a></td>
<td>从 proto 文件，生成 swagger 所需的json文件</td>
<td><a href="https://github.com/grpc-ecosystem/grpc-gateway#installation">Install</a></td>
</tr>
</tbody>
</table>
<h2>buf出现之前</h2>
<p>buf出现之前，我们只能使用protoc，配合一堆参数来生成代码</p>
<pre><code>cuiwei@weideMacBook-Pro protobuf % protoc --go_out=. --go-grpc_out=require_unimplemented_servers=false:. ./helloworld.proto 

cuiwei@weideMacBook-Pro protobuf % protoc -I . --grpc-gateway_out ../gen \
--grpc-gateway_opt logtostderr=true \
--grpc-gateway_opt paths=source_relative \
--grpc-gateway_opt generate_unbound_methods=true \
helloworld.proto
</code></pre>
<h2>buf 的使用</h2>
<h3>安装</h3>
<pre><code>//macOS
brew install bufbuild/buf/buf</code></pre>
<h3>登陆 Buf Schema Registry (BSR)</h3>
<p>在此之前，确保你已经创建了API令牌</p>
<pre><code>cuiwei@weideMacBook-Pro ent-serve % buf registry login

cuiwei@weideMacBook-Pro ~ % cat ~/.netrc
machine buf.build
  login cuiwei
  password 95b5be604e7f44eaaa48c031756da4c63547b8d7b21849e6b0f。。。
machine go.buf.build
  login cuiwei
  password 95b5be604e7f44eaaa48c031756da4c63547b8d7b21849e6b0f。。。</code></pre>
<h3>创建blog模块，或者在网站后台添加</h3>
<pre><code>cuiwei@weideMacBook-Pro ent-serve % buf beta registry repository create buf.build/cuiwei/blog --visibility public</code></pre>
<h3>推送模块</h3>
<pre><code>cuiwei@weideMacBook-Pro ent-serve % cd blogapis
cuiwei@weideMacBook-Pro ent-serve % ls blogapis 
blog            buf.md          buf.yaml
cuiwei@weideMacBook-Pro blogapis % buf push</code></pre>
<p>此模块已公开，欢迎使用：<a href="https://buf.build/cuiwei/blog">https://buf.build/cuiwei/blog</a></p>
<h3>创建buf.gen.yaml文件</h3>
<pre><code>version: v1
managed:
  enabled: true
  go_package_prefix:
    default: ent/gen/proto/go

plugins:
  - name: go
    out: gen/proto/go
    opt: paths=source_relative
  - name: go-grpc
    out: gen/proto/go
    opt:
      - paths=source_relative
      - require_unimplemented_servers=false</code></pre>
<h3>创建buf.work.yaml文件</h3>
<pre><code>version: v1
directories:
  - blogapis</code></pre>
<h3>生成代码</h3>
<p>确保已创建了buf.gen.yaml文件</p>
<pre><code>cuiwei@weideMacBook-Pro ent-serve % buf generate
cuiwei@weideMacBook-Pro ent-serve % tree gen 
gen
└── proto
    └── go
        └── admin
            └── v1
                ├── admin.pb.go
                ├── article.pb.go
                ├── error.pb.go
                ├── service.pb.go
                └── service_grpc.pb.go

4 directories, 5 files</code></pre>
<h3>远程生成</h3>
<p>首先，在您使用<code>go get</code>请求Go代码存根之前，不会生成Go代码存</p>
<pre><code>go get go.buf.build/grpc/go/cuiwei/blog</code></pre>
<p>其次，修改import</p>
<pre><code> import (
-    blogv1 "enterprise-api/gen/proto/go/blog/v1"
+    blogv1 "go.buf.build/grpc/go/cuiwei/blog/admin/v1"
 )</code></pre>
<p>如果需要更新，可以修改go.mod中的版本号</p>
<h2>参考</h2>
<p><a href="https://docs.buf.build/tour/introduction">https://docs.buf.build/tour/introduction</a></p>
<p><a href="https://docs.buf.build/tour/use-remote-generation">https://docs.buf.build/tour/use-remote-generation</a></p>
<p><a href="https://blog.csdn.net/pointgoal_io/article/details/120500504">https://blog.csdn.net/pointgoal_io/article/details/120500504</a></p></div>]]></description>
            <guid isPermaLink="false">使用 buf 替代 protoc 生成和管理代码</guid>
        </item>
        <item>
            <title><![CDATA[gRPC 同时提供 Restful API 接口]]></title>
            <description><![CDATA[<link rel="stylesheet" href="https://www.cuiwei.net/static/css/github-markdown.min.css?t=20260430" type="text/css" media="screen" /><div class="markdown-body"><p>gRPC支持很多语言，但是种种原因，要么对方的语言不支持，要么老项目无法改造，这时就需要提供Restful API 接口。如果重新写一套肯定是不划算的，这时候使用<code>gRPC-Gateway</code>，只需要在现有的gRPC项目做稍许修改就可以轻松实现Restful API 接口</p>
<p>引用etcd文档中的一段话</p>
<blockquote>
<p>为什么你应该考虑使用gRPC网关？
etcd v3使用gRPC作为其消息传递协议。etcd项目包括一个基于gRPC的Go客户端和一个命令行实用程序etcdctl，用于通过gRPC与etcd集群通信。对于不支持gRPC的语言，etcd提供JSON gRPC网关。此网关服务于RESTful代理，将HTTP/JSON请求转换为gRPC消息。</p>
</blockquote>
<h2>安装</h2>
<h3>编译</h3>
<p>首先项目启用了<code>Go Modules</code>,随便新建一个文件，比如<code>tmp.go</code>，内容如下</p>
<pre><code>// +build tools

package tools

import (
    _ "github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-grpc-gateway"
    _ "github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-openapiv2"
)</code></pre>
<p>然后运行<code>go mod tidy</code>来解析依赖。再通过运行以下命令来安装</p>
<pre><code>go install \
    github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-grpc-gateway \
    github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-openapiv2</code></pre>
<p>这时，你应该可以看到<code>$GOBIN</code>目录多了以下两个文件</p>
<pre><code>protoc-gen-grpc-gateway
protoc-gen-openapiv2</code></pre>
<p>这里默认你已经提前装好了<code>protoc-gen-go-grpc</code>和<code>protoc-gen-go</code></p>
<p>最后，上面创建的<code>tmp.go</code>文件就可以删除了</p>
<h3>下载二进制文件</h3>
<p><a href="https://github.com/grpc-ecosystem/grpc-gateway/releases">https://github.com/grpc-ecosystem/grpc-gateway/releases</a></p>
<h2>生成代码</h2>
<h3>protoc</h3>
<p>需要从
<a href="https://github.com/googleapis/googleapis/tree/master/google/api">googleapis存储库</a> 复制几个文件</p>
<pre><code>google/api/annotations.proto
google/api/field_behavior.proto
google/api/http.proto
google/api/httpbody.proto</code></pre>
<p>本地目录如下</p>
<pre><code>cuiwei@weideMacBook-Pro grpc-demo % tree protobuf 
protobuf
├── google
│   └── api
│       ├── annotations.proto
│       ├── field_behavior.proto
│       ├── http.proto
│       └── httpbody.proto
└── helloworld.proto</code></pre>
<p>然后修改<code>helloworld.proto</code></p>
<pre><code> syntax = "proto3";
 package your.service.v1;
 option go_package = "github.com/yourorg/yourprotos/gen/go/your/service/v1";
+
+import "google/api/annotations.proto";
+
 message StringMessage {
   string value = 1;
 }

 service YourService {
-  rpc Echo(StringMessage) returns (StringMessage) {}
+  rpc Echo(StringMessage) returns (StringMessage) {
+    option (google.api.http) = {
+      post: "/v1/example/echo"
+      body: "*"
+    };
+  }
 }</code></pre>
<p>最后生成代码</p>
<pre><code>cuiwei@weideMacBook-Pro protobuf % protoc --go_out=. ./helloworld.proto 

cuiwei@weideMacBook-Pro protobuf % protoc --go-grpc_out=require_unimplemented_servers=false:. ./helloworld.proto 

cuiwei@weideMacBook-Pro protobuf % protoc -I . --grpc-gateway_out ../gen \
--grpc-gateway_opt logtostderr=true \
--grpc-gateway_opt paths=source_relative \
--grpc-gateway_opt generate_unbound_methods=true \
helloworld.proto</code></pre>
<h3>buf</h3>
<p>安装请移步：<a href="https://www.cuiwei.net/p/1679512807">https://www.cuiwei.net/p/1679512807</a></p>
<p>gw_mapping.yaml</p>
<pre><code>type: google.api.Service
config_version: 3

http:
  rules:
    - selector: blog.v1.BlogService.AdminDetail
      get: "/v1/admin/{admin_id}"
    - selector: blog.v1.BlogService.AdminChange
      put: "/v1/admin/{admin_id}"
    - selector: blog.v1.BlogService.AdminChangepasswd
      put: "/v1/admin/{admin_id}/password"
    - selector: blog.v1.BlogService.AdminAvatar
      put: "/v1/admin/{admin_id}/avatar"
    - selector: blog.v1.BlogService.AdminLogout
      delete: "/v1/admin/{admin_id}/logout"
</code></pre>
<p>buf.gen.yaml</p>
<pre><code>version: v1
managed:
  enabled: true
  go_package_prefix:
    default: ent/gen/proto/go

plugins:
  - name: grpc-gateway
    out: gen/proto/go
    opt:
      - paths=source_relative
      - grpc_api_configuration=blogapis/blog/v1/gw_mapping.yaml
      - allow_repeated_fields_in_body=true
      - generate_unbound_methods=true
  - name: openapiv2
    out: gen/proto/go
    opt:
      - grpc_api_configuration=blogapis/blog/v1/gw_mapping.yaml
      - allow_repeated_fields_in_body=true</code></pre>
<h2>创建入口文件</h2>
<p>这里命名<code>httpserver.go</code></p>
<pre><code>package main

import (
  "context"
  "flag"
  "net/http"

  "github.com/golang/glog"
  "github.com/grpc-ecosystem/grpc-gateway/v2/runtime"
  "google.golang.org/grpc"
  "google.golang.org/grpc/credentials/insecure"

  gw "github.com/yourorg/yourrepo/proto/gen/go/your/service/v1/your_service"  // Update
)

var (
  grpcServerEndpoint = flag.String("grpc-server-endpoint",  "localhost:9090", "gRPC server endpoint")
)

func run() error {
  ctx := context.Background()
  ctx, cancel := context.WithCancel(ctx)
  defer cancel()

  // Register gRPC server endpoint
  // Note: Make sure the gRPC server is running properly and accessible
  mux := runtime.NewServeMux()
  opts := []grpc.DialOption{grpc.WithTransportCredentials(insecure.NewCredentials())}
  err := gw.RegisterYourServiceHandlerFromEndpoint(ctx, mux,  *grpcServerEndpoint, opts)
  if err != nil {
    return err
  }

  // Start HTTP server (and proxy calls to gRPC server endpoint)
  return http.ListenAndServe(":8081", mux)
}

func main() {
  flag.Parse()
  defer glog.Flush()

  if err := run(); err != nil {
    glog.Fatal(err)
  }
}</code></pre>
<p>以上代码需要根据实际情况做些修改</p>
<p>最后，执行<code>go run httpserver.go</code>，没意外的话服务就起来了，然后以post方式请求<code>http://localhost:8081/v1/example/echo</code>就能看到结果了</p>
<h2>参考</h2>
<p><a href="https://github.com/grpc-ecosystem/grpc-gateway">https://github.com/grpc-ecosystem/grpc-gateway</a></p>
<p><a href="https://grpc-ecosystem.github.io/grpc-gateway/docs/mapping/examples/">https://grpc-ecosystem.github.io/grpc-gateway/docs/mapping/examples/</a></p></div>]]></description>
            <guid isPermaLink="false">gRPC 同时提供 Restful API 接口</guid>
        </item>
        <item>
            <title><![CDATA[grpc TLS证书认证]]></title>
            <description><![CDATA[<link rel="stylesheet" href="https://www.cuiwei.net/static/css/github-markdown.min.css?t=20260430" type="text/css" media="screen" /><div class="markdown-body"><p>首先，申请证书，这个证书和nginx用的是一样的，具体步骤请移步：<a href="http://www.cuiwei.net/p/1135009574">RabbitMQ插件之MQTT</a></p>
<pre><code>ca.cer
www.cuiwei.net.key
www.cuiwei.net.pem</code></pre>
<h2>看代码</h2>
<h3>服务端</h3>
<pre><code>...
    // 从输入证书文件和密钥文件为服务端构造TLS凭证
    creds, err := credentials.NewServerTLSFromFile("./www.cuiwei.net.pem", "./www.cuiwei.net.key")
    if err != nil {
        log.Fatalf("Failed to generate credentials %v", err)
    }
    s := grpc.NewServer(grpc.Creds(creds))
...</code></pre>
<h3>客户端</h3>
<pre><code>...
    //从输入的证书文件中为客户端构造TLS凭证
    creds, err := credentials.NewClientTLSFromFile("./ca.cer", "www.cuiwei.net")
    if err != nil {
        log.Fatalf("Failed to create TLS credentials %v", err)
    }
    conn, err := grpc.Dial(":50051", grpc.WithTransportCredentials(creds))
...</code></pre>
<h2>参考</h2>
<p><a href="https://www.cnblogs.com/randysun/p/16273945.html">https://www.cnblogs.com/randysun/p/16273945.html</a></p></div>]]></description>
            <guid isPermaLink="false">grpc TLS证书认证</guid>
        </item>
        <item>
            <title><![CDATA[Golang大文件下载]]></title>
            <description><![CDATA[<link rel="stylesheet" href="https://www.cuiwei.net/static/css/github-markdown.min.css?t=20260430" type="text/css" media="screen" /><div class="markdown-body"><p>文件下载</p>
<pre><code>func DownloadFile() {
    url := "https://www.baidu.com/img/pc_79bff59263430e2e42693b50cf376490.png"
    resp, _ := http.Get(url)
    defer resp.Body.Close()

    data, err := io.ReadAll(resp.Body)
    if err != nil {
        panic(err)
    }

    _ = os.WriteFile("./pc_79bff59263430e2e42693b50cf376490.png", data, 0777)
}</code></pre>
<p>上面用到了<code>io.ReadAll</code>，如果是小文件没什么问题，大文件就不合适了，需要用<code>io.Copy</code></p>
<p>大文件下载</p>
<pre><code>func DownloadFile() {
    url := "https://download.geany.org/geany-1.38_osx-4.dmg"
    response, _ := http.Get(url)

    defer response.Body.Close()

    out, err := os.Create("./geany-1.38_osx-4.dmg")
    //wt := bufio.NewWriter(out)

    defer out.Close()

    n, err := io.Copy(out, response.Body) //使用固定的32K缓冲区，因此无论源数据多大，都只会占用32K内存空间。
    fmt.Println("write", n)
    if err != nil {
        panic(err)
    }
    //wt.Flush()
    fmt.Println("ok")
}</code></pre>
<h2>参考</h2>
<p><a href="https://www.cnblogs.com/smartrui/p/12110576.html">https://www.cnblogs.com/smartrui/p/12110576.html</a></p></div>]]></description>
            <guid isPermaLink="false">Golang大文件下载</guid>
        </item>
        <item>
            <title><![CDATA[Golang获取普通字符串和大文件的MD5]]></title>
            <description><![CDATA[<link rel="stylesheet" href="https://www.cuiwei.net/static/css/github-markdown.min.css?t=20260430" type="text/css" media="screen" /><div class="markdown-body"><p>先看一下md5的两种用法</p>
<pre><code>    hash := md5.New()
    b := []byte("test")
    hash.Write(b)
    fmt.Printf("%x %x\n", hash.Sum(nil), md5.Sum(b))</code></pre>
<p>普通字符串的md5</p>
<pre><code>func Md5(str string) string {
    data := []byte(str)
    return fmt.Sprintf("%x", md5.Sum(data))
}</code></pre>
<p>小文件的md5（不推荐）</p>
<pre><code>func Md5File(file string) string {
    cf, _ := os.Open(file)
    defer cf.Close()
    body, _ := io.ReadAll(cf)
    return fmt.Sprintf("%x", md5.Sum(body))
}</code></pre>
<p>文件的md5（支持大文件）</p>
<pre><code>func Md5File(file string) string {
    f, _ := os.Open(file)
    defer f.Close()
    md5hash := md5.New()
    if _, err := io.Copy(md5hash, f); err != nil {
        panic(err.Error())
    }
    return fmt.Sprintf("%x", md5hash.Sum(nil))
}</code></pre>
<h2>参考</h2>
<p><a href="https://www.codenong.com/js46d9c04c4ff4/">https://www.codenong.com/js46d9c04c4ff4/</a></p></div>]]></description>
            <guid isPermaLink="false">Golang获取普通字符串和大文件的MD5</guid>
        </item>
        <item>
            <title><![CDATA[Go 1.16 开始已废弃 io/ioutil 包，相关的功能被移到 io 包或 os 包]]></title>
            <description><![CDATA[<link rel="stylesheet" href="https://www.cuiwei.net/static/css/github-markdown.min.css?t=20260430" type="text/css" media="screen" /><div class="markdown-body"><p>废弃原因是：Io/ioutil，就像大多数名称中带有util的东西一样，事实证明是一个定义不明确且难以理解的东西集合。</p>
<h2>参考</h2>
<p><a href="https://blog.csdn.net/asd1126163471/article/details/112975739">https://blog.csdn.net/asd1126163471/article/details/112975739</a></p></div>]]></description>
            <guid isPermaLink="false">Go 1.16 开始已废弃 io/ioutil 包，相关的功能被移到 io 包或 os 包</guid>
        </item>
        <item>
            <title><![CDATA[Golang使用微软语音SDK实现文字转语音，Docker环境开箱即用]]></title>
            <description><![CDATA[<link rel="stylesheet" href="https://www.cuiwei.net/static/css/github-markdown.min.css?t=20260430" type="text/css" media="screen" /><div class="markdown-body"><p>微软语音是什么这里就不多说了，文本转语音我之前尝试过调用 REST API，但是太慢了，一句话的文本还行，几百字的文本就需要几十秒，甚至几分钟，就很容易失败。所以这次尝试一下Go版本的 SDK，吸引我的点是它快，并且可以在服务端运行</p>
<h2>配置开发环境</h2>
<p>按照官方文档所说需要先配置语音SDK，而这个SDK只支持Linux，所以我选择了Go的官方Docker镜像 <a href="https://hub.docker.com/_/golang">Golang:1.19</a>（此镜像是基于Debian），然后配合VS Code进行容器内开发</p>
<p><img src="https://www.cuiwei.net/data/upload/2022-10-13/166563658651898.jpg" alt="WX202210131247392x.jpg" /></p>
<p>如上，我打开了VS Code，按照官方文档的步骤</p>
<pre><code>apt-get update

apt-get install build-essential libssl-dev libasound2 wget

export SPEECHSDK_ROOT="$HOME/speechsdk"
mkdir -p "$SPEECHSDK_ROOT"

wget -O SpeechSDK-Linux.tar.gz https://aka.ms/csspeech/linuxbinary
tar --strip 1 -xzf SpeechSDK-Linux.tar.gz -C "$SPEECHSDK_ROOT"
ls -l "$SPEECHSDK_ROOT"

export CGO_CFLAGS="-I$SPEECHSDK_ROOT/include/c_api"

export CGO_LDFLAGS="-L$SPEECHSDK_ROOT/lib/x64 -lMicrosoft.CognitiveServices.Speech.core"

export LD_LIBRARY_PATH="$SPEECHSDK_ROOT/lib/x64:$LD_LIBRARY_PATH"
</code></pre>
<p>如上，导入了几个环境变量，而且是用export，export命令的效果仅限于当前登录终端，也就是说你关闭VS Code，再重新打开就失效了，需要重新导入。这不是我想要的，所以就需要重新构建镜像，直接看Dockerfile</p>
<pre><code>FROM golang:1.19
RUN go env -w GO111MODULE=on
RUN go env -w GOPROXY=https://goproxy.cn,direct

#speechsdk start
ENV SPEECHSDK_ROOT="$HOME/speechsdk"
RUN apt-get update &amp;&amp; apt-get install -y build-essential libssl-dev libasound2 wget \
    &amp;&amp; mkdir -p "$SPEECHSDK_ROOT" \
    &amp;&amp; wget -O SpeechSDK-Linux.tar.gz https://aka.ms/csspeech/linuxbinary \
    &amp;&amp; tar --strip 1 -xzf SpeechSDK-Linux.tar.gz -C "$SPEECHSDK_ROOT" \
    &amp;&amp; ls -l "$SPEECHSDK_ROOT" \
    &amp;&amp; rm SpeechSDK-Linux.tar.gz
ENV CGO_CFLAGS="-I$SPEECHSDK_ROOT/include/c_api"
ENV CGO_LDFLAGS="-L$SPEECHSDK_ROOT/lib/x64 -lMicrosoft.CognitiveServices.Speech.core"
ENV LD_LIBRARY_PATH="$SPEECHSDK_ROOT/lib/x64:$LD_LIBRARY_PATH"
#speechsdk end</code></pre>
<p>然后，构建</p>
<pre><code>docker build -t chudaozhe/golang:1.19-speechsdk .</code></pre>
<blockquote>
<p>此镜像已推到Docker Hub，大家可以直接使用</p>
</blockquote>
<h3>镜像的使用</h3>
<p>这里简单介绍一下如何使用这个镜像</p>
<p>首先，在你的项目根目录创建一个<code>.docker</code>目录，然后在里面添加一个<code>docker-compose.yaml</code>文件，内容如下</p>
<pre><code>cuiwei@weideMacBook-Pro cobra-demo % cat .docker/docker-compose.yaml 
version: '3'

networks:
  go-network:

services:
  docker-go:
    image: chudaozhe/golang:1.19-speechsdk
    tty: true
#    command: /bin/bash -c "go env -w GO111MODULE=on &amp;&amp; go env -w GOPROXY=https://goproxy.cn,direct &amp;&amp; bash"
    ports:
      - 8000:8000
    networks:
      - go-network</code></pre>
<p>然后，依次找到<code>Docker Desktop</code> -&gt; <code>Dev Environments</code> -&gt; <code>Create</code>，打开你的项目，这里假设项目目录为<code>cobra-demo</code></p>
<p><img src="https://www.cuiwei.net/data/upload/2022-10-13/166563918331660.jpg" alt="WX202210131330162x.jpg" /></p>
<p>如上图，继续下一步你就会看到<code>OPEN IN VSCODE</code>的按钮</p>
<h2>测试一下</h2>
<p>直接使用官方的demo：文本转语音输出到扬声器</p>
<p><img src="https://www.cuiwei.net/data/upload/2022-10-13/166564235331970.jpg" alt="WX202210131415062x.jpg" /></p>
<p>如图，先替换一下key和region，然后执行</p>
<p><img src="https://www.cuiwei.net/data/upload/2022-10-13/166564232784136.jpg" alt="WX202210131422342x.png" /></p>
<p>可以看到输出成功了，但你可能听不到声音；没关系，语音输出到扬声器不是我的目的，输出到文件才是。</p>
<p>好了，至此这篇文章就先结束了，我要继续研究<code>将语音合成到文件</code>了，再见，再见👋👋</p>
<h2>参考</h2>
<p><a href="https://learn.microsoft.com/zh-cn/azure/cognitive-services/speech-service/quickstarts/setup-platform">https://learn.microsoft.com/zh-cn/azure/cognitive-services/speech-service/quickstarts/setup-platform</a></p>
<p><a href="https://learn.microsoft.com/zh-cn/azure/cognitive-services/speech-service/how-to-speech-synthesis">https://learn.microsoft.com/zh-cn/azure/cognitive-services/speech-service/how-to-speech-synthesis</a></p>
<p><a href="https://github.com/microsoft/cognitive-services-speech-sdk-go/tree/master/samples">https://github.com/microsoft/cognitive-services-speech-sdk-go/tree/master/samples</a></p></div>]]></description>
            <guid isPermaLink="false">Golang使用微软语音SDK实现文字转语音，Docker环境开箱即用</guid>
        </item>
        <item>
            <title><![CDATA[使用docker-compose快速部署etcd]]></title>
            <description><![CDATA[<link rel="stylesheet" href="https://www.cuiwei.net/static/css/github-markdown.min.css?t=20260430" type="text/css" media="screen" /><div class="markdown-body"><p>docker-compose.yml</p>
<pre><code>version: '3'

networks:
  web-network:

services:
  docker-etcd:
    hostname: etcd
    image: bitnami/etcd:3.5.5
    volumes:
      - "./etcd/data:/bitnami/etcd/data"
    environment:
      - ALLOW_NONE_AUTHENTICATION=yes
      - ETCD_ADVERTISE_CLIENT_URLS=http://0.0.0.0:2379
    ports:
      - "2379:2379"
      - "2380:2380"
    networks:
      - web-network

  docker-etcdkeeper:
    hostname: etcdkeeper
    image: evildecay/etcdkeeper:v0.7.6
    ports:
      - "8099:8080"
    networks:
      - web-network</code></pre>
<h2>web管理</h2>
<pre><code>//etcdkeeper
http://localhost:8099</code></pre>
<p><img src="https://www.cuiwei.net/data/upload/2022-10-13/166563155244811.jpg" alt="WX202209172351132x.png" /></p></div>]]></description>
            <guid isPermaLink="false">使用docker-compose快速部署etcd</guid>
        </item>
        <item>
            <title><![CDATA[go内置的性能分析工具 - pprof]]></title>
            <description><![CDATA[<link rel="stylesheet" href="https://www.cuiwei.net/static/css/github-markdown.min.css?t=20260430" type="text/css" media="screen" /><div class="markdown-body"><h2>获取数据</h2>
<h3>控制台程序 - 使用runtime/pprof库</h3>
<p>如下，一个基于 cobra 的程序</p>
<pre><code>package main

import (
    "cobra-demo/cmd"
    "fmt"
    "os"
    "runtime/pprof"
)

func main() {
    //start
    cpuProfile, err := os.Create("./pprof/cpu_profile")
    if err != nil {
        fmt.Printf("创建文件失败:%s", err.Error())
        return
    }
    defer cpuProfile.Close()

    memProfile, err := os.Create("./pprof/mem_profile")
    if err != nil {
        fmt.Printf("创建文件失败:%s", err.Error())
        return
    }
    defer memProfile.Close()
    //采集CPU信息
    pprof.StartCPUProfile(cpuProfile)
    defer pprof.StopCPUProfile()

    //采集内存信息
    pprof.WriteHeapProfile(memProfile)
    //end
    cmd.Execute()
}</code></pre>
<p>执行上面的程序会得到两个数据文件<code>./pprof/cpu_profile</code>和<code>./pprof/mem_profile</code></p>
<h3>Web应用 - 使用net/http/pprof库</h3>
<p>这里介绍两种：原生net/http框架和Gin框架</p>
<h4>原生net/http框架</h4>
<pre><code>package main

import (
    "fmt"
    "net/http"
    _ "net/http/pprof"
)

func HelloWorld(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintln(w, "hello world")
}

func main() {
    http.HandleFunc("/", HelloWorld)

    err := http.ListenAndServe(":8080", nil)
    if err != nil {
        fmt.Println(err)
    }
}</code></pre>
<h4>Gin框架</h4>
<pre><code>package main

import (
    "enterprise-api/app/routes"
    "github.com/gin-contrib/pprof"
)

func main() {
    //注册路由
    router := routes.InitRouter()
    pprof.Register(router) //就这一句
    router.Run(":8080")
}</code></pre>
<p>如上，无论你是用的<code>原生net/http框架</code>，还是用的<code>Gin框架</code>，现在都可以访问 <code>http://localhost:8080/debug/pprof/</code></p>
<pre><code>http://localhost:8080/debug/pprof/allocs
http://localhost:8080/debug/pprof/block
http://localhost:8080/debug/pprof/cmdline
http://localhost:8080/debug/pprof/heap
http://localhost:8080/debug/pprof/mutex
http://localhost:8080/debug/pprof/profile
http://localhost:8080/debug/pprof/threadcreate
http://localhost:8080/debug/pprof/trace</code></pre>
<p>我们选一个</p>
<pre><code>cuiwei@weideMacBook-Pro enterprise-api % go tool pprof http://localhost:8080/debug/pprof/allocs
Fetching profile over HTTP from http://localhost:8080/debug/pprof/allocs
Saved profile in /Users/cuiwei/pprof/pprof.alloc_objects.alloc_space.inuse_objects.inuse_space.002.pb.gz
Type: alloc_space
Time: Oct 12, 2022 at 11:45pm (CST)
Entering interactive mode (type "help" for commands, "o" for options)
(pprof) 

#这里可以进行一些交互，比如执行 top</code></pre>
<p>如上我们得到了新的数据文件<code>~/pprof/pprof.alloc_objects.alloc_space.inuse_objects.inuse_space.002.pb.gz</code></p>
<h2>使用可视化界面</h2>
<p>必须安装<code>graphviz</code></p>
<pre><code>#macOS
brew install graphviz</code></pre>
<p>选择一个上面得到的数据文件，比如：<code>pprof.alloc_objects.alloc_space.inuse_objects.inuse_space.002.pb.gz</code></p>
<pre><code>cuiwei@weideMacBook-Pro enterprise-api % go tool pprof -http=localhost:8081 ~/pprof/pprof.alloc_objects.alloc_space.inuse_objects.inuse_space.002.pb.gz
Serving web UI on http://localhost:8081</code></pre>
<p><a href="http://localhost:8081/ui/">http://localhost:8081/ui/</a></p>
<p><img src="https://www.cuiwei.net/data/upload/2022-10-12/166559029856000.jpg" alt="WX202210122356452x.jpg" /></p>
<h2>参考</h2>
<p><a href="https://blog.csdn.net/s2603898260/article/details/124160531">https://blog.csdn.net/s2603898260/article/details/124160531</a></p></div>]]></description>
            <guid isPermaLink="false">go内置的性能分析工具 - pprof</guid>
        </item>
        <item>
            <title><![CDATA[golang 中实现并发安全的map]]></title>
            <description><![CDATA[<link rel="stylesheet" href="https://www.cuiwei.net/static/css/github-markdown.min.css?t=20260430" type="text/css" media="screen" /><div class="markdown-body"><p>Go语言中的 map 在并发情况下，只读是线程安全的，同时读写是线程不安全的。</p>
<h2>问题还原</h2>
<p>下面来看下并发情况下读写 map 时会出现的问题</p>
<pre><code>func main() {
    m := make(map[int]int)
    go func() {
        // 不停地对map进行写入
        for {
            m[1] = 1
        }
    }()

    go func() {
        // 不停地对map进行读取
        for {
            _ = m[1]
        }
    }()
}</code></pre>
<p>会报错</p>
<pre><code>fatal error: concurrent map read and map write</code></pre>
<h2>实现方案1 - 加锁</h2>
<pre><code>// 加锁的map
type Map struct {
    m map[int]int
    sync.RWMutex
}

func (this *Map) Get(key int) int {
    this.RLock()
    defer this.RUnlock()

    v := this.m[key]
    return v
}

func (this *Map) Set(k, v int) {
    this.Lock()
    defer this.Unlock()

    this.m[k] = v
}

func main() {
    newMap := &amp;Map{m: make(map[int]int)}
    for i := 0; i &lt; 1000; i++ {
        go newMap.Set(i, i)
    }
    for i := 0; i &lt; 1000; i++ {
        go fmt.Println(i, newMap.Get(i))
    }

    time.Sleep(time.Second)
}</code></pre>
<h2>实现方案2 - sync.Map</h2>
<pre><code>type Map struct
func (m *Map) Store(key, value interface{})
func (m *Map) Load(key interface{}) (value interface{}, ok bool)
func (m *Map) Range(f func(key, value interface{}) bool)
func (m *Map) Delete(key interface{})</code></pre>
<p>sync.Map 有以下特性：</p>
<ul>
<li>无须初始化，直接声明即可。</li>
<li>sync.Map 不能使用 map 的方式进行取值和设置等操作，而是使用 sync.Map 的方法进行调用，Store 表示存储，Load 表示获取，Delete 表示删除。</li>
<li>使用 Range 配合一个回调函数进行遍历操作，通过回调函数返回内部遍历出来的值，Range 参数中回调函数的返回值在需要继续迭代遍历时，返回 true，终止迭代遍历时，返回 false。</li>
<li>LoadOrStore：参数是一对key：value，如果该key存在且没有被标记删除则返回原先的value（不更新）和true；不存在则store，返回该value 和false</li>
</ul>
<pre><code>func main() {
    var m sync.Map
    m.Store("bb", 22)
    m.Store("cc", 33)
    m.Store("aa", 11)
    m.Store("dd", 33)
    m.Store("ee", 11)

    //Load 方法，获得value
    if v, ok := m.Load("cc"); ok {
        fmt.Printf("Load 方法，获得value   %v: %v\n", v, ok)
    }
    m.Delete("cc")

    //LoadOrStore方法，获取或者保存
    //就是如果key还在，那么就保持原来并返回原来的值，如果key不存在就存储
    if vv, ok := m.LoadOrStore("bb", 22); ok {
        fmt.Println(vv)
    } else {
        fmt.Printf("LoadOrStore 方法，获得value   %v: %v\n", vv, ok)
    }

    //遍历该map
    m.Range(func(key, value interface{}) bool {
        fmt.Printf("[%v]=[%v]\n", key, value)
        return true
    })
}</code></pre>
<h2>实现方案3 - 分段锁</h2>
<p>未完，待续。。。</p>
<h2>参考</h2>
<p><a href="https://blog.csdn.net/zhizhengguan/article/details/107879969">https://blog.csdn.net/zhizhengguan/article/details/107879969</a></p>
<p><a href="https://blog.csdn.net/lanyang123456/article/details/114178724">https://blog.csdn.net/lanyang123456/article/details/114178724</a></p></div>]]></description>
            <guid isPermaLink="false">golang 中实现并发安全的map</guid>
        </item>
        <item>
            <title><![CDATA[使用docker-compose快速部署InfluxDB 2.4]]></title>
            <description><![CDATA[<link rel="stylesheet" href="https://www.cuiwei.net/static/css/github-markdown.min.css?t=20260430" type="text/css" media="screen" /><div class="markdown-body"><p>docker-compose.yml</p>
<pre><code>version: '3'

networks:
  web-network:

services:
  docker-influxdb:
    image: influxdb:2.4
    container_name: influxdb
    restart: always
    ports:
      - "8086:8086" #HTTP UI and API port
    environment:
      DOCKER_INFLUXDB_INIT_MODE: "setup"
      DOCKER_INFLUXDB_INIT_USERNAME: "root" #创建管理员用户
      DOCKER_INFLUXDB_INIT_PASSWORD: "a123456a" #创建管理员密码，太简单会报错
      DOCKER_INFLUXDB_INIT_ORG: "chudaozhe" #组织名称
      DOCKER_INFLUXDB_INIT_BUCKET: "my-bucket"
    volumes:
      - "./influxdb/data:/var/lib/influxdb2"
      - "./influxdb/config:/etc/influxdb2"
    networks:
      - web-network

  docker-chronograf:
    container_name: chronograf
    image: chronograf:1.10
    restart: always
    ports:
      - "8888:8888"
    environment:
      INFLUXDB_URL: "http://influxdb:8086"
      INFLUXDB_USERNAME: "root"
      INFLUXDB_PASSWORD: "a123456a"
      INFLUXDB_ORG: "chudaozhe"
      INFLUXDB_TOKEN: "7p3ogq9FlWxF3ygez29049KfJRotlezkcAQ1GnvWrADN3ZaqiZStPLKlJLVcUT631LoWCI9R9DgZvzWoQ4xX0A=="
    volumes:
      - ./chronograf:/var/lib/chronograf
    networks:
      - web-network</code></pre>
<p><code>INFLUXDB_TOKEN</code>获取</p>
<pre><code>cuiwei@weideMacBook-Pro docker-influxdb % cat ./influxdb/config/influx-configs 
[default]
  url = "http://localhost:8086"
  token = "7p3ogq9FlWxF3ygez29049KfJRotlezkcAQ1GnvWrADN3ZaqiZStPLKlJLVcUT631LoWCI9R9DgZvzWoQ4xX0A=="
  org = "chudaozhe"
  active = true</code></pre>
<h2>web管理</h2>
<pre><code>//自带ui
http://localhost:8086

//chronograf
http://localhost:8888</code></pre>
<h2>1.x和2.x的区别</h2>
<pre><code>InfluxDB 1一般配合Grafana使用，2自带ui

1.x 版本使用 influxQL 查询语言
2.x 和 1.8+（beta） 使用 flux 查询语法
相比V1 移除了database 和 RP，增加了bucket。

V2具有以下几个概念：
timestamp、field key、field value、field set、tag key、tag value、tag set、measurement、series、point、bucket、bucket schema、organization

新增的概念：
bucket：所有 InfluxDB 数据都存储在一个存储桶中。一个桶结合了数据库的概念和存储周期（时间每个数据点仍然存在持续时间）。一个桶属于一个组织
bucket schema：具有明确的schema-type的存储桶需要为每个度量指定显式架构。测量包含标签、字段和时间戳。显式模式限制了可以写入该度量的数据的形状。
organization：InfluxDB组织是一组用户的工作区。所有仪表板、任务、存储桶和用户都属于一个组织。</code></pre>
<h2>参考</h2>
<p><a href="https://hub.docker.com/_/influxdb">https://hub.docker.com/_/influxdb</a></p>
<p><a href="https://docs.influxdata.com/chronograf/v1.10/administration/config-options/#--influxdb-url">https://docs.influxdata.com/chronograf/v1.10/administration/config-options/#--influxdb-url</a></p></div>]]></description>
            <guid isPermaLink="false">使用docker-compose快速部署InfluxDB 2.4</guid>
        </item>
        <item>
            <title><![CDATA[nginx 安装rtmp模块实现推流服务器]]></title>
            <description><![CDATA[<link rel="stylesheet" href="https://www.cuiwei.net/static/css/github-markdown.min.css?t=20260430" type="text/css" media="screen" /><div class="markdown-body"><h2>安装模块</h2>
<p>请移步 <a href="https://www.cuiwei.net/p/1011052604">https://www.cuiwei.net/p/1011052604</a></p>
<p>配置文档
<a href="https://github.com/arut/nginx-rtmp-module/wiki/Directives">https://github.com/arut/nginx-rtmp-module/wiki/Directives</a></p>
<pre><code>vi /etc/nginx/nginx.conf

load_module modules/ngx_rtmp_module.so;
events {
    worker_connections  1024;
}
rtmp {
    server {
        listen 1935;
        chunk_size 4096;

        application rtmp-live {
            live on;
        }
    }
}

http {
...
}</code></pre>
<h2>推流测试</h2>
<h3>ffmpeg 命令行推流</h3>
<pre><code>ffmpeg -re -stream_loop -1 -i ./test.flv -c copy -f flv rtmp://127.0.0.1:1935/rtmp-live/test</code></pre>
<h3>OBS推流</h3>
<p><img src="https://www.cuiwei.net/data/upload/2022-10-02/166471780642183.jpg" alt="WX202210022132542x.png" /></p>
<h2>拉流测试</h2>
<h3>VLC</h3>
<p>容易失败，失败就多试几次</p>
<p><img src="https://www.cuiwei.net/data/upload/2022-10-02/166471779549759.jpg" alt="WX202210022132012x.png" /></p>
<h3>hls.js</h3>
<pre><code>&lt;!DOCTYPE html&gt;
&lt;html lang="en"&gt;
&lt;head&gt;
    &lt;meta charset="UTF-8"&gt;
    &lt;title&gt;Title&lt;/title&gt;
    &lt;script src="https://cdn.jsdelivr.net/hls.js/latest/hls.min.js"&gt;&lt;/script&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;video id="video" controls autoplay&gt;&lt;/video&gt;
&lt;script&gt;
    if(Hls.isSupported()) {
        var video = document.getElementById('video');
        var hls = new Hls();
        hls.loadSource('https://pull-hls-f96.douyincdn.com/stage/stream-399947309713982122_or4.m3u8');
        hls.attachMedia(video);
        hls.on(Hls.Events.MANIFEST_PARSED,function() {
            video.play();
        });
    }
&lt;/script&gt;
&lt;/body&gt;
&lt;/html&gt;</code></pre></div>]]></description>
            <guid isPermaLink="false">nginx 安装rtmp模块实现推流服务器</guid>
        </item>
        <item>
            <title><![CDATA[nginx 安装第三方模块]]></title>
            <description><![CDATA[<link rel="stylesheet" href="https://www.cuiwei.net/static/css/github-markdown.min.css?t=20260430" type="text/css" media="screen" /><div class="markdown-body"><p>下面以rtmp模块为例 <a href="https://github.com/arut/nginx-rtmp-module">https://github.com/arut/nginx-rtmp-module</a></p>
<h2>普通方式</h2>
<h3>静态模块</h3>
<pre><code>./configure --add-module=/path/to/nginx-rtmp-module

make

//make编译，编译好的程序在objs文件夹下面，这时候不要执行make install 避免新编译的程序有问题，又覆盖了原有的程序

//备份原来的nginx可执行文件
mv /usr/local/nginx/sbin/nginx /usr/local/nginx/sbin/nginx.old

//把编译好的Nginx程序替换到原来的目录里
cp objs/nginx /usr/local/nginx/sbin/

//升级检测
make upgrade

//如果显示新模块信息则安装成功
nginx -V</code></pre>
<h3>动态模块</h3>
<p>NGINX 1.9.11开始增加加载动态模块支持，从此不再需要替换nginx文件即可增加第三方扩展。</p>
<pre><code>./configure --add-dynamic-module=/path/to/nginx-rtmp-module
make
make install</code></pre>
<p>加载</p>
<pre><code>vi /etc/nginx/nginx.conf
...
load_module modules/ngx_rtmp_module.so;
events {
    worker_connections  1024;
}
...</code></pre>
<h2>docker方式</h2>
<p>这种方式会得到一个新的nginx镜像，如果官方镜像无法满足你的使用，就可以用这种方式构建自己的镜像</p>
<pre><code>//https://github.com/nginxinc/docker-nginx/tree/master/modules
cd modules
docker pull nginx:mainline
docker build --build-arg ENABLED_MODULES="rtmp" -t nginx-with-rtmp .</code></pre>
<p><a href="https://github.com/nginxinc/docker-nginx/issues/332">https://github.com/nginxinc/docker-nginx/issues/332</a></p>
<p><a href="https://github.com/nginxinc/docker-nginx/tree/master/modules">https://github.com/nginxinc/docker-nginx/tree/master/modules</a></p></div>]]></description>
            <guid isPermaLink="false">nginx 安装第三方模块</guid>
        </item>
        <item>
            <title><![CDATA[go-zero 创建api/rpc项目]]></title>
            <description><![CDATA[<link rel="stylesheet" href="https://www.cuiwei.net/static/css/github-markdown.min.css?t=20260430" type="text/css" media="screen" /><div class="markdown-body"><p>1.创建目录<code>ent-api</code>，然后用goland打开
设置代理</p>
<pre><code>GOPROXY=https://goproxy.cn</code></pre>
<p><img src="https://www.cuiwei.net/data/upload/2022-09-21/166373952954630.jpg" alt="WX202209211349032x.png" /></p>
<p>2.初始化项目</p>
<pre><code>cuiwei@weideMacBook-Pro ent-api % go mod init ent-api
go: creating new go.mod: module ent-api</code></pre>
<ol start="3">
<li>创建项目（这里咱不用这种方式，咱通过 api文件创建）
<pre><code>
cuiwei@weideMacBook-Pro ent-api % goctl api new ent</code></pre></li>
</ol>
<p>//rpc
cuiwei@weideMacBook-Pro ent-api % goctl rpc new article</p>
<pre><code>
4. 编写api文件</code></pre>
<p>api
├── ent.api
└── flash.api</p>
<pre><code>
5. 通过api文件生成项目（当然也可以通过ide插件生成）</code></pre>
<p>goctl api go -api api/ent.api -dir . -style goZero</p>
<p>//rpc
cd apps/article/rpc
goctl rpc protoc article.proto --go_out=. --go-grpc_out=. --zrpc_out=.</p>
<pre><code>
6. 下载依赖项</code></pre>
<p>cuiwei@weideMacBook-Pro ent-api % go mod tidy</p>
<pre><code>
7. 生成model（当然也可以通过ide插件生成）</code></pre>
<p>cuiwei@weideMacBook-Pro ent-api % goctl model mysql ddl -src=&quot;./model/*.sql&quot; -dir=&quot;./model&quot; -c -style goZero</p>
<pre><code></code></pre></div>]]></description>
            <guid isPermaLink="false">go-zero 创建api/rpc项目</guid>
        </item>
        <item>
            <title><![CDATA[静态HTML和CSS网站生成器 - Hugo]]></title>
            <description><![CDATA[<link rel="stylesheet" href="https://www.cuiwei.net/static/css/github-markdown.min.css?t=20260430" type="text/css" media="screen" /><div class="markdown-body"><p>Hugo是用Go编写的静态HTML和CSS网站生成器。它针对速度、易用性和可配置性进行了优化。Hugo拿一个包含内容和模板的目录，并将其渲染成一个完整的HTML网站。</p>
<p>Hugo依赖带有前置内容的Markdown文件作为元数据，您可以从任何目录运行Hugo。这适用于共享主机和其他没有特权帐户的系统。</p>
<p>Hugo在几分之一秒内呈现了一个中等大小的典型网站。一个好的经验法则是，每段内容在大约1毫秒内呈现。</p>
<p>Hugo旨在适用于任何类型的网站，包括博客、tumbles和文档。</p>
<h2>步骤</h2>
<pre><code>//安装
brew install hugo
//查看版本
hugo version
//新建站点
hugo new site quickstart
//进到站点根目录
cd quickstart
//git初始化（方便下面安装主题
git init
//添加git子项目，并clone到themes/ananke目录
git submodule add https://github.com/theNewDynamic/gohugo-theme-ananke.git themes/ananke
//将主题名称添加到配置文件
echo theme = \"ananke\" &gt;&gt; config.toml\n
//添加一篇文章
hugo new posts/my-first-post.md\n
//修改一下
vi content/posts/my-first-post.md
---
title: "My First Post"
date: 2022-09-20T09:59:41+08:00
draft: false
---

jjj
//预览
hugo server -D
//构建，默认生成的文件在public目录下
hugo -D</code></pre>
<h2>相关链接</h2>
<p><a href="https://gohugo.io/getting-started/quick-start/">https://gohugo.io/getting-started/quick-start/</a></p>
<p><a href="https://themes.gohugo.io">https://themes.gohugo.io</a></p></div>]]></description>
            <guid isPermaLink="false">静态HTML和CSS网站生成器 - Hugo</guid>
        </item>
        <item>
            <title><![CDATA[缺少某些方法: mustEmbedUnimplementedJobServiceServer()]]></title>
            <description><![CDATA[<link rel="stylesheet" href="https://www.cuiwei.net/static/css/github-markdown.min.css?t=20260430" type="text/css" media="screen" /><div class="markdown-body"><p>默认情况下，要使用此工具生成的方法注册服务，服务实现必须嵌入相应的<code>Unimplemented&lt;ServiceName&gt;Server</code>，以实现未来的兼容性。这是与之前包含在protoc-gen-go中包含的grpc代码生成器的行为更改。要恢复此行为，请设置<code>require_unimplemented_servers=false</code>选项。例如：</p>
<pre><code>protoc --go-grpc_out=require_unimplemented_servers=false[,other options...]:. \</code></pre>
<p>请注意，不建议这样做，并且仅提供该选项来恢复与之前生成的代码向后兼容性。</p>
<p>建议的做法是，在你的实现（blogServer）文件中添加如下代码</p>
<pre><code>...

type blogServer struct {
    *blogv1.UnimplementedBlogServiceServer //解决"missing mustEmbedUnimplementedBlogServiceServer method"的问题
}

//func (s *blogServer) mustEmbedUnimplementedBlogServiceServer() {
//  //为了解决"missing mustEmbedUnimplementedBlogServiceServer method"的问题，但我测试时不好使
//}
...</code></pre>
<h2>来源</h2>
<p><a href="https://github.com/grpc/grpc-go/blob/master/cmd/protoc-gen-go-grpc/README.md">https://github.com/grpc/grpc-go/blob/master/cmd/protoc-gen-go-grpc/README.md</a></p></div>]]></description>
            <guid isPermaLink="false">缺少某些方法: mustEmbedUnimplementedJobServiceServer()</guid>
        </item>
        <item>
            <title><![CDATA[go grpc]]></title>
            <description><![CDATA[<link rel="stylesheet" href="https://www.cuiwei.net/static/css/github-markdown.min.css?t=20260430" type="text/css" media="screen" /><div class="markdown-body"><h2>安装go环境</h2>
<p>略。。。</p>
<h2>安装protobuf</h2>
<pre><code>brew install protobuf

cuiwei@weideMacBook-Pro ~ % protoc --version
libprotoc 3.21.5
</code></pre>
<h2>安装Go plugins for the protocol compiler</h2>
<pre><code>$ go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.28
$ go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@v1.2

export PATH="$PATH:$(go env GOPATH)/bin"</code></pre>
<h2>生成代码</h2>
<pre><code>protoc --go_out=. ./protos/job.proto
//protoc --go-grpc_out=. ./protos/job.proto
protoc --go-grpc_out=require_unimplemented_servers=false:. ./protos/job.proto</code></pre>
<h2>参考</h2>
<p><a href="https://grpc.io/docs/languages/go/quickstart/">https://grpc.io/docs/languages/go/quickstart/</a></p></div>]]></description>
            <guid isPermaLink="false">go grpc</guid>
        </item>
        <item>
            <title><![CDATA[php 使用 grpc]]></title>
            <description><![CDATA[<link rel="stylesheet" href="https://www.cuiwei.net/static/css/github-markdown.min.css?t=20260430" type="text/css" media="screen" /><div class="markdown-body"><h2>前提</h2>
<ol>
<li>protoc（the protobuf compiler binary to generate PHP classes for your messages and service definition.）</li>
<li>grpc_php_plugin（a plugin for protoc to generate the service stub classes.）</li>
<li>grpc（PHP扩展）</li>
<li>protobuf（PHP扩展）</li>
<li>grpc/grpc（composer包）</li>
<li>google/protobuf（composer包）</li>
</ol>
<p>其中<code>protoc</code>，<code>grpc_php_plugin</code>，<code>grpc</code>，<code>protobuf</code>，这4个最耗时（正常需要30～40分钟），我做了一个<code>php-fpm 8.1.9</code>的镜像，大家可以参考：
<a href="https://github.com/chudaozhe/grpc-php">https://github.com/chudaozhe/grpc-php</a></p>
<h2>测试</h2>
<h3>服务端demo</h3>
<p><a href="https://grpc.io/docs/languages/node/quickstart/">https://grpc.io/docs/languages/node/quickstart/</a></p>
<blockquote>
<p>您只能在 PHP 中创建 gRPC 客户端。使用另一个 用于创建 gRPC 服务器的语言。</p>
</blockquote>
<p>所以咱使用node创建服务端</p>
<pre><code># Clone the repository to get the example code
$ git clone -b @grpc/grpc-js@1.9.0 --depth 1 --shallow-submodules https://github.com/grpc/grpc-node
# Navigate to the node example
$ cd grpc-node/examples
# Install the example's dependencies
$ npm install
# Navigate to the dynamic codegen "hello, world" Node example:
$ cd helloworld/dynamic_codegen</code></pre>
<p>运行 服务端 程序</p>
<pre><code>cd examples/helloworld/dynamic_codegen
node greeter_server.js</code></pre>
<h3>客户端demo（PHP）</h3>
<p><a href="https://github.com/chudaozhe/php-demo/tree/master/grpc">https://github.com/chudaozhe/php-demo/tree/master/grpc</a></p>
<h2>参考</h2>
<p><a href="https://github.com/grpc/grpc/blob/v1.60.0/src/php/README.md">https://github.com/grpc/grpc/blob/v1.60.0/src/php/README.md</a></p></div>]]></description>
            <guid isPermaLink="false">php 使用 grpc</guid>
        </item>
        <item>
            <title><![CDATA[cli和双击go的二进制文件得到不同的路径]]></title>
            <description><![CDATA[<link rel="stylesheet" href="https://www.cuiwei.net/static/css/github-markdown.min.css?t=20260430" type="text/css" media="screen" /><div class="markdown-body"><p>先看代码</p>
<pre><code>func main() {
    root, err0 := os.Getwd()
    if err0 != nil {
        println("empty")
    }
    println("dir: " + root)
}</code></pre>
<p>生成可执行文件</p>
<pre><code>cuiwei@weideMacBook-Pro demo % go build -o app2 .   </code></pre>
<h2>cli的方式执行</h2>
<pre><code>cuiwei@weideMacBook-Pro demo % ./app2               
dir: /Users/cuiwei/GolandProjects/demo</code></pre>
<h2>双击 app2 的方式</h2>
<pre><code>Last login: Fri Sep  9 12:44:47 on ttys001
/Users/cuiwei/GolandProjects/demo/app2 ; exit;
cuiwei@weideMacBook-Pro ~ % /Users/cuiwei/GolandProjects/demo/app2 ; exit;
dir: /Users/cuiwei</code></pre>
<h2>总结</h2>
<p>所以，依赖<code>os.Getwd()</code>做文件上传的，需要注意一下</p>
<h2>更好的办法</h2>
<pre><code>func main() {
    ePath, err := os.Executable()
    if err != nil {
        panic(err)
    }

    // 全路径
    fmt.Println(ePath)
    // 所在目录
    fmt.Println("file directory", path.Dir(ePath))
}</code></pre>
<p><code>os.Executable()</code>获取的是可执行文件的绝对路径。需要注意的是：如果执行<code>go run main.go</code>得到的是一个临时的路径</p>
<pre><code>cuiwei@weideMacBook-Pro demo % go run main.go 
/var/folders/0r/9vy7796d43v3tmp2fwwfqgpw0000gn/T/go-build1779961896/b001/exe/main
file directory /var/folders/0r/9vy7796d43v3tmp2fwwfqgpw0000gn/T/go-build1779961896/b001/exe
</code></pre></div>]]></description>
            <guid isPermaLink="false">cli和双击go的二进制文件得到不同的路径</guid>
        </item>
        <item>
            <title><![CDATA[alpine容器中运行go的二进制文件]]></title>
            <description><![CDATA[<link rel="stylesheet" href="https://www.cuiwei.net/static/css/github-markdown.min.css?t=20260430" type="text/css" media="screen" /><div class="markdown-body"><p>问题重现</p>
<pre><code>/data # ls
app
/data # ./app 
/bin/sh: ./app: not found</code></pre>
<h2>解决办法</h2>
<h3>方法一</h3>
<p>查看下依赖库</p>
<pre><code>/data # ldd app 
    /lib64/ld-linux-x86-64.so.2 (0x7ff4fc486000)
    libpthread.so.0 =&gt; /lib64/ld-linux-x86-64.so.2 (0x7ff4fc486000)
    libc.so.6 =&gt; /lib64/ld-linux-x86-64.so.2 (0x7ff4fc486000)
/data # ls /lib64/ld-linux-x86-64.so.2
ls: /lib64/ld-linux-x86-64.so.2: No such file or directory</code></pre>
<p>结果提示没找到</p>
<p>借鉴大神的方法</p>
<pre><code>mkdir /lib64 &amp;&amp; ln -s /lib/libc.musl-x86_64.so.1 /lib64/ld-linux-x86-64.so.2</code></pre>
<h3>方法二</h3>
<p><code>go build</code>添加参数<code>--tags netgo</code>，如下</p>
<pre><code>go build -mod=mod --tags netgo -o hello .</code></pre>
<h3>最后</h3>
<p>看下<code>Dockerfile</code>文件</p>
<pre><code>FROM golang:1.19 AS build
WORKDIR /go/src/gin-demo
COPY . .

RUN go env -w GO111MODULE=on
RUN go env -w GOPROXY=https://goproxy.cn,direct

RUN go build -mod=mod --tags netgo -o hello .

FROM alpine:3.16
WORKDIR "/data"
EXPOSE 8097
# RUN mkdir /lib64 &amp;&amp; ln -s /lib/libc.musl-x86_64.so.1 /lib64/ld-linux-x86-64.so.2
COPY --from=build /go/src/gin-demo/hello app
CMD ["./app"]</code></pre>
<h2>参考</h2>
<p><a href="https://blog.51cto.com/welcomeweb/2652026">https://blog.51cto.com/welcomeweb/2652026</a></p>
<p><a href="https://blog.csdn.net/txblovehq/article/details/122233543">https://blog.csdn.net/txblovehq/article/details/122233543</a></p></div>]]></description>
            <guid isPermaLink="false">alpine容器中运行go的二进制文件</guid>
        </item>
        <item>
            <title><![CDATA[gin config]]></title>
            <description><![CDATA[<link rel="stylesheet" href="https://www.cuiwei.net/static/css/github-markdown.min.css?t=20260430" type="text/css" media="screen" /><div class="markdown-body"><p>这里我选择json文件作为项目的配置文件</p>
<pre><code>{
  "app_name": "app",
  "app_model": "debug",
  "app_host": "aa.com",
  "app_port": "8097",
  "database": {
    "dsn": "root:@tcp(docker-mysql:3306)/test?charset=utf8&amp;parseTime=True&amp;loc=Local&amp;timeout=10ms"
  },
  "redis_config": {
    "host": "docker-redis",
    "port": ":6379",
    "password": "",
    "db": 0
  },
  "smtp_config": {
    "host": "smtp.mxhichina.com",
    "port": ":465",
    "ssl_port": ":25",
    "username": "notifications-noreply@aaaa.com",
    "password": "111111",
    "ssl": true
  },
  "file_config": {
    "prefix": "data/upload/",
    "avatar": "avatar/[admin_id].[ext]",
    "photo": "[date].[ext]",
    "editor": "editor/[date][ext]"
  }
}</code></pre>
<p>config.go</p>
<pre><code>...
func ParseConfig(path string)(*Config, error)  {
    file, err := os.Open(path)
    defer file.Close()

    if err != nil {
        panic(err)
    }
    reader := bufio.NewReader(file)
    decoder := json.NewDecoder(reader)
    if  err = decoder.Decode(&amp;cfg); err != nil{
        return nil, err
    }
    return cfg, nil
}</code></pre>
<p>使用的时候大概是这样</p>
<pre><code>//main.go
config.ParseConfig("app/config/app.json")

//其他文件中
config.GetConfig().RedisConfig.Host</code></pre>
<h2>发现问题</h2>
<p>在调试模式，上面的代码没有问题，当你要部署的时候发现找不到<code>app/config/app.json</code>文件了，因为<code>go build</code>只会打包go文件，json文件没打包</p>
<h2>解决问题</h2>
<p>Go 在1.16版解决了静态文件嵌入的问题 —— 加入了<code>go embed</code></p>
<p>下面看下如何使用<code>go embed</code>解决上面的问题</p>
<p>config.go</p>
<pre><code>package config

import (
    "embed"
    _ "embed"
    "encoding/json"
    "github.com/gin-gonic/gin"
)

//go:embed app.json
//go:embed app-dev.json
var f embed.FS

type Config struct {
    AppName     string         `json:"app_name"`
    AppModel    string         `json:"app_model"`
    AppHost     string         `json:"app_host"`
    AppPort     string         `json:"app_port"`
    Database    DatabaseConfig `json:"database"`
    RedisConfig RedisConfig    `json:"redis_config"`
    SMTPConfig  SMTPConfig     `json:"smtp_config"`
    FileConfig  FileConfig     `json:"file_config"`
}

type DatabaseConfig struct {
    DSN string `json:"dsn"`
}

type RedisConfig struct {
    Host     string `json:"host"`
    Port     string `json:"port"`
    Password string `json:"password"`
    Db       int    `json:"db"`
}

type SMTPConfig struct {
    Host     string `json:"host"`
    Port     string `json:"port"`
    SSLPort  string `json:"ssl_port"`
    Username string `json:"username"`
    Password string `json:"password"`
    SSL      bool   `json:"ssl"`
}

type FileConfig struct {
    Prefix string `json:"prefix"`
    Avatar string `json:"avatar"`
    Photo  string `json:"photo"`
    Editor string `json:"editor"`
}

func GetConfig() *Config {
    return cfg
}

var cfg *Config = nil

func ParseConfig() (*Config, error) {
    configPath := "app-dev.json"
    if gin.Mode() == gin.ReleaseMode {
        configPath = "app.json"
    }
    data, _ := f.ReadFile(configPath)
    err := json.Unmarshal(data, &amp;cfg)
    if err != nil {
        return nil, err
    }
    return cfg, nil
}</code></pre>
<h2>参考</h2>
<p><a href="https://blog.csdn.net/Batac_Lee/article/details/109625932">https://blog.csdn.net/Batac_Lee/article/details/109625932</a></p>
<p><a href="https://github.red/go-embed/">https://github.red/go-embed/</a></p>
<p><a href="https://colobu.com/2021/01/17/go-embed-tutorial/">https://colobu.com/2021/01/17/go-embed-tutorial/</a></p></div>]]></description>
            <guid isPermaLink="false">gin config</guid>
        </item>
        <item>
            <title><![CDATA[gorm 基本操作]]></title>
            <description><![CDATA[<link rel="stylesheet" href="https://www.cuiwei.net/static/css/github-markdown.min.css?t=20260430" type="text/css" media="screen" /><div class="markdown-body"><pre><code>type Test struct {
    Id         int    `json:"id"`
    Name       string `json:"name"`
    Memo       string `json:"memo"`
    CreateTime string `json:"create_time"`
    UpdateTime string `json:"update_time"`
    DeleteTime string `json:"delete_time"`
}

// 设置表名
func (Test) TableName() string {
    return "cw_test"
}</code></pre>
<h2>查询</h2>
<h3>检索单个对象</h3>
<ul>
<li>First：获取第一条记录（主键升序）</li>
<li>Take：获取一条记录，没有指定排序字段</li>
<li>Last：获取最后一条记录（主键降序）</li>
</ul>
<p>下面以 First 为例，第一种：</p>
<pre><code>    test := &amp;Test{}
    result := orm.Db.First(&amp;test)
    fmt.Println(result.RowsAffected) //返回找到的记录数: 0 or 1
    if errors.Is(result.Error, gorm.ErrRecordNotFound) {
        fmt.Println("not found")
        return
    }
    data, _ := json.Marshal(&amp;test)
    fmt.Printf("%s\n", data)</code></pre>
<p>第二种：</p>
<pre><code>    result := map[string]interface{}{}
    orm.Db.Model(&amp;Test{}).First(&amp;result)
    data, _ := json.Marshal(&amp;result)
    fmt.Printf("%s\n", data)</code></pre>
<p>输出</p>
<pre><code>{"create_time":"0","delete_time":"0","id":1,"memo":"b","name":"a","update_time":"0"}</code></pre>
<h3>用主键检索</h3>
<pre><code>    //test := &amp;Test{}

    //result := orm.Db.First(&amp;test, 2)
    //result := orm.Db.First(&amp;test, "name = ?", "aa")

    test := &amp;Test{Id: 2}
    result := orm.Db.First(&amp;test)

    fmt.Println(result.RowsAffected) //返回找到的记录数: 0 or 1
    if errors.Is(result.Error, gorm.ErrRecordNotFound) {
        fmt.Println("not found")
        return
    }

    data, _ := json.Marshal(&amp;test)
    fmt.Printf("%s\n", data)

    //db.Model()
    var result Test
    orm.Db.Model(Test{Id: 2}).First(&amp;result)
    data, _ := json.Marshal(&amp;result)
    fmt.Printf("%s\n", data)</code></pre>
<p>输出</p>
<pre><code>{"id":2,"name":"aa","memo":"bb","create_time":"0","update_time":"0","delete_time":"0"}</code></pre>
<p>find(in)</p>
<pre><code>    var tests []*Test
    orm.Db.Find(&amp;tests, []int{1, 2, 3})
    data, _ := json.Marshal(&amp;tests)
    fmt.Printf("%s\n", data)</code></pre>
<p>输出</p>
<pre><code>[{"id":1,"name":"a","memo":"b","create_time":"0","update_time":"0","delete_time":"0"},{"id":2,"name":"aa","memo":"bb","create_time":"0","update_time":"0","delete_time":"0"}]</code></pre>
<h3>检索全部对象</h3>
<p>无条件查全部</p>
<pre><code>    var tests []*Test
    result := orm.Db.Find(&amp;tests)
    fmt.Println(result.RowsAffected) //返回找到的记录总数
    if result.Error != nil {
        fmt.Println("not found")
        return
    }

    data, _ := json.Marshal(&amp;tests)
    fmt.Printf("%s\n", data)</code></pre>
<p>输出</p>
<pre><code>[{"id":1,"name":"a","memo":"b","create_time":"0","update_time":"0","delete_time":"0"},{"id":2,"name":"aa","memo":"bb","create_time":"0","update_time":"0","delete_time":"0"}]</code></pre>
<h3>条件</h3>
<p>String 条件</p>
<pre><code>    var tests []*Test

    result := orm.Db.Where("name &lt;&gt; ?", "jinzhu").Find(&amp;tests)
    //result := orm.Db.Where("name IN ?", []string{"jinzhu", "jinzhu 2"}).Find(&amp;tests)
    //result := orm.Db.Where("name LIKE ?", "%jin%").Find(&amp;tests)
    //result := orm.Db.Where("name = ? AND age &gt;= ?", "jinzhu", "22").Find(&amp;tests)
    //result := orm.Db.Where("updated_at &gt; ?", lastWeek).Find(&amp;tests)
    //result := orm.Db.Where("created_at BETWEEN ? AND ?", lastWeek, today).Find(&amp;tests)

    fmt.Println(result.RowsAffected) //返回找到的记录总数
    if result.Error != nil {
        fmt.Println("not found")
        return
    }

    data, _ := json.Marshal(&amp;tests)
    fmt.Printf("%s\n", data)</code></pre>
<p>Struct &amp; Map 条件</p>
<pre><code>// Struct
db.Where(&amp;User{Name: "jinzhu", Age: 20}).First(&amp;user)
// SELECT * FROM users WHERE name = "jinzhu" AND age = 20 ORDER BY id LIMIT 1;

// Map
db.Where(map[string]interface{}{"name": "jinzhu", "age": 20}).Find(&amp;users)
// SELECT * FROM users WHERE name = "jinzhu" AND age = 20;

// Slice of primary keys
db.Where([]int64{20, 21, 22}).Find(&amp;users)
// SELECT * FROM users WHERE id IN (20, 21, 22);</code></pre>
<blockquote>
<p>注意：字段值为0，&quot;，false或其他零值，将不会用于构建查询条件。要在查询条件中包含0值，可以使用map，它将包含所有键值作为查询条件</p>
</blockquote>
<pre><code>db.Where(&amp;User{Name: "jinzhu", Age: 0}).Find(&amp;users)
// SELECT * FROM users WHERE name = "jinzhu";

db.Where(map[string]interface{}{"Name": "jinzhu", "Age": 0}).Find(&amp;users)
// SELECT * FROM users WHERE name = "jinzhu" AND age = 0;</code></pre>
<p>指定结构体查询字段</p>
<pre><code>db.Where(&amp;User{Name: "jinzhu"}, "name", "Age").Find(&amp;users)
// SELECT * FROM users WHERE name = "jinzhu" AND age = 0;

db.Where(&amp;User{Name: "jinzhu"}, "Age").Find(&amp;users)
// SELECT * FROM users WHERE age = 0;</code></pre>
<h2>创建</h2>
<h3>创建记录</h3>
<pre><code>    test := Test{Name: "zhangshan3", Memo: "真棒3", CreateTime: helper.GetUnix()}

    result := orm.Db.Create(&amp;test)
    //result := orm.Db.Select("Name", "CreateTime").Create(&amp;test) //除了Name，CreateTime，其他字段为空
    //result := orm.Db.Omit("Memo").Create(&amp;test) //填充所有字段，除了Memo

    fmt.Println(test.Id)             //返回插入数据的主键
    fmt.Println(result.RowsAffected) //返回找到的记录总数
    if result.Error != nil {
        fmt.Println("insert error")
        return
    }

    data, _ := json.Marshal(&amp;test)
    fmt.Printf("%s\n", data)</code></pre>
<h3>批量插入</h3>
<pre><code>    tests := []Test{{Name: "jinzhu1"}, {Name: "jinzhu2", CreateTime: helper.GetUnix()}, {Name: "heihei"}}
    result := orm.Db.Create(&amp;tests)
    //result := orm.Db.CreateInBatches(tests, 2) //分批创建，一批2个

    for _, user := range tests {
        fmt.Println(user.Id) // 1,2,3
    }
    fmt.Println(result.RowsAffected) //返回找到的记录总数
    if result.Error != nil { //某条数据出错，将导致全部错误，但mysql的主键会被占用
        fmt.Println("insert error")
        return
    }

    data, _ := json.Marshal(&amp;tests)
    fmt.Printf("%s\n", data)</code></pre>
<h3>创建钩子</h3>
<p>GORM 允许用户定义的钩子有 BeforeSave, BeforeCreate, AfterSave, AfterCreate 创建记录时将调用这些钩子方法</p>
<pre><code>func (u *User) BeforeCreate(tx *gorm.DB) (err error) {
  u.UUID = uuid.New()

    if u.Role == "admin" {
        return errors.New("invalid role")
    }
    return
}</code></pre>
<p>如果您想跳过 钩子 方法，您可以使用 SkipHooks 会话模式，例如：</p>
<pre><code>DB.Session(&amp;gorm.Session{SkipHooks: true}).Create(&amp;user)

DB.Session(&amp;gorm.Session{SkipHooks: true}).Create(&amp;users)

DB.Session(&amp;gorm.Session{SkipHooks: true}).CreateInBatches(users, 100)</code></pre>
<h3>根据 Map 创建</h3>
<pre><code>    result := orm.Db.Model(&amp;Test{}).Create(map[string]interface{}{
        "Name": "jinzhu", "Memo": "真棒3",
    })
    fmt.Println(result.RowsAffected) //返回找到的记录总数
    if result.Error != nil {
        fmt.Println("insert error")
        return
    }

// batch insert from `[]map[string]interface{}{}`
db.Model(&amp;User{}).Create([]map[string]interface{}{
  {"Name": "jinzhu_1", "Age": 18},
  {"Name": "jinzhu_2", "Age": 20},
})</code></pre>
<h2>更新</h2>
<p>Save 是保存所有字段，你需要先查出来(test)，在test的基础上修改</p>
<pre><code>    test := Test{Id: 21}

    orm.Db.First(&amp;test)

    test.Name = "jinzhu up.."
    test.Memo = "up.."
    test.UpdateTime = helper.GetUnix()
    result := orm.Db.Save(&amp;test)
    //result := orm.Db.Table("cw_test").Where("id = ?", 21).Update("name", "hello")

    fmt.Println(result.RowsAffected) //返回找到的记录总数
    if result.Error != nil {
        fmt.Println("update error")
        return
    }

    data, _ := json.Marshal(&amp;test)
    fmt.Printf("%s\n", data)</code></pre>
<p>更新单个列，Update 为部分更新</p>
<pre><code>    test := Test{Id: 21}
    result := orm.Db.Model(&amp;test).Update("name", "hello222")

    fmt.Println(result.RowsAffected) //返回找到的记录总数
    if result.Error != nil {
        fmt.Println("update error")
        return
    }

    data, _ := json.Marshal(&amp;test)
    fmt.Printf("%s\n", data)

// 条件更新
db.Model(&amp;User{}).Where("active = ?", true).Update("name", "hello")
// UPDATE users SET name='hello', updated_at='2013-11-17 21:34:10' WHERE active=true;

// 根据条件和 model 的值进行更新
db.Model(&amp;user).Where("active = ?", true).Update("name", "hello")
// UPDATE users SET name='hello', updated_at='2013-11-17 21:34:10' WHERE id=111 AND active=true;
</code></pre>
<h3>更新多列</h3>
<p>Updates 方法支持 struct 和 map[string]interface{} 参数。当使用 struct 更新时，默认情况下，GORM 只会更新非零值的字段</p>
<pre><code>    test := Test{Id: 21}
    result := orm.Db.Model(&amp;test).Updates(Test{Name: "hello33", Memo: "hhh", UpdateTime: helper.GetUnix()})
    //result := orm.Db.Model(&amp;test).Updates(map[string]interface{}{"name": "hello", "Memo": 18})

    fmt.Println(result.RowsAffected) //返回找到的记录总数
    if result.Error != nil {
        fmt.Println("update error")
        return
    }

    data, _ := json.Marshal(&amp;test)
    fmt.Printf("%s\n", data)</code></pre>
<blockquote>
<p>注意 当通过 struct 更新时，GORM 只会更新非零字段。</p>
</blockquote>
<h3>更新选定字段</h3>
<p>如果您想要在更新时选定、忽略某些字段，您可以使用 Select、Omit</p>
<pre><code>    test := Test{Id: 21}
    result := orm.Db.Model(&amp;test).Select("Name", "Memo").Updates(Test{Name: "new_name", Memo: "0"})

    fmt.Println(result.RowsAffected) //返回找到的记录总数
    if result.Error != nil {
        fmt.Println("update error")
        return
    }

    data, _ := json.Marshal(&amp;test)
    fmt.Printf("%s\n", data)

// 使用 Map 进行 Select
// User's ID is `111`:
db.Model(&amp;user).Select("name").Updates(map[string]interface{}{"name": "hello", "age": 18, "active": false})
// UPDATE users SET name='hello' WHERE id=111;

db.Model(&amp;user).Omit("name").Updates(map[string]interface{}{"name": "hello", "age": 18, "active": false})
// UPDATE users SET age=18, active=false, updated_at='2013-11-17 21:34:10' WHERE id=111;

// 使用 Struct 进行 Select（会 select 零值的字段）
db.Model(&amp;user).Select("Name", "Age").Updates(User{Name: "new_name", Age: 0})
// UPDATE users SET name='new_name', age=0 WHERE id=111;

// Select 所有字段（查询包括零值字段的所有字段）
db.Model(&amp;user).Select("*").Update(User{Name: "jinzhu", Role: "admin", Age: 0})

// Select 除 Role 外的所有字段（包括零值字段的所有字段）
db.Model(&amp;user).Select("*").Omit("Role").Update(User{Name: "jinzhu", Role: "admin", Age: 0})</code></pre>
<h3>更新 Hook</h3>
<p>对于更新操作，GORM 支持 BeforeSave、BeforeUpdate、AfterSave、AfterUpdate 钩子，这些方法将在更新记录时被调用</p>
<pre><code>func (u *User) BeforeUpdate(tx *gorm.DB) (err error) {
    if u.Role == "admin" {
        return errors.New("admin user not allowed to update")
    }
    return
}</code></pre>
<h3>批量更新</h3>
<p>无主键更新</p>
<pre><code>    result := orm.Db.Model(Test{}).Where("name = ?", "new_name").Updates(Test{Name: "new_name", Memo: "10"})

    fmt.Println(result.RowsAffected) //返回找到的记录总数
    if result.Error != nil {
        fmt.Println("update error")
        return
    }

// 根据 struct 更新
db.Model(User{}).Where("role = ?", "admin").Updates(User{Name: "hello", Age: 18})
// UPDATE users SET name='hello', age=18 WHERE role = 'admin';

// 根据 map 更新
db.Table("users").Where("id IN ?", []int{10, 11}).Updates(map[string]interface{}{"name": "hello", "age": 18})
// UPDATE users SET name='hello', age=18 WHERE id IN (10, 11);</code></pre>
<h2>删除</h2>
<h3>删除一条记录</h3>
<pre><code>    test := Test{Id: 21}
    result := orm.Db.Delete(&amp;test)

    fmt.Println(result.RowsAffected) //返回找到的记录总数
    if result.Error != nil {
        fmt.Println("del error")
        return
    }

// 带额外条件的删除
db.Where("name = ?", "jinzhu").Delete(&amp;email)
// DELETE from emails where id = 10 AND name = "jinzhu";
</code></pre>
<h3>根据主键删除</h3>
<pre><code>    result := orm.Db.Delete(&amp;Test{}, 20)

    fmt.Println(result.RowsAffected) //返回找到的记录总数
    if result.Error != nil {
        fmt.Println("del error")
        return
    }

db.Delete(&amp;User{}, "10")
// DELETE FROM users WHERE id = 10;

db.Delete(&amp;users, []int{1,2,3})
// DELETE FROM users WHERE id IN (1,2,3);</code></pre>
<h3>批量删除</h3>
<p>无主键删除</p>
<pre><code>db.Where("email LIKE ?", "%jinzhu%").Delete(&amp;Email{})
// DELETE from emails where email LIKE "%jinzhu%";

db.Delete(&amp;Email{}, "email LIKE ?", "%jinzhu%")
// DELETE from emails where email LIKE "%jinzhu%";</code></pre>
<h3>逻辑删</h3>
<p>删除方法不变，区别在结构体定义</p>
<pre><code>type Test struct {
    Id         int            `json:"id"`
    //DeleteTime gorm.DeletedAt `json:"delete_time"` //逻辑删除时将该字段更新为Y-m-d H:i:s
    DeleteTime soft_delete.DeletedAt `json:"delete_time"`// 逻辑删除时将该字段更新为时间戳
}</code></pre>
<blockquote>
<p>注意，使用 soft_delete.DeletedAt 类型时需要导入 gorm.io/plugin/soft_delete</p>
</blockquote></div>]]></description>
            <guid isPermaLink="false">gorm 基本操作</guid>
        </item>
        <item>
            <title><![CDATA[go 数字和字符串相互转换]]></title>
            <description><![CDATA[<link rel="stylesheet" href="https://www.cuiwei.net/static/css/github-markdown.min.css?t=20260430" type="text/css" media="screen" /><div class="markdown-body"><p>字符串转数字</p>
<p>方法1</p>
<pre><code>userId := "100"
userId2, _ := strconv.Atoi(userId)

//如果userId不是数字字符串，结果为0</code></pre>
<p>方法2</p>
<pre><code>    //golang strconv.ParseInt 是将字符串转换为数字的函数,功能灰常之强大.
    //参数1 数字的字符串形式
    //参数2 数字字符串的进制 比如二进制 八进制 十进制 十六进制
    //参数3 返回结果的bit大小 也就是int8 int16 int32 int64
    //func ParseInt(s string, base int, bitSize int) (i int64, err error)

    i, err := strconv.ParseInt("123", 10, 32)
    if err != nil {
        panic(err)
    }
    println(i)</code></pre>
<p>数字转字符串</p>
<pre><code>a := 200
a2 :=strconv.Itoa(a)
fmt.Println(a2)</code></pre>
<p>参考</p>
<p><a href="https://blog.csdn.net/qq_42410605/article/details/112677904">https://blog.csdn.net/qq_42410605/article/details/112677904</a></p></div>]]></description>
            <guid isPermaLink="false">go 数字和字符串相互转换</guid>
        </item>
        <item>
            <title><![CDATA[go gin封装redis]]></title>
            <description><![CDATA[<link rel="stylesheet" href="https://www.cuiwei.net/static/css/github-markdown.min.css?t=20260430" type="text/css" media="screen" /><div class="markdown-body"><h2>封装</h2>
<pre><code>package cache

import (
    "fmt"
    "github.com/go-redis/redis"
    "time"
)

var RedisClient *redis.Client

func InitRedis() {
    RedisClient = redis.NewClient(&amp;redis.Options{
        Addr:         "localhost:6379",
        Password:     "",
        DB:           0,
        DialTimeout:  10 * time.Second,
        ReadTimeout:  30 * time.Second,
        WriteTimeout: 30 * time.Second,
        PoolSize:     10,
        PoolTimeout:  30 * time.Second,
    })
    ping, err := RedisClient.Ping().Result()
    if err == redis.Nil {
        fmt.Print("Redis异常")
    } else if err != nil {
        fmt.Print("失败:", err)
    } else {
        fmt.Print(ping)
    }
}</code></pre>
<h2>使用</h2>
<p>model</p>
<pre><code>package model

import (
    "gin-demo/api/core/cache"
    "strconv"
)

func Key(id int) string {
    return "gin.www.admin." + strconv.Itoa(id) + ".token"
}

func GetToken(id int) (string, error) {
    key := Key(id)
    token, err := cache.RedisClient.Get(key).Result()
    return token, err
}</code></pre>
<p>controller</p>
<pre><code>    //model.SetToken(1, true, 0)
    token, err := model.GetToken(1)
    if err != nil {
        fmt.Println(err.Error())
        return
    }
    fmt.Println(token)</code></pre></div>]]></description>
            <guid isPermaLink="false">go gin封装redis</guid>
        </item>
        <item>
            <title><![CDATA[go gin 封装gorm]]></title>
            <description><![CDATA[<link rel="stylesheet" href="https://www.cuiwei.net/static/css/github-markdown.min.css?t=20260430" type="text/css" media="screen" /><div class="markdown-body"><p>封装</p>
<pre><code>package db

import (
    "fmt"
    "gorm.io/driver/mysql"
    "gorm.io/gorm"
)

var Db *gorm.DB

func init() {
    var err error
    dsn := "root:@tcp(127.0.0.1:3306)/test?charset=utf8&amp;parseTime=True&amp;loc=Local&amp;timeout=10ms"
    Db, err = gorm.Open(mysql.Open(dsn), &amp;gorm.Config{})

    if err != nil {
        fmt.Printf("mysql connect error %v", err)
    }

    if Db.Error != nil {
        fmt.Printf("database error %v", Db.Error)
    }
}
</code></pre>
<h2>使用</h2>
<p>model</p>
<pre><code>package model

import (
    orm "gin-demo/api/core/db"
)

type User struct {
    Id       int    `json:"id"`
    Username string `json:"username"`
    Nickname string `json:"nickname"`
}

// 设置表名
func (User) TableName() string { //默认为结构体名称的复数，即users
    return "user"
}

// 根据id查询用户User
func GetUserById(id string) (user User, err error) {
    orm.Db.First(&amp;user, "id = ?", id)
    return
}</code></pre>
<p>controller</p>
<pre><code>package controller

import (
    "gin-demo/api/core"
    "gin-demo/api/model"
    "github.com/gin-gonic/gin"
)

func GetUserDetail(c *gin.Context) {
    id, ok := c.Params.Get("id")
    if !ok {
        core.Error(c, 400, "无效的id")
    }
    detail, err := model.GetUserById(id)
    if err != nil {
        core.Error(c, 404, err.Error())
    } else {
        core.Success(c, 0, detail)
    }
}
</code></pre>
<h2>参考</h2>
<p><a href="https://www.cnblogs.com/-wenli/p/13748719.html">https://www.cnblogs.com/-wenli/p/13748719.html</a></p></div>]]></description>
            <guid isPermaLink="false">go gin 封装gorm</guid>
        </item>
        <item>
            <title><![CDATA[go gin上传文件]]></title>
            <description><![CDATA[<link rel="stylesheet" href="https://www.cuiwei.net/static/css/github-markdown.min.css?t=20260430" type="text/css" media="screen" /><div class="markdown-body"><h2>multipart/form-data</h2>
<pre><code>
func Upload(c *gin.Context) {
    file, err := c.FormFile("file")
    if err != nil {
        c.String(500, "上传图片出错")
    }

    extName := path.Ext(file.Filename)
    allowExtMap := map[string]bool{
        ".jpg":  true,
        ".png":  true,
        ".gif":  true,
        ".jpeg": true,
    }
    if _, ok := allowExtMap[extName]; !ok {
        c.String(200, "文件类型不合法")
        return
    }

    dir := "./upload/" + helper.GetDay()
    if err := os.MkdirAll(dir, 0755); err != nil {
        log.Println(err)
    }
    fileUnixName := strconv.FormatInt(helper.GetUnix(), 10)
    dst := path.Join(dir, fileUnixName+extName)
    fmt.Println(dst)
    err2 := c.SaveUploadedFile(file, dst)
    if err2 != nil {
        return
    }
    c.String(http.StatusOK, dst)
}</code></pre>
<h2>base64编码</h2>
<pre><code>// base64
func Upload2(c *gin.Context) {
    content := c.Request.FormValue("content")
    if len(content) == 0 {
        c.String(400, "base64编码为空")
    }

    dir := "./upload/" + helper.GetDay()
    if err := os.MkdirAll(dir, 0755); err != nil {
        log.Println(err)
    }
    fileUnixName := strconv.FormatInt(helper.GetUnix(), 10)
    dst := path.Join(dir, fileUnixName+".jpg")
    fmt.Println(dst)

    fileObj, err := os.OpenFile(dst, os.O_CREATE|os.O_WRONLY, 0644)
    if err != nil {
        fmt.Println("打开文件出错，err:", err)
        return
    }
    // 向打开的文件句柄中写入内容
    fmt.Fprintf(fileObj, "%s", Base64Decode(content))
    core.Success(c, 204, dst)
}

func Base64Decode(str string) string {
    reader := strings.NewReader(str)
    decoder := base64.NewDecoder(base64.RawStdEncoding, reader)
    buf := make([]byte, 1024)
    dst := ""
    for {
        n, err := decoder.Read(buf)
        dst += string(buf[:n])
        if n == 0 || err != nil {
            break
        }
    }
    return dst
}</code></pre>
<h2>参考</h2>
<p><a href="https://blog.csdn.net/qq_55752792/article/details/126330655">https://blog.csdn.net/qq_55752792/article/details/126330655</a></p>
<p><a href="https://blog.csdn.net/taoshihan/article/details/113895681">https://blog.csdn.net/taoshihan/article/details/113895681</a></p></div>]]></description>
            <guid isPermaLink="false">go gin上传文件</guid>
        </item>
        <item>
            <title><![CDATA[laravel 调试工具]]></title>
            <description><![CDATA[<link rel="stylesheet" href="https://www.cuiwei.net/static/css/github-markdown.min.css?t=20260430" type="text/css" media="screen" /><div class="markdown-body"><h2>Laravel-debugbar</h2>
<p>可以打印出每个请求执行的sql
<img src="https://www.cuiwei.net/data/upload/2022-08-30/166187426561062.jpg" alt="WX202208302343202x.jpg" /></p>
<h3>安装</h3>
<pre><code>composer require barryvdh/laravel-debugbar</code></pre>
<p>执行完即可，打开任一html页面（返回json的不行）</p>
<p>更详细的说明请参考：<a href="https://github.com/barryvdh/laravel-debugbar">https://github.com/barryvdh/laravel-debugbar</a></p>
<h2>Artisan tail</h2>
<p>实时显示系统日志</p>
<h3>安装</h3>
<pre><code>composer require spatie/laravel-tail</code></pre>
<p>执行完即可，常用命令</p>
<pre><code>php artisan help tail

php artisan tail</code></pre>
<p>更详细的说明请参考：<a href="https://github.com/spatie/laravel-tail">https://github.com/spatie/laravel-tail</a></p></div>]]></description>
            <guid isPermaLink="false">laravel 调试工具</guid>
        </item>
        <item>
            <title><![CDATA[yii debug和gii模块]]></title>
            <description><![CDATA[<link rel="stylesheet" href="https://www.cuiwei.net/static/css/github-markdown.min.css?t=20260430" type="text/css" media="screen" /><div class="markdown-body"><h2>开启 <code>vi config/main.php</code></h2>
<pre><code>if (YII_ENV_DEV) {
    $config['bootstrap'][] = 'debug';
    $config['modules']['debug'] = [
        'class'=&gt;'yii\debug\Module',
        'allowedIPs'=&gt;['*',],
    ];

    $config['bootstrap'][] = 'gii';
    $config['modules']['gii'] = [
        'class' =&gt; 'yii\gii\Module',
        'allowedIPs' =&gt; ['*'],
    ];
}</code></pre>
<h2>访问链接</h2>
<p><a href="http://yii.cw.net/?r=gii">http://yii.cw.net/?r=gii</a></p>
<p><a href="http://yii.cw.net/?r=debug">http://yii.cw.net/?r=debug</a></p>
<p>如果启用了美化的Url, <code>enablePrettyUrl=true</code></p>
<p><a href="http://yii.cw.net/gii">http://yii.cw.net/gii</a></p>
<p><a href="http://yii.cw.net/debug">http://yii.cw.net/debug</a></p>
<h2>已知问题</h2>
<p>yii2.0.46, yii2-gii2.2.4在php8.1.9会报错，切换到php7.4才可以</p></div>]]></description>
            <guid isPermaLink="false">yii debug和gii模块</guid>
        </item>
        <item>
            <title><![CDATA[The file or directory to be published does not exist: /var/www/yii-demo/vendor/yiisoft/yii2/gii/assets]]></title>
            <description><![CDATA[<link rel="stylesheet" href="https://www.cuiwei.net/static/css/github-markdown.min.css?t=20260430" type="text/css" media="screen" /><div class="markdown-body"><p>报错内容</p>
<pre><code>Invalid Argument – yii\base\InvalidArgumentException

The file or directory to be published does not exist: /var/www/yii-demo/vendor/yiisoft/yii2/gii/assets</code></pre>
<p>这错报的莫名其秒</p>
<p>我的目录结构</p>
<pre><code>├── api
│   ├── config
│   │   ├── bootstrap.php
│   │   ├── main.php
│   │   └── params.php
│   ├── controllers
│   │   ├── ArticleController.php
│   └── models
│       ├── Article.php
│       ├── Category.php
├── composer.json
├── composer.lock
├── readme.md
├── runtime
├── vendor
├── views
│   ├── layouts
│   │   └── main.php
│   └── page
│       └── index.php
└── web
    ├── assets
    ├── css
    ├── favicon.ico
    └── index.php</code></pre>
<p>解决办法</p>
<pre><code>vi api/config/main.php

    'basePath' =&gt; dirname(__DIR__, 2),</code></pre></div>]]></description>
            <guid isPermaLink="false">The file or directory to be published does not exist: /var/www/yii-demo/vendor/yiisoft/yii2/gii/assets</guid>
        </item>
        <item>
            <title><![CDATA[Json Web Token(JWT)的使用]]></title>
            <description><![CDATA[<link rel="stylesheet" href="https://www.cuiwei.net/static/css/github-markdown.min.css?t=20260430" type="text/css" media="screen" /><div class="markdown-body"><p>JWT 用于生成token，token里面可以包含用户信息，下面介绍两种php的实现方法</p>
<h2>借助 composer 库</h2>
<pre><code>composer require firebase/php-jwt</code></pre>
<h3>生成token</h3>
<pre><code>&lt;?php
require_once __DIR__ . '/../vendor/autoload.php';
use Firebase\JWT\JWT;

$key = 'abc';//app key
$payload = [
    'iss' =&gt; 'http://example.org',
    'aud' =&gt; 'http://example.com',
    'iat' =&gt; 1356999524,
    'nbf' =&gt; 1357000000
];

$token = JWT::encode($payload, $key, 'HS256');
echo $token.PHP_EOL;</code></pre>
<h3>传递token</h3>
<p>上一步生成了token，前端拿到后，在访问需要鉴权的接口时，通过header传给后端，类似这样</p>
<pre><code>Authorization: Bearer &lt;token&gt;</code></pre>
<h3>验证token</h3>
<pre><code>&lt;?php
require_once __DIR__ . '/../vendor/autoload.php';
use Firebase\JWT\JWT;
use Firebase\JWT\Key;

$key = 'abc';//app key
$token = '前端传过来的token'
$decoded = JWT::decode($token, new Key($key, 'HS256'));
print_r($decoded);</code></pre>
<h2>手动实现</h2>
<h3>生成token</h3>
<pre><code>&lt;?php
$key = 'abc';//app key
$payload = [
    'iss' =&gt; 'http://example.org',
    'aud' =&gt; 'http://example.com',
    'iat' =&gt; 1356999524,
    'nbf' =&gt; 1357000000
];
$base64header=base64UrlEncode(json_encode(['typ'=&gt;'JWT', 'alg'=&gt;'HS256'], JSON_UNESCAPED_SLASHES));
$base64payload=base64UrlEncode(json_encode($payload, JSON_UNESCAPED_SLASHES));
echo $token=$base64header.'.'.$base64payload.'.'.signature($base64header, $base64payload, $key);
echo PHP_EOL;

function signature(string $base64header, string $base64payload, string $key){
    return base64UrlEncode(hash_hmac('sha256',
        $base64header.
        '.'.
        $base64payload,
        $key,true));
}

function base64UrlEncode(string $input) {
    return str_replace('=', '', strtr(base64_encode($input), '+/', '-_'));
}</code></pre>
<h3>验证token</h3>
<pre><code>list($base64header, $base64payload, $sign) = explode('.', $token);
//签名验证
if (signature($base64header, $base64payload, $key)!==$sign) die('validation failure');

$payload = json_decode(base64UrlDecode($base64payload), JSON_OBJECT_AS_ARRAY);
var_dump($payload);

function signature(string $base64header, string $base64payload, string $key){
    return base64UrlEncode(hash_hmac('sha256',
        $base64header.
        '.'.
        $base64payload,
        $key,true));
}
function base64UrlDecode(string $input) {
    $remainder = strlen($input) % 4;
    if ($remainder) {
        $addlen = 4 - $remainder;
        $input .= str_repeat('=', $addlen);
    }
    return base64_decode(strtr($input, '-_', '+/'));
}</code></pre>
<h2>为什么相同的一组数据你生成的token和别人的不一样？</h2>
<ul>
<li>你用的是<code>['alg'=&gt;'HS256', 'typ'=&gt;'JWT']</code>，他用的是<code>['typ'=&gt;'JWT', 'alg'=&gt;'HS256']</code></li>
<li>你用的是<code>json_encode([], JSON_UNESCAPED_UNICODE)</code>，他用的是<code>json_encode([], JSON_UNESCAPED_SLASHES)</code></li>
</ul>
<h2>参考</h2>
<p><a href="https://jwt.io">https://jwt.io</a></p>
<p><a href="https://github.com/firebase/php-jwt">https://github.com/firebase/php-jwt</a></p>
<p><a href="https://www.h5w3.com/223863.html">https://www.h5w3.com/223863.html</a></p>
<p><a href="https://www.jianshu.com/p/a2efb2c8dcde">https://www.jianshu.com/p/a2efb2c8dcde</a></p>
<p><a href="https://blog.csdn.net/wnvalentin/article/details/123802484">https://blog.csdn.net/wnvalentin/article/details/123802484</a></p></div>]]></description>
            <guid isPermaLink="false">Json Web Token(JWT)的使用</guid>
        </item>
        <item>
            <title><![CDATA[Redis 应用场景]]></title>
            <description><![CDATA[<link rel="stylesheet" href="https://www.cuiwei.net/static/css/github-markdown.min.css?t=20260430" type="text/css" media="screen" /><div class="markdown-body"><p>首先，总结一下这些应用场景，它们不是独立存在的，很多都还是要依赖mysql；甚至项目初期这些都不是第一选择，很多场景mysql也能做，并且更简单</p>
<h2>生成唯一的随机数</h2>
<p>很多网站的详情页链接都有一个随机数，比如<code>http://www.cuiwei.net/p/1937090613</code>、<code>https://www.zhihu.com/question/48759965</code>、<code>https://segmentfault.com/a/1190000041091095</code>等</p>
<p>通常的做法是：一个<code>code(id,article_id,code,used_time)</code>表，一个<code>article(id,code, ...)</code>表，在添加文章时从<code>code</code>表选一个未使用的跟这篇文章绑定就可以了，前提是<code>code</code>表要有足够的码</p>
<p>下面重点来了，如何生成唯一的随机数？方法有很多，这里直接介绍使用 Redis 集合</p>
<p>如下，第一批可以直接用，第二批及以后的批次需要和之前的批次求差集，确保<code>我有的你没有</code>才能往数据库里写</p>
<pre><code>    function generateCode($length=5000){
        $codes=[];
        for($i=0; $i&lt;$length; $i++){
            $codes[]=mt_rand(1000000000, 1999999999);
        }
        return $codes;
    }

        $ok = $this-&gt;cache()-&gt;sAddArray('code', $this-&gt;generateCode());//第一批
        $ok2 = $this-&gt;cache()-&gt;sAddArray('code2', $this-&gt;generateCode());//第二批

        $sInter=$this-&gt;cache()-&gt;sInter('code2', 'code');//交集，我们都有的
        $sDiff=$this-&gt;cache()-&gt;sDiffStore('code3', 'code2', 'code');//差集，我有的你没有，并将结果保存到code3

        $count=$this-&gt;cache()-&gt;sCard('code');
        $list=$this-&gt;cache()-&gt;sMembers('code');
        $count2=$this-&gt;cache()-&gt;sCard('code2');
        $list2=$this-&gt;cache()-&gt;sMembers('code2');
        $this-&gt;status(0, ['ok'=&gt;$ok, 'ok2'=&gt;$ok2, 'count'=&gt;$count, 'count2'=&gt;$count2, 'sInter'=&gt;$sInter, 'sDiff'=&gt;$sDiff, 'list'=&gt;$list, 'list2'=&gt;$list2]);</code></pre>
<h2>抽奖</h2>
<pre><code>        $ok = $this-&gt;cache()-&gt;sAddArray('users', [1,2,3,4,5,6,7,8,9]);
        $users=$this-&gt;cache()-&gt;sPop('users', 2);//随机选出2名幸运观众，选中后不在参与下面的抽奖
//        $users=$this-&gt;cache()-&gt;sRandMember('users', 2);//随机选出2名幸运观众，选中后还可以继续参与下面的抽奖
        $this-&gt;status(0, ['ok'=&gt;$ok, 'users'=&gt;$users]);</code></pre>
<h2>浏览量排行榜</h2>
<pre><code>        $ok = $this-&gt;cache()-&gt;zIncrBy('hotblog', 1, 100);//id为100的文章每次访问加1
        $list=$this-&gt;cache()-&gt;zRevRangeByScore('hotblog', 30, 10, ['withscores' =&gt; TRUE, 'limit' =&gt; [0, 10]]);//浏览量在10～30之间的
        $list=$this-&gt;cache()-&gt;zRevRange('hotblog', 0, 2, true);//浏览量前3的文章
        $list=$this-&gt;cache()-&gt;zRange('hotblog', 0, 2, true);//浏览量垫底的3篇文章
        $rank=$this-&gt;cache()-&gt;zRevRank('hotblog', 100)+1;//id为100的文章排第几</code></pre>
<h2>互相关注，可能认识</h2>
<pre><code>        $this-&gt;cache()-&gt;sAdd('user_1_fans', 2,3,4,5,6);//user1的粉丝
        $this-&gt;cache()-&gt;sAdd('user_1_follow', 2,4);//user1的关注
        $this-&gt;cache()-&gt;sAdd('user_2_fans', 1);//user2的粉丝
        $this-&gt;cache()-&gt;sAdd('user_2_follow', 1,3);//user2的关注
        //互相关注（看我的关注列表，哪些是互相关注？
        $sInter=$this-&gt;cache()-&gt;sInter('user_1_fans', 'user_1_follow');
        //可能认识（user1可能认识/喜欢的人？如user1关注了user2，那么user2的关注列表可能是user1喜欢的
        $sDiff=$this-&gt;cache()-&gt;sDiff('user_2_follow', 'user_1_follow');</code></pre>
<h2>点赞</h2>
<pre><code>        $this-&gt;cache()-&gt;sAdd('article_100_like', 1);//user1给article100点赞
        $this-&gt;cache()-&gt;sRem('article_100_like', 1);//user1取消点赞
        $this-&gt;cache()-&gt;sIsMember('article_100_like', 1);//user1是否点赞
        $this-&gt;cache()-&gt;sMembers('article_100_like');//点赞的所以用户
        $this-&gt;cache()-&gt;sCard('article_100_like');//点赞总数</code></pre>
<h2>附近的人</h2>
<pre><code>        $r=$this-&gt;cache()-&gt;geoadd('users', 114.09981,33.585519, 'user1');
        $r=$this-&gt;cache()-&gt;geoadd('users', 114.070524,33.59067, 'user2');
        $r=$this-&gt;cache()-&gt;geoadd('users', 113.971066,33.577242, 'user3');

        //两个成员之间的距离
        $r=$this-&gt;cache()-&gt;geodist('users', 'user1', 'user2', 'km');
        //获取方圆500m的用户
        $r=$this-&gt;cache()-&gt;geoRadiusByMember('users', 'user1', 500, 'm');
</code></pre>
<h2>队列</h2>
<p>一些耗时的任务，可以加到队列里异步处理。如果想用redis写一个完善的队列是很复杂的，建议使用 beanstalkd、rabbitmq等</p>
<pre><code>        $r=$this-&gt;cache()-&gt;lPush('list', 2);//左边进
        $r=$this-&gt;cache()-&gt;rPop('list');//右边出</code></pre>
<h2>token登陆令牌</h2>
<p>这是我最常用的一个场景，当初从 memcache 切换到 redis 就是因为这个</p>
<p>用户登陆成功会给他设置一个token，并返回给前端；前端每次请求接口会把token带过来，后台验证这个token是否存在，不存在则提示用户重新登陆</p>
<pre><code>        $r=$this-&gt;cache()-&gt;set('user_1_token', 'aaa', 7*24*3600);//写入token，并设置7天后过期
        $r=$this-&gt;cache()-&gt;get('user_1_token');//取出</code></pre>
<h2>限流</h2>
<p>详见 <a href="http://www.cuiwei.net/p/1203489253">http://www.cuiwei.net/p/1203489253</a></p>
<h2>分布式锁（互斥锁）</h2>
<p>如今都是分布式的环境下java自带的单体锁已经不适用的。在 Redis 2.6.12 版本开始，string的set命令增加了一些参数：</p>
<ul>
<li>EX：设置键的过期时间（单位为秒）</li>
<li>PX：设置键的过期时间（单位为毫秒）</li>
<li>NX ：只在键不存在时，才对键进行设置操作。 SET key value NX 效果等同于 SETNX key value 。</li>
<li>XX ：只在键已经存在时，才对键进行设置操作。</li>
</ul>
<p>由于这个操作是原子性的，可以简单地以此实现一个分布式的锁，例如：</p>
<pre><code>set lock_key locked NX EX 1 </code></pre>
<p>如果这个操作返回false，说明 key 的添加不成功，也就是当前有人在占用这把锁。而如果返回true，则说明得了锁，便可以继续进行操作，并且在操作后通过del命令释放掉锁。并且即使程序因为某些原因并没有释放锁，由于设置了过期时间，该锁也会在 1 秒后自动释放，不会影响到其他程序的运行。</p>
<p>示例代码</p>
<pre><code>        $name=$request-&gt;get('name');
        $lockKey='withdraw_lock_11111111';

        //正常逻辑判断，如标题已存在 等。。

        //上锁
        if ($this-&gt;cache()-&gt;setnx($lockKey, 'withdraw')) {
            $this-&gt;cache()-&gt;expire($lockKey, 3);//3秒，防止处理中断，3秒自动解锁
            // 执行业务逻辑

            sleep(1);

            //业务处理完毕，解锁
            $this-&gt;cache()-&gt;del($lockKey);
            return $this-&gt;successResult(0, '', ['name'=&gt;$name]);
        } else {
            return $this-&gt;failResult(400, '手速太快了，休息一下吧！');
        }</code></pre>
<p>curl并发测试</p>
<pre><code>curl -Z http://test.cw.net/api/test?name=1 http://test.cw.net/api/test?name=2</code></pre>
<h2>缓存穿透</h2>
<p>指查询一个一定不存在的数据，由于缓存是不命中时需要从数据库查询，</p>
<p>方案：如果查询数据库为空，我们可以给缓存设置个空值，或者默认值。但是如有有写请求进来的话，需要更新缓存哈，以保证缓存一致性，同时，最后给缓存设置适当的过期时间。</p>
<h2>缓存雪崩</h2>
<p>指缓存中数据大批量到过期时间，而查询数据量巨大，引起数据库压力过大甚至down机。</p>
<h2>缓存击穿</h2>
<p>指热点key在某个时间点过期的时候，而恰好在这个时间点对这个Key有大量的并发请求过来，从而大量的请求打到db。</p>
<p>有些文章认为它俩区别，是区别在于击穿针对某一热点key缓存，雪崩则是很多key。</p>
<p>互斥锁方案：抢到锁了才能查数据库，否则只能重试查询缓存</p>
<h2>参考</h2>
<p><a href="https://blog.csdn.net/agonie201218/article/details/123640871">https://blog.csdn.net/agonie201218/article/details/123640871</a></p>
<p><a href="https://blog.csdn.net/weixin_40205234/article/details/124614720">https://blog.csdn.net/weixin_40205234/article/details/124614720</a></p></div>]]></description>
            <guid isPermaLink="false">Redis 应用场景</guid>
        </item>
        <item>
            <title><![CDATA[php使用yield解决Fatal error: Allowed memory size of 134217728 bytes exhausted]]></title>
            <description><![CDATA[<link rel="stylesheet" href="https://www.cuiwei.net/static/css/github-markdown.min.css?t=20260430" type="text/css" media="screen" /><div class="markdown-body"><p>yield生成器允许你 在 foreach 代码块中写代码来迭代一组数据而不需要在内存中创建一个数组。</p>
<h2>错误还原</h2>
<pre><code>&lt;?php
$file = './access.log';
$lines=readfile2($file);

foreach($lines as $line){
    file_put_contents('access2.log', $line.PHP_EOL, FILE_APPEND);
}
echo 'ok'.PHP_EOL;

//试图读取一个248M的日志文件，将所有行放到一个数组里面并返回
function readFile2($path){
    $handle = fopen($path, "r");
    $lines=[];
    while (!feof($handle)) {
        $lines[]= fgets($handle);
    }
    fclose($handle);
    return $lines;
}</code></pre>
<p>结果</p>
<pre><code>Fatal error: Allowed memory size of 134217728 bytes exhausted (tried to allocate 12288 bytes) in /com.docker.devenvironments.code/test.php on line 14</code></pre>
<h2>解决办法</h2>
<p>这个时候你除了修改代码<code>ini_set('memory_limit', '500M')</code>，或者修改<code>php.ini</code>，你也可以使用 yield ，如下，修改一下 readFile2 函数</p>
<pre><code>function readFile2($path): iterable{
    $handle = fopen($path, "r");

    // $lines=[];
    while (!feof($handle)) {
        // $lines[]= fgets($handle);
        yield fgets($handle);
    }
    fclose($handle);
    // return $lines;
}</code></pre></div>]]></description>
            <guid isPermaLink="false">php使用yield解决Fatal error: Allowed memory size of 134217728 bytes exhausted</guid>
        </item>
        <item>
            <title><![CDATA[容器化的LNMP环境，如何升级PHP到8.1.9]]></title>
            <description><![CDATA[<link rel="stylesheet" href="https://www.cuiwei.net/static/css/github-markdown.min.css?t=20260430" type="text/css" media="screen" /><div class="markdown-body"><h2>为什么要容器化</h2>
<p>提到docker你可能会想到k8s，想到分布式，想到高并发；那单机服务器，或者开发环境有没有必要上docker呢？我觉得有！以LNMP环境为例</p>
<h3>相比传统的LNMP环境搭建，docker有哪些优势？</h3>
<p>之前安装LNMP环境你可能会选择yum，或手动编译，或宝塔之类的；我不太喜欢宝塔之类的，因为给它们的权限太大了，如果有漏洞那是很危险的。再说yum和手动编译，这两个你都没法保证一次成功次次成功！！！比如你在本地安装好了，然后你用同样的步骤到服务上安装，有可能会失败，因为系统不一样！</p>
<p>docker 成功的解决了上面的问题，就是能做到<code>一次成功次次成功</code>。利用编排工具<code>docker-compose</code>，你不必记忆<code>docker run</code>的一堆参数，只需要通过几个命令就能很方便的管理一组容器。剩下的你只需要关注一个<code>docker-compose.yml</code>文件，不管什么时候用，用在哪里，都能一摸一样的还原出来</p>
<h2>PHP7.4升级到8.1.9</h2>
<p>容器化以后，升级也变的简单了，你只需要制作新的镜像，然后替换掉旧的镜像即可</p>
<h3>本地制作PHP8.1.9镜像及使用</h3>
<p>我的<code>Dockerfile</code>，里面有几个扩展，不需要可以去掉</p>
<pre><code>FROM php:8.1.9-fpm
RUN apt-get update &amp;&amp; apt-get install -y git procps inetutils-ping net-tools \
        libfreetype6-dev \
        libjpeg62-turbo-dev \
        libpng-dev \
        libzip-dev \
        libssl-dev \
        libcurl4-openssl-dev \
        libc-ares-dev \
    &amp;&amp; docker-php-ext-configure gd --with-freetype --with-jpeg \
    &amp;&amp; docker-php-ext-install -j$(nproc) gd \
    &amp;&amp; pecl install redis-5.3.7 mongodb-1.14.0 \
    &amp;&amp; pecl install -D 'enable-sockets="no" enable-openssl="yes" enable-http2="yes" enable-mysqlnd="yes" enable-swoole-json="no" enable-swoole-curl="yes" enable-cares="yes"' swoole-5.0.0 \
    &amp;&amp; docker-php-ext-install pdo pdo_mysql mysqli zip sockets \
    &amp;&amp; docker-php-ext-enable redis swoole mongodb \
    &amp;&amp; curl -sfL https://getcomposer.org/installer | php -- --install-dir=/usr/bin --filename=composer \
    &amp;&amp; chmod +x /usr/bin/composer \
    &amp;&amp; composer self-update 2.3.10 \
    &amp;&amp; composer config -g repo.packagist composer https://mirrors.aliyun.com/composer/</code></pre>
<p>构建镜像</p>
<pre><code>docker build -t registry.cn-hangzhou.aliyuncs.com/cuiw/php:v2-mongodb .  </code></pre>
<p>推送镜像（你需要先申请阿里云的容器镜像服务）</p>
<pre><code>docker push registry.cn-hangzhou.aliyuncs.com/cuiw/php:v2-mongodb </code></pre>
<p>使用镜像，以下是部分<code>docker-compose.yml</code></p>
<pre><code>version: '3'

networks:
  web-network:
...
  docker-php-fpm:
    image: registry.cn-hangzhou.aliyuncs.com/cuiw/php:v2-mongodb
    hostname: php-fpm
    restart: always
    tty: true
    volumes:
      - ./php-fpm/etc/php/php.ini:/usr/local/etc/php/php.ini
      - ./php-fpm/etc/php-fpm.d/docker.conf:/usr/local/etc/php-fpm.d/docker.conf
      - ./php-fpm/etc/php-fpm.d/www.conf:/usr/local/etc/php-fpm.d/www.conf
      - ../../PhpstormProjects:/var/www
      - ../log/php:/var/log/php
      - ../log/php-fpm:/var/log/php-fpm
    networks:
      - web-network
...</code></pre>
<blockquote>
<p>注意：php-7.4和php-8.1.9的配置文件有些许差异，建议比较后修改</p>
</blockquote>
<h3>将PHP8.1.9镜像同步到服务器</h3>
<p>好了，本地的PHP8.1.9镜像测试完没问题，就可以修改服务器上的<code>docker-compose.yml</code>文件了，修改完之后，按以下步骤进行</p>
<p>拉取镜像</p>
<pre><code>docker pull registry.cn-hangzhou.aliyuncs.com/cuiw/php:v2-mongodb</code></pre>
<p>停止并删除旧容器</p>
<pre><code>docker-compose down {容器id}</code></pre>
<p>启动新容器</p>
<pre><code>docker-compose up -d docker-php-fpm</code></pre>
<blockquote>
<p>美中不足，这个替换旧容器的过程不能做到无缝衔接。。。</p>
</blockquote></div>]]></description>
            <guid isPermaLink="false">容器化的LNMP环境，如何升级PHP到8.1.9</guid>
        </item>
        <item>
            <title><![CDATA[Google play 实时开发者通知——一次性购买]]></title>
            <description><![CDATA[<link rel="stylesheet" href="https://www.cuiwei.net/static/css/github-markdown.min.css?t=20260430" type="text/css" media="screen" /><div class="markdown-body"><p>若使用通知需要先配置，详见：<a href="http://www.cuiwei.net/p/1632593347/">http://www.cuiwei.net/p/1632593347/</a></p>
<p>实时开发者通知 有三种类型</p>
<ul>
<li>订阅购买 - SubscriptionNotification</li>
<li>一次性购买 - OneTimeProductNotification</li>
<li>play管理中心发出的测试消息 - TestNotification</li>
</ul>
<p>这篇文章只说 TestNotification和OneTimeProductNotification两种</p>
<h2>TestNotification</h2>
<p>这个没什么好说的，就是你配置完<code>实时开发者通知</code>，在play管理中心发出的测试通知</p>
<p><img src="https://www.cuiwei.net/data/upload/2022-07-12/165759404714467.jpg" alt="WX202207111714332x.png" /></p>
<h2>OneTimeProductNotification</h2>
<p>Google play将应用内商品购买称为一次性购买</p>
<table>
<thead>
<tr>
<th>属性名称</th>
<th>值</th>
<th>说明</th>
</tr>
</thead>
<tbody>
<tr>
<td>version</td>
<td>string</td>
<td>此通知的版本。最初，此值为“1.0”。此版本与其他版本字段不同。</td>
</tr>
<tr>
<td>notificationType</td>
<td>int</td>
<td>通知的类型。它可以具有以下值：(1) ONE_TIME_PRODUCT_PURCHASED - 用户成功购买了一次性商品。(2) ONE_TIME_PRODUCT_CANCELED - 用户已取消待处理的一次性商品购买交易。</td>
</tr>
<tr>
<td>purchaseToken</td>
<td>string</td>
<td>购买时向用户设备提供的令牌。</td>
</tr>
<tr>
<td>sku</td>
<td>string</td>
<td>购买的一次性商品的商品 ID（例如“sword_001”）。</td>
</tr>
</tbody>
</table>
<blockquote>
<p>注意：仅针对某些类型的一次性购买发送 OneTimeProductNotification。</p>
</blockquote>
<p>如上，官方只是说“仅针对某些类型的一次性购买发送”，很模糊；经过测试，只有“客户没有在规定的时间范围内完成付款”才会发送这种消息。也就是说，正常支付是收不到<code>OneTimeProductNotification</code>消息的1️⃣</p>
<h2>服务端需要做什么</h2>
<p>解析通知，得到3个参数：<code>packageName</code>，<code>productId</code>，<code>purchaseToken</code>，然后请求<code>Google Play Developer API</code>得到购买详情，判断是否购买，是否确认，没有确认就确认，已购买并且已确认就可以认为支付成功</p>
<p>如何配置Google Play Developer API，请参考 <a href="http://www.cuiwei.net/p/1370199631/">使用服务账号请求Google Play Developer API</a></p>
<pre><code>    /**
     * google play支付异步回调
     * 只有延迟支付才会通知
     */
    public function postPlayback() {
        $json=file_get_contents('php://input');
        $this-&gt;log($json);
        $arr= json_decode($json, true);
        $oneTimeProductNotification=base64_decode($arr['message']['data']);
        $arr['message']['data']=$oneTimeProductNotification;
        $this-&gt;log($arr);
        $notify=json_decode($oneTimeProductNotification, true);
        if ($notify['oneTimeProductNotification']['notificationType']==1){
            //notificationType: 1.用户成功购买了一次性商品 2.用户已取消待处理的一次性商品购买交易
            var_dump($notify['packageName'], $notify['oneTimeProductNotification']['sku'], $notify['oneTimeProductNotification']['purchaseToken']);
            $configLocation = $this-&gt;app-&gt;path.'/config/pc-api-***-797-ac21a2656c65.json';
//            echo file_get_contents($configLocation);exit;
            //将 JSON 设置环境变量
            putenv('GOOGLE_APPLICATION_CREDENTIALS='.$configLocation);
            try {
                $google_client = new \Google_Client();
                $google_client-&gt;useApplicationDefaultCredentials();
                $google_client-&gt;addScope(\Google_Service_AndroidPublisher::ANDROIDPUBLISHER);
                $androidPublishService = new \Google_Service_AndroidPublisher($google_client);
                $result = $androidPublishService-&gt;purchases_products-&gt;get(
                    $notify['packageName'],
                    $notify['oneTimeProductNotification']['sku'],
                    $notify['oneTimeProductNotification']['purchaseToken']
                );
//                echo json_encode($result, JSON_UNESCAPED_UNICODE);
                //purchaseState: 0.Purchased 1.Canceled 2.Pending
                //acknowledgementState: 0.Yet to be acknowledged 1.Acknowledged已确认
                //consumptionState: 0.Yet to be consumed 1.Consumed已消耗
                //已确认和已消耗的区别：
                //- 没有确认的，超时会自动退款
                //- 消耗，只能app端消耗，服务端操作不了。所以会出现已确认但未消耗的情况
                //- 消耗，会自动确认
                //- 未消耗，app端如果不处理，再次点击该sku会提示"您已经拥有此内容"，无法再次购买
                if($result-&gt;purchaseState==0){//已支付
                    if ($result-&gt;acknowledgementState == 0) {//未确认
                        $androidPublishService-&gt;purchases_products-&gt;acknowledge(
                            $notify['packageName'],
                            $notify['oneTimeProductNotification']['sku'],
                            $notify['oneTimeProductNotification']['purchaseToken'],
                            new \Google\Service\AndroidPublisher\ProductPurchasesAcknowledgeRequest((array)$result-&gt;toSimpleObject())
                        );
                    }
                    //支付成功发货的逻辑，同时标记该订单已经支付完成
                }
            }catch (Exception $exception){
                return show(0, $exception-&gt;getMessage());
            }
        }
    }</code></pre>
<p>⚠️注意，这里有个坑，purchaseState的值app端和服务端不一致！！</p>
<p>服务端：purchaseState: 0.Purchased 1.Canceled 2.Pending</p>
<p>详见：<a href="https://developers.google.cn/android-publisher/api-ref/rest/v3/purchases.products#ProductPurchase">https://developers.google.cn/android-publisher/api-ref/rest/v3/purchases.products#ProductPurchase</a></p>
<p>app端：Purchase.PurchaseState 0.UNSPECIFIED_STATE 1.PURCHASED 2.PENDING</p>
<p>详见：<a href="https://developer.android.google.cn/reference/com/android/billingclient/api/Purchase.PurchaseState">https://developer.android.google.cn/reference/com/android/billingclient/api/Purchase.PurchaseState</a></p>
<h2>参考</h2>
<p><a href="https://developer.android.com/google/play/billing/rtdn-reference#one-time">官方文档</a></p>
<p>1️⃣ 关于一次性购买收不到异步通知</p>
<p>网友收到谷歌的回复：</p>
<p>对于一次性购买，今天只为待定交易发送实时开发人员通知。“测试卡，始终批准”不是待定交易，这就是为什么今天没有发送通知。我们将努力在文档中更清楚地说明这一点。</p>
<p>是什么让所有这些实时开发人员通知变得毫无用处，因为您无法有一个地方始终如一地处理所有购买。</p>
<p><a href="https://stackoverflow.com/questions/65029988/android-real-time-developer-notification-for-one-time-purchase-is-half-working">https://stackoverflow.com/questions/65029988/android-real-time-developer-notification-for-one-time-purchase-is-half-working</a></p>
<p><a href="https://stackoverflow.com/questions/71037066/google-play-realtime-developer-notification-for-one-time-products-not-working">https://stackoverflow.com/questions/71037066/google-play-realtime-developer-notification-for-one-time-products-not-working</a></p></div>]]></description>
            <guid isPermaLink="false">Google play 实时开发者通知——一次性购买</guid>
        </item>
        <item>
            <title><![CDATA[google play配置实时开发者通知]]></title>
            <description><![CDATA[<link rel="stylesheet" href="https://www.cuiwei.net/static/css/github-markdown.min.css?t=20260430" type="text/css" media="screen" /><div class="markdown-body"><p>若要使用 Pub/Sub，您需有一个 Google Cloud 项目。</p>
<h2>创建主题</h2>
<p><img src="https://www.cuiwei.net/data/upload/2022-07-12/165761819182159.jpg" alt="WX202207121729312x.png" /></p>
<h3>设置权限</h3>
<p><img src="https://www.cuiwei.net/data/upload/2022-07-12/165762479142154.jpg" alt="WX202207121856522x.jpg" /></p>
<p>添加服务帐号 <code>google-play-developer-notifications@system.gserviceaccount.com</code>，然后授予其 <code>Pub/Sub 发布商</code>的角色。
<img src="https://www.cuiwei.net/data/upload/2022-07-12/165762494082327.jpg" alt="WX202207121921252x.png" /></p>
<h2>创建 Pub/Sub 订阅</h2>
<p><img src="https://www.cuiwei.net/data/upload/2022-07-12/165761842296404.jpg" alt="WX202207121732492x.png" /></p>
<p>传送类型选择推送，并提供一个接收post请求的链接，如下
<img src="https://www.cuiwei.net/data/upload/2022-07-12/165761863778126.jpg" alt="WX202207121736172x.png" /></p>
<h2>为您的应用启用实时开发者通知</h2>
<p><img src="https://www.cuiwei.net/data/upload/2022-07-12/165762529027590.jpg" alt="WX202207121927082x.jpg" /></p>
<p>如上，填写完主题名称，就可以点击“发送测试通知”，不出意外上面配置的<code>端点网址</code>就收到了</p>
<h2>参考</h2>
<p><a href="https://developer.android.com/google/play/billing/getting-ready#configure-rtdn">https://developer.android.com/google/play/billing/getting-ready#configure-rtdn</a></p>
<p><a href="https://cloud.google.com/pubsub/docs/push">https://cloud.google.com/pubsub/docs/push</a></p></div>]]></description>
            <guid isPermaLink="false">google play配置实时开发者通知</guid>
        </item>
        <item>
            <title><![CDATA[如何切换google play地区？]]></title>
            <description><![CDATA[<link rel="stylesheet" href="https://www.cuiwei.net/static/css/github-markdown.min.css?t=20260430" type="text/css" media="screen" /><div class="markdown-body"><h2>注册gmail</h2>
<p>首先我尝试使用电脑浏览器进行注册，结果提示“此电话号码无法用于进行验证”，无果。</p>
<p>后来我使用我的 Nexus 6 手机注册：设置-&gt;帐号-&gt;添加帐号-&gt;Google-&gt;创建帐号，成功注册。</p>
<h2>Google play 1</h2>
<p>然后，登陆Google play，发现底部只有游戏和应用；接着又试了几个app内购，发现不能内购</p>
<p><img src="https://www.cuiwei.net/data/upload/2022-07-11/165752551771097.jpg" alt="WX202207111543182x.jpg" /></p>
<h2>设置美国地址</h2>
<p>知道被锁区了，需要假装在美国</p>
<p>访问<a href="https://pay.google.com/gp/w/u/0/home/addressbook">链接</a>，添加一个美国地址，并设置为法定地址</p>
<h2>Google play 2</h2>
<p>设置完美国地址，再打开Google play，找到<code>设置</code>-&gt;<code>帐号和设备偏好设置</code>-&gt;<code>国家/地区和个人资料</code>，会看到<code>美国</code>，点击选择即可</p></div>]]></description>
            <guid isPermaLink="false">如何切换google play地区？</guid>
        </item>
        <item>
            <title><![CDATA[Android Studio多渠道打包之productFlavors]]></title>
            <description><![CDATA[<link rel="stylesheet" href="https://www.cuiwei.net/static/css/github-markdown.min.css?t=20260430" type="text/css" media="screen" /><div class="markdown-body"><p>我的app目前有两个渠道，Google play和官网，两者最大的差异是Google play对接了内购。所以需要有个方法能把两者区分开来，只有Google play渠道才显示内购相关的界面。这个方法就是打渠道包</p>
<h2>配置productFlavors</h2>
<p>修改<code>AndroidManifest.xml</code></p>
<pre><code>&lt;?xml version="1.0" encoding="utf-8"?&gt;
&lt;manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="net.cuiwei.voice"&gt;

    &lt;application
        android:theme="@style/AppTheme"&gt;

        &lt;meta-data android:name="CHANNEL" android:value="${CHANNEL_VALUE}" /&gt;

    &lt;/application&gt;

&lt;/manifest&gt;</code></pre>
<p>修改<code>build.gradle</code></p>
<pre><code>android {
    defaultConfig {
    ...
        //必须要保证所有的flavor 都属于同一个维度
        flavorDimensions "default"
    }
    ...
    productFlavors {
        guanwang {
            manifestPlaceholders = [CHANNEL_VALUE: "guanwang"]
        }
        google {
            manifestPlaceholders = [CHANNEL_VALUE: "google"]
        }
    }

}</code></pre>
<h2>指定调试模式使用的渠道</h2>
<p>有个问题，上面配置了多个渠道，那调试模式使用的是哪个渠道呢？</p>
<p><img src="https://www.cuiwei.net/data/upload/2022-07-10/165746455473851.jpg" alt="WX202207102247582x.png" /></p>
<p>其实是可以配置的，如下图</p>
<p><img src="https://www.cuiwei.net/data/upload/2022-07-10/165746475637054.jpg" alt="WX202207102252162x.png" /></p>
<blockquote>
<p>如果你如图切换了渠道没起作用，建议重启一下<code>Android Studio</code></p>
</blockquote></div>]]></description>
            <guid isPermaLink="false">Android Studio多渠道打包之productFlavors</guid>
        </item>
        <item>
            <title><![CDATA[使用服务账号请求Google Play Developer API]]></title>
            <description><![CDATA[<link rel="stylesheet" href="https://www.cuiwei.net/static/css/github-markdown.min.css?t=20260430" type="text/css" media="screen" /><div class="markdown-body"><p>Google Play 虽然提供了纯客户端的对接方案，但是官方也更推荐将敏感逻辑移至后端</p>
<p>Google Play Developer API 是一种服务器到服务器 API，与 Android 平台上的 Google Play 结算库相辅相成。此 API 提供了 Google Play 结算库中未提供的功能，如安全地验证购买交易以及为用户办理退款。</p>
<h2>配置 Google Play Developer API</h2>
<p>若要使用 Google Play Developer API，您需有一个 Google Cloud 项目。</p>
<h3>关联Google Cloud项目</h3>
<p><img src="https://www.cuiwei.net/data/upload/2022-07-09/165733313153848.jpg" alt="WX202207091017492x.jpg" /></p>
<p>如上图，你可以选择关联现有项目，也可以选择创建新项目</p>
<h4>关联现有项目</h4>
<p>选择现有项目前，需确认该项目开启了<code>Google Play Android Developer API</code></p>
<p><img src="https://www.cuiwei.net/data/upload/2022-07-09/165733374265630.jpg" alt="WX202207091028352x.png" /></p>
<p><img src="https://www.cuiwei.net/data/upload/2022-07-09/165733390365965.jpg" alt="WX202207091030072x.jpg" /></p>
<p><img src="https://www.cuiwei.net/data/upload/2022-07-09/165733391362812.jpg" alt="WX202207091030462x.png" /></p>
<h4>创建新项目</h4>
<p>创建新项目就方便了，系统会自动开启<code>Google Play Android Developer API</code></p>
<h3>在已关联Google Cloud项目中创建服务账号</h3>
<p><img src="https://www.cuiwei.net/data/upload/2022-07-09/165733442123255.jpg" alt="WX202207091037482x.png" /></p>
<p>要访问<code>Google Play Developer API</code>，你可以选择OAuth 客户端ID或服务帐号，这里推荐使用 服务帐号</p>
<p>创建一个服务帐户：</p>
<blockquote>
<p>点击add创建服务帐户。</p>
<p>在服务帐户的详细信息，键入一个名称，ID和服务帐户的描述，然后单击创建并继续。</p>
<p>可选：在授予此服务帐户访问到项目中，选择IAM角色授予服务帐户。(我理解应该是必选)</p>
<p>点击继续。</p>
<p>可选：在授予用户访问该服务帐户，添加允许使用和管理服务帐户的用户或组。(我理解也是可选，我没选)</p>
<p>点击完成。</p>
<p>点击add创建键，然后单击创建。</p>
</blockquote>
<p>在创建帐号的过程中，您需要向自己的服务帐号授予对 Google Cloud 项目的访问权限，这样它才能显示在 Google Play 管理中心内。</p>
<p><img src="https://www.cuiwei.net/data/upload/2022-07-09/165733603874173.jpg" alt="WX202207091106212x.jpg" /></p>
<p>如需使用 Google Play 结算服务 API，您必须授予以下权限：</p>
<ul>
<li>查看财务数据、订单和用户取消订阅时对调查问卷的书面回复</li>
<li>管理订单和订阅</li>
</ul>
<p><img src="https://www.cuiwei.net/data/upload/2022-07-09/165733588353438.jpg" alt="WX202207091100242x.jpg" /></p>
<p><img src="https://www.cuiwei.net/data/upload/2022-07-09/165733589667619.jpg" alt="WX202207091101352x.jpg" /></p>
<h4>为服务账号创建密钥</h4>
<p><img src="https://www.cuiwei.net/data/upload/2022-07-09/165733641330221.jpg" alt="WX202207091112112x.jpg" /></p>
<p>密钥创建成功，会提示你保存到本地，一个<code>pc-api-***-797-ac21a2656c65.json</code>文件，保存好，后面PHP要用</p>
<p>此时，您应该能够通过服务帐号访问 Google Play Developer API。</p>
<h2>PHP出场，这里应该有尖叫声～</h2>
<p>Google为PHP提供了库，安装</p>
<pre><code>composer require google/apiclient</code></pre>
<p>以请求<code>purchases.products.get</code>接口为例</p>
<pre><code>        $configLocation = $this-&gt;app-&gt;path.'/config/pc-api-***-797-ac21a2656c65.json';
//        echo file_get_contents($configLocation);exit;
        // 将 JSON 设置 环境变量
        putenv('GOOGLE_APPLICATION_CREDENTIALS='.$configLocation);
        //包名
        $package_name='net.cuiwei.voice';
        //商品ID
        $product_id='voice_0';
        //客户端传过来的 purchaseToken
        $purchase_token='facbncnplohikbjmf。。。';
        try {
            $google_client = new \Google_Client();
            $google_client-&gt;useApplicationDefaultCredentials();
            $google_client-&gt;addScope(\Google_Service_AndroidPublisher::ANDROIDPUBLISHER);
            $androidPublishService = new \Google_Service_AndroidPublisher($google_client);
            $result = $androidPublishService-&gt;purchases_products-&gt;get(
                $package_name,
                $product_id,
                $purchase_token
            );
            echo json_encode($result, JSON_UNESCAPED_UNICODE);exit;
            //更详细的代码请参考 http://www.cuiwei.net/p/1613838190/
        }catch (Exception $exception){
            return show(0, $exception-&gt;getMessage());
        }</code></pre>
<blockquote>
<p>注意：上述权限设置完不是立马生效的。我就是刚设置完，就使用PHP请求，结果提示401，然后各种找原因，未果；刚好到饭点，我就去吃饭了，等回来继续试，竟奇迹般的成功了。中间大概隔了1～2个小时</p>
</blockquote>
<h2>参考</h2>
<p><a href="https://developers.google.cn/android-publisher/getting_started">Google Play Developer API 使用入门</a></p>
<p><a href="https://developers.google.cn/android-publisher/getting_started#service-account">如何创建服务账号？</a></p>
<p><a href="https://developers.google.cn/identity/protocols/oauth2/service-account">将 OAuth 2.0 用于服务器到服务器应用</a></p>
<p><a href="https://developers.google.cn/android-publisher/api-ref/rest/v3/purchases.products/get">purchases.products.get接口文档</a></p>
<p><a href="https://googleapis.github.io/google-api-php-client/main/doc-index.html">Google APIs Client Library for PHP API Reference</a></p></div>]]></description>
            <guid isPermaLink="false">使用服务账号请求Google Play Developer API</guid>
        </item>
        <item>
            <title><![CDATA[google play 支付签名验证]]></title>
            <description><![CDATA[<link rel="stylesheet" href="https://www.cuiwei.net/static/css/github-markdown.min.css?t=20260430" type="text/css" media="screen" /><div class="markdown-body"><p>app端支付成功会有一个<code>Purchase</code>对象，里面有<code>购买令牌(purchaseToken)</code>和其他参数，如下</p>
<pre><code>JSONObject jsonObject=new JSONObject();
try {
    jsonObject.put("packageName", purchase.getPackageName());
    jsonObject.put("purchaseToken", purchase.getPurchaseToken());
    jsonObject.put("signature", purchase.getSignature());
    jsonObject.put("purchaseTime", purchase.getPurchaseTime());
    jsonObject.put("purchaseState", purchase.getPurchaseState());
    jsonObject.put("developerPayload", purchase.getDeveloperPayload());
//  jsonObject.put("accountIdentifiers", purchase.getAccountIdentifiers());
    jsonObject.put("orderId", purchase.getOrderId());
    jsonObject.put("originalJson", purchase.getOriginalJson());
    jsonObject.put("products", StringUtils.join(purchase.getProducts(), ","));
    jsonObject.put("quantity", purchase.getQuantity());
    jsonObject.put("isAutoRenewing", purchase.isAutoRenewing());
    jsonObject.put("isAcknowledged", purchase.isAcknowledged());
    Log.e("TAG", jsonObject.toString());
} catch (JSONException e) {
    e.printStackTrace();
}</code></pre>
<p>得到的json，如下</p>
<pre><code>{
    "packageName": "net.cuiwei.voice",
    "purchaseToken": "mjnmdjeccbcmeagmnfieahnd.AO-J1Oza5K7ZQVA。。",
    "signature": "BjEqq1T4NYMlIC\/SXXNgtX2UQRBh0kN。。",
    "purchaseTime": 1657271487378,
    "purchaseState": 1,
    "developerPayload": "",
    "orderId": "GPA.3349-0595-6867-76089",
    "originalJson": "{\"orderId\":\"GPA.3349-0595-6867-76089\",\"packageName\":\"net.cuiwei.voice\",\"productId\":\"voice_0\",\"purchaseTime\":1657271487378,\"purchaseState\":0,\"purchaseToken\":\"mjnmdjeccbcmeagmnfieahnd.AO-J1Oza5K7ZQVA。。",\"quantity\":1,\"acknowledged\":false}",
    "products": "voice_0",
    "quantity": 1,
    "isAutoRenewing": false,
    "isAcknowledged": false
}</code></pre>
<p>建议这些参数都上传给服务器。</p>
<p>作为服务端，我们知道客户端传过来的数据是可以伪造的，那么我们需要有一个验证签名的步骤</p>
<h3>验证签名</h3>
<p>验证签名需要三个参数</p>
<ul>
<li>originalJson</li>
<li>signature</li>
<li>google公钥</li>
</ul>
<p><img src="https://www.cuiwei.net/data/upload/2022-07-08/165727120116971.jpg" alt="WX202207081646222x.jpg" /></p>
<p>如上图可以取得<code>Google公钥</code></p>
<p>下面是PHP代码</p>
<pre><code>echo googlePayVerify('original_json...', 'signature...', 'google_public_key...').PHP_EOL;

/**
 * 谷歌支付签名验证
 * @param string $original_json
 * @param string $signature
 * @param string $google_public_key
 * @return bool
 */
function googlePayVerify(string $original_json, string $signature, string $google_public_key):bool {
    $public_key_handle = openssl_pkey_get_public($google_public_key);
    if($public_key_handle===false){
        $public_key = "-----BEGIN PUBLIC KEY-----" . PHP_EOL .
            chunk_split($google_public_key, 64, PHP_EOL) .
            "-----END PUBLIC KEY-----";
        $public_key_handle = openssl_pkey_get_public($public_key);
        if($public_key_handle===false) return false;
    }
    $result = openssl_verify($original_json, base64_decode($signature), $public_key_handle, OPENSSL_ALGO_SHA1);
    openssl_free_key($public_key_handle);
    return $result;
}</code></pre>
<h3>Google Play Developer API</h3>
<p>验证完签名，如果觉得不够，还可以通过<code>Google Play Developer API</code>查询购买详情，里面有购买状态，是否消耗，是否确认等更多信息，详见：<a href="http://www.cuiwei.net/p/1370199631/">http://www.cuiwei.net/p/1370199631/</a></p></div>]]></description>
            <guid isPermaLink="false">google play 支付签名验证</guid>
        </item>
        <item>
            <title><![CDATA[对接google play支付]]></title>
            <description><![CDATA[<link rel="stylesheet" href="https://www.cuiwei.net/static/css/github-markdown.min.css?t=20260430" type="text/css" media="screen" /><div class="markdown-body"><p>这里假如你已经有上架Google play的app，准备对接<code>应用内商品</code>（非<code>订阅</code>）</p>
<h2>设置 Google Play 开发者帐号</h2>
<p>在 <a href="https://pay.google.com/">Google 付款中心</a>设置付款资料</p>
<h2>在 Google Play 管理中心内启用结算相关功能</h2>
<p>设置开发者帐号后，您必须发布包含 Google Play 结算库的应用版本。如需在 Google Play 管理中心启用结算相关功能（如配置您要销售的商品），必须执行此步骤。</p>
<h3>添加库依赖项</h3>
<p>将依赖项添加到应用的 <code>build.gradle</code> 文件中，如下所示：</p>
<pre><code>dependencies {
    def billing_version = "5.0.0"

    implementation "com.android.billingclient:billing:$billing_version"
}</code></pre>
<h3>上传应用</h3>
<p>将该库添加到您的应用后，构建并发布您的应用。将其发布到任何轨道，包括内部测试轨道。</p>
<h2>创建和配置您的商品</h2>
<p><img src="https://www.cuiwei.net/data/upload/2022-07-07/165718021884531.jpg" alt="WX202207071549132x.png" /></p>
<h2>配置 Google Play Developer API</h2>
<p>详见：<a href="http://www.cuiwei.net/p/1370199631/">http://www.cuiwei.net/p/1370199631/</a></p>
<h2>配置实时开发者通知</h2>
<p>需要一个<code>Google Cloud 项目</code>，上一步创建过了，这里可以直接使用</p>
<p>详见：<a href="http://www.cuiwei.net/p/1632593347/">http://www.cuiwei.net/p/1632593347/</a></p>
<h2>参考</h2>
<p><a href="https://developer.android.com/google/play/billing/getting-ready">https://developer.android.com/google/play/billing/getting-ready</a></p>
<p><a href="https://support.google.com/googleplay/android-developer/answer/1153481">https://support.google.com/googleplay/android-developer/answer/1153481</a></p></div>]]></description>
            <guid isPermaLink="false">对接google play支付</guid>
        </item>
        <item>
            <title><![CDATA[根据srt字幕生成语音，并保持原有的时间间隔]]></title>
            <description><![CDATA[<link rel="stylesheet" href="https://www.cuiwei.net/static/css/github-markdown.min.css?t=20260430" type="text/css" media="screen" /><div class="markdown-body"><p>制作短视频时，配音是个麻烦事儿，比如我，我不想用自己的声音</p>
<p>下面介绍这个<a href="https://voice.cuiwei.net/download/">语音助手</a>可以很方便的实现 AI 配音</p>
<p>最近微软的“云希”火了，各大短视频平台上 讲故事的，影视剪辑的，配音都是用的“云希”，效果非常好。鉴于此，<a href="https://voice.cuiwei.net/download/">语音助手</a> 也使用了微软的 SDK，除了云希，还有十多种声音可以选择</p>
<h3>生成srt字幕</h3>
<p>如下图，点击按钮后开始说话，说完再次点击按钮即可生成字幕和语音，字幕可以分享到微信，也可以通过手机的文件管理器查看；语音是自己的声音，不想要可以不用理会。</p>
<p>假如，原创字幕文案准备好了，无声音的短视频也准备好了（在电脑上，或另一部手机上），我是这样生成srt字幕的：两只手，一只手按短视频的播放按钮，另一只手按 <code>语音助手</code> 的录音按钮(如下图)，注意，两只手尽量同时按下，避免生成的字幕和画面不同步。紧接着，根据你看到的短视频画面 读出你的文案即可（尽量使用普通话），这样srt字幕就生成好了</p>
<p><img src="https://www.cuiwei.net/data/upload/2022-07-02/165674236994628.jpg" alt="WX202206242120282x.jpg" /></p>
<h3>srt字幕转语音</h3>
<p>将上一步得到的srt字幕内容粘贴到下面的输入框，并选择自己喜欢的角色，就可以生成语音了
<img src="https://www.cuiwei.net/data/upload/2022-07-02/165674240824001.jpg" alt="WX202207012245272x.png" /></p>
<p>如下，点击“链接”或“二维码”，按照提示就可以下载语音了</p>
<p><img src="https://www.cuiwei.net/data/upload/2022-07-02/165674436097360.jpg" alt="WX202207021418282x.jpg" /></p>
<h3>结果</h3>
<p>无声音的视频有了，srt字幕有了，AI语音也有了，能把这三者组合到一起就完美了；我通常使用ks或bili的网页版剪辑 来做这个事</p>
<h3>更多</h3>
<p>更多 app 的操作视频：<a href="https://space.bilibili.com/388493354">https://space.bilibili.com/388493354</a></p></div>]]></description>
            <guid isPermaLink="false">根据srt字幕生成语音，并保持原有的时间间隔</guid>
        </item>
        <item>
            <title><![CDATA[语音助手 - 变声器、文字转语音、语音转文字、字幕翻译]]></title>
            <description><![CDATA[<link rel="stylesheet" href="https://www.cuiwei.net/static/css/github-markdown.min.css?t=20260430" type="text/css" media="screen" /><div class="markdown-body"><p>我的新作品《语音助手》上架Google Play了，欢迎下载体验</p>
<p>主要功能</p>
<blockquote>
<p>文字转语音：支持10多种声音选择；中英双语/ssml/srt字幕转语音；支持长文本</p>
<p>语音转文字：实时语音转文字，并支持导出SRT字幕，支持批量听录</p>
<p>语音翻译：译文实时输出，可导出 SRT 字幕</p>
<p>悬浮窗：生成语音后，可以在第三方app上方播放，以实现变声的效果</p>
<p>文字识别：采用OCR技术自动识别图片上的文字</p>
<p>我的：管理自己的文本及语音</p>
</blockquote>
<p><a href="https://voice.cuiwei.net/download/">下载链接</a></p>
<p><img src="https://www.cuiwei.net/data/upload/2022-08-08/165992927469683.jpg" alt="首页.jpg" /></p>
<p><img src="https://www.cuiwei.net/data/upload/2022-08-08/165992927455619.jpg" alt="变声器.jpg" /></p>
<p><img src="https://www.cuiwei.net/data/upload/2022-08-08/165992927435978.jpg" alt="文字转语音.jpg" /></p>
<p><img src="https://www.cuiwei.net/data/upload/2022-08-08/165992927461257.jpg" alt="语音转文字.jpg" /></p>
<p><img src="https://www.cuiwei.net/data/upload/2022-08-13/166038252893566.jpg" alt="语音翻译.jpg" /></p>
<p><img src="https://www.cuiwei.net/data/upload/2022-08-08/165992927412368.jpg" alt="我的.jpg" /></p>
<p><img src="https://www.cuiwei.net/data/upload/2022-08-08/165992927451146.jpg" alt="文本详情.jpg" /></p></div>]]></description>
            <guid isPermaLink="false">语音助手 - 变声器、文字转语音、语音转文字、字幕翻译</guid>
        </item>
        <item>
            <title><![CDATA[H5的音视频播放器 —— MediaElement.js]]></title>
            <description><![CDATA[<link rel="stylesheet" href="https://www.cuiwei.net/static/css/github-markdown.min.css?t=20260430" type="text/css" media="screen" /><div class="markdown-body"><p>首先，只需嵌入<code>&lt;audio&gt;</code>标签或<code>&lt;video&gt;</code>标签就可以实现媒体播放器。但是这样在不同浏览器下呈现的效果会有差异，为了让每个浏览器下都有一致的效果，我们选择了<a href="https://github.com/mediaelement/mediaelement">MediaElement.js</a></p>
<blockquote>
<p>mediaelement
HTML5 audio and video players in pure HTML and CSS. MediaElementPlayer.js uses the same HTML/CSS for all players.</p>
</blockquote>
<h1>使用方法</h1>
<p>简单的使用只需要引入两个文件，<a href="https://cdnjs.com/libraries/mediaelement">获取最新版本的文件</a></p>
<pre><code>&lt;script type="text/javascript" src="/static/js/mediaelement-and-player.min.js"&gt;&lt;/script&gt;
&lt;link rel="stylesheet" href="/static/css/mediaelementplayer.min.css"&gt;</code></pre>
<p>直接用他们的文件，几个图标可能显示不出来。打开<code>mediaelement-and-player.min.js</code>文件，找到<code>mejs-controls.svg</code>并将其替换为正确的路径，比如<code>/static/images/mejs-controls.svg</code></p>
<h3>在body中添加</h3>
<pre><code>&lt;audio id="audioPlayer"&gt;&lt;/audio&gt;</code></pre>
<h3>音频播放器</h3>
<pre><code>var player = new MediaElementPlayer('audioPlayer');
player.setSrc('sample.wav');
player.play();</code></pre>
<h3>视频播放器</h3>
<pre><code>var videoPlayer = new MediaElementPlayer('moviePlayer');
videoPlayer.setSrc('sample.mp4');
videoPlayer.play();</code></pre>
<h1>参考</h1>
<p><a href="https://techblog.recochoku.jp/8102">https://techblog.recochoku.jp/8102</a></p>
<p><a href="https://qiita.com/g-imai/items/dcc61f360703ed61cca7">https://qiita.com/g-imai/items/dcc61f360703ed61cca7</a></p></div>]]></description>
            <guid isPermaLink="false">H5的音视频播放器 —— MediaElement.js</guid>
        </item>
        <item>
            <title><![CDATA[Fragment中使用startActivityForResult]]></title>
            <description><![CDATA[<link rel="stylesheet" href="https://www.cuiwei.net/static/css/github-markdown.min.css?t=20260430" type="text/css" media="screen" /><div class="markdown-body"><p>在Fragment中使用startActivityForResult之后，onActivityResult的调用是从activity中开始的（即会先调用activity中的onActivityResult）。</p>
<p>一.只嵌套了一层Fragment（比如activity中使用了viewPager，viewPager中添加了几个Fragment）</p>
<p>在这种情况下要注意几个点：</p>
<p>1.在Fragment中使用startActivityForResult的时候，不要使用getActivity().startActivityForResult,而是应该直接使startActivityForResult()。</p>
<p>2.如果activity中重写了onActivityResult，那么activity中的onActivityResult一定要加上super.onActivityResult(requestCode, resultCode, data)。</p>
<p>如果违反了上面两种情况，那么onActivityResult只能够传递到activity中的，无法传递到Fragment中的。</p>
<p>没有违反上面两种情况的前提下，可以直接在Fragment中使用startActivityForResult和onActivityResult，和在activity中使用的一样。</p>
<p>二.嵌套多层Fragment（比如activity中使用了viewPager，viewPager中添加了几个Fragment，即第一层Fragment。其中一个Fragment又使用了一个ViewPager，这个ViewPager又加入了几个Fragment，即第二层Fragment）</p>
<p>在这种情况下activity中的onActivityResult调用无法传到第二层Fragment中。自己动手丰衣足食，我们只有手动传了。</p>
<p>来源：<a href="https://blog.csdn.net/generallizhong/article/details/102744319">https://blog.csdn.net/generallizhong/article/details/102744319</a></p></div>]]></description>
            <guid isPermaLink="false">Fragment中使用startActivityForResult</guid>
        </item>
        <item>
            <title><![CDATA[GridView某个单元格的选中状态受到键盘影响]]></title>
            <description><![CDATA[<link rel="stylesheet" href="https://www.cuiwei.net/static/css/github-markdown.min.css?t=20260430" type="text/css" media="screen" /><div class="markdown-body"><p>GridView如何设置某个单元格为选中状态？</p>
<p>首先，该组件自带的<code>gridview.setSelector(R.color.orange);</code>，可以设置选中；但如果页面上同时有输入控件，比如<code>EditText</code>，这时GridView的选中状态就会受到键盘影响，比如当前GridView的某个单元格为选中状态，拉起/收回 键盘，这个选中状态会自动取消，下面介绍一种方法：</p>
<pre><code>        gridview.setOnItemClickListener(new AdapterView.OnItemClickListener() {
            public void onItemClick(AdapterView&lt;?&gt; arg0, View arg1, int arg2, long arg3) {
                GridView gridView=(GridView) arg1.getParent();
                for (int i=0; i&lt;menuList.size(); ++i){
                    RelativeLayout relativeLayout=(RelativeLayout) gridView.getChildAt(i);
                    if (i==arg2){
                        relativeLayout.setBackgroundColor(Color.GREEN);
                    }else {
                        relativeLayout.setBackgroundColor(Color.WHITE);
                    }
                }
//                gridview.setSelector(R.color.orange);

            }
        });</code></pre></div>]]></description>
            <guid isPermaLink="false">GridView某个单元格的选中状态受到键盘影响</guid>
        </item>
        <item>
            <title><![CDATA[MediaPlayer播放音频文件]]></title>
            <description><![CDATA[<link rel="stylesheet" href="https://www.cuiwei.net/static/css/github-markdown.min.css?t=20260430" type="text/css" media="screen" /><div class="markdown-body"><h1>播放应用的资源文件</h1>
<pre><code>法1. 直接调用create函数实例化一个MediaPlayer对象，播放位于res/raw/test.mp3文件
MediaPlayer  mMediaPlayer = MediaPlayer.create(this, R.raw.test);

法2. test.mp3放在res/raw/目录下，使用setDataSource(Context context, Uri uri)
mp = new MediaPlayer(); 
Uri setDataSourceuri = Uri.parse("android.resource://com.android.sim/"+R.raw.test);
mp.setDataSource(this, uri);

说明：此种方法是通过res转换成uri然后调用setDataSource()方法，需要注意格式Uri.parse("android.resource://[应用程序包名Application package name]/"+R.raw.播放文件名);
例子中的包名为com.android.sim，播放文件名为：test;特别注意包名后的"/"。

法3. test.mp3文件放在assets目录下，使用setDataSource(FileDescriptor fd, long offset, long length)
AssetManager assetMg = this.getApplicationContext().getAssets();
AssetFileDescriptor fileDescriptor = assetMg.openFd("test.mp3");  
mp.setDataSource(fileDescriptor.getFileDescriptor(), fileDescriptor.getStartOffset(), fileDescriptor.getLength()); </code></pre>
<h1>播放存储设备的资源文件</h1>
<pre><code>MediaPlayer mediaPlayer = new MediaPlayer();  
mediaPlayer.setDataSource("/mnt/sdcard/test.mp3");</code></pre>
<h1>播放远程的资源文件</h1>
<pre><code>Uri uri = Uri.parse("http://**");  
MediaPlayer mediaPlayer = new MediaPlayer(); 
mediaPlayer.setDataSource(Context, uri);  </code></pre>
<p>来源：<a href="https://blog.csdn.net/yanlinembed/article/details/51887642">https://blog.csdn.net/yanlinembed/article/details/51887642</a></p></div>]]></description>
            <guid isPermaLink="false">MediaPlayer播放音频文件</guid>
        </item>
        <item>
            <title><![CDATA[SimpleAdapter加载网络图片]]></title>
            <description><![CDATA[<link rel="stylesheet" href="https://www.cuiwei.net/static/css/github-markdown.min.css?t=20260430" type="text/css" media="screen" /><div class="markdown-body"><p>要实现加载网络图片，需要使用SimpleAdapter中的<code>setViewBinder()</code>方法</p>
<pre><code>        SimpleAdapter simpleAdapter = new SimpleAdapter(this.getActivity(),
                menuList, //数据源
                R.layout.grid_item, //xml实现
                new String[]{"avatar", "name", "memo"}, //对应map的Key
                new int[]{R.id.avatar,R.id.name,R.id.memo});  //对应R的Id
        simpleAdapter.setViewBinder(new SimpleAdapter.ViewBinder() {
            @Override
            public boolean setViewValue(View view, Object data, String textRepresentation) {
                if (view instanceof ImageView) {
                    ImageView iv = (ImageView) view;
                    Glide.with(iv.getContext()).load("https://xxx.net/"+data).into(iv);
                    return true;
                }
                return false;
            }
        });</code></pre></div>]]></description>
            <guid isPermaLink="false">SimpleAdapter加载网络图片</guid>
        </item>
        <item>
            <title><![CDATA[跟踪代码管理器 —— Google Tag Manager]]></title>
            <description><![CDATA[<link rel="stylesheet" href="https://www.cuiwei.net/static/css/github-markdown.min.css?t=20260430" type="text/css" media="screen" /><div class="markdown-body"><p>平时网站上避免不了要嵌入第三方代码，比如</p>
<ul>
<li>对接 <code>百度统计</code></li>
<li>对接 <code>Google Analytics</code></li>
<li>对接 <code>Google AdSense</code></li>
<li>对接 <code>百度联盟</code></li>
<li>
<p>站长工具添加站点</p>
<p>等等...</p>
</li>
</ul>
<p>这些都需要在网页头部或底部添加代码，对接的多了，页面上会有很多这样的代码，</p>
<p><a href="https://tagmanager.google.com/">Google Tag Manager</a> 就是管理这些代码的，只需要在页面上添加<code>Google Tag Manager</code>的代码，以后想对接什么，直接在<code>Google Tag Manager</code>后台添加即可。</p>
<p><img src="https://www.cuiwei.net/data/upload/2022-06-01/165407392723433.jpg" alt="20220601165652.png" /></p></div>]]></description>
            <guid isPermaLink="false">跟踪代码管理器 —— Google Tag Manager</guid>
        </item>
        <item>
            <title><![CDATA[docker部署web自动化工具 —— selenium]]></title>
            <description><![CDATA[<link rel="stylesheet" href="https://www.cuiwei.net/static/css/github-markdown.min.css?t=20260430" type="text/css" media="screen" /><div class="markdown-body"><p>引用官方的一段话</p>
<blockquote>
<p>Selenium 是支持 web 浏览器自动化的一系列工具和库的综合项目。</p>
<p>它提供了扩展来模拟用户与浏览器的交互，用于扩展浏览器分配的分发服务器， 以及用于实现 W3C WebDriver 规范 的基础结构， 该 规范 允许您为所有主要 Web 浏览器编写可互换的代码。</p>
<p>Selenium 的核心是 <a href="https://www.selenium.dev/zh-cn/documentation/webdriver/">WebDriver</a>，这是一个编写指令集的接口，可以在许多浏览器中互换运行。</p>
</blockquote>
<h1>独立模式</h1>
<p>部分docker-compose</p>
<pre><code>  chrome:
#    selenium/standalone-firefox:4.1.4-20220427
#    selenium/standalone-edge:4.1.4-20220427
    image: selenium/standalone-chrome:4.1.4-20220427
    shm_size: 2gb
    container_name: standalone-chrome
    ports:
      - "4444:4444"
      - "7900:7900"
      - "5900:5900"
    environment:
      - VNC_VIEW_ONLY=1 #查看模式
      - VNC_NO_PASSWORD=1 #取消密码验证</code></pre>
<h1>Hub + Nodes模式</h1>
<p>部分docker-compose</p>
<pre><code>  selenium-hub:
    image: selenium/hub:4.1.4-20220427
    container_name: selenium-hub
    ports:
      - "4442:4442"
      - "4443:4443"
      - "4444:4444"

  chrome:
#    selenium/node-edge:4.1.4-20220427
#    selenium/node-firefox:4.1.4-20220427
    image: selenium/node-chrome:4.1.4-20220427
    shm_size: 2gb
    depends_on:
      - selenium-hub
    environment:
      - SE_EVENT_BUS_HOST=selenium-hub
      - SE_EVENT_BUS_PUBLISH_PORT=4442
      - SE_EVENT_BUS_SUBSCRIBE_PORT=4443
      - VNC_VIEW_ONLY=1 #查看模式
      - VNC_NO_PASSWORD=1 #取消密码验证
    ports:
      - "7900:7900"
      - "5900:5900"</code></pre>
<h1>视频录制</h1>
<p>可以使用<code>selenium/video:ffmpeg-4.3.1-20220427</code> Docker 映像记录测试执行情况。每个运行浏览器的容器都需要一个容器。</p>
<ul>
<li>不支持无头浏览器的视频录制。</li>
<li>停止并删除容器后，您应该会在机器的<code>./videos</code>目录上看到一个视频文件。</li>
</ul>
<p>部分docker-compose</p>
<pre><code>  chrome_video:
    image: selenium/video:ffmpeg-4.3.1-20220427
    volumes:
      - ./videos:/videos
    depends_on:
      - chrome
    environment:
      - DISPLAY_CONTAINER_NAME=chrome
      - FILE_NAME=chrome_video.mp4</code></pre>
<h1>启动服务</h1>
<pre><code>docker-compose up -d</code></pre>
<p>常用链接</p>
<pre><code>Hub 控制台
http://localhost:4444/grid/console

节点状态
http://localhost:4444/wd/hub/status

要查看容器内发生的情况，请访问
http://localhost:7900
密码：secret</code></pre>
<h1>调试</h1>
<p>该项目使用 <a href="https://github.com/LibVNC/x11vnc">x11vnc</a> 作为 VNC 服务器，以允许用户检查容器内发生的情况。用户可以通过两种方式连接到该服务器</p>
<p>如果您收到要求输入密码的提示，它是：<code>secret</code></p>
<h3>使用 VNC 客户端</h3>
<p><a href="https://www.realvnc.com/en/connect/download/viewer/">Download VNC Viewer</a></p>
<p>全平台支持：<code>Windows</code>、<code>macOS</code>、<code>Linux</code>、<code>Raspberry Pi</code>、<code>iOS</code>、<code>Android</code>、<code>Solaris</code>、<code>HP-UX</code>、<code>AIX</code></p>
<p>连接<code>localhost:5900</code>，如下</p>
<p><img src="https://www.cuiwei.net/data/upload/2022-05-27/165363393482368.jpg" alt="20220527144340.png" /></p>
<h3>使用浏览器（不需要 VNC 客户端）</h3>
<p>连接<code>http://localhost:7900/</code>，如下</p>
<p><img src="https://www.cuiwei.net/data/upload/2022-05-27/165363420512814.jpg" alt="20220527144936.png" /></p>
<h1>测试脚本</h1>
<p>Selenium 支持很多语言：<code>Java</code>、<code>Python</code>、<code>CSharp</code>、<code>Ruby</code>、<code>JavaScript</code>、<code>Kotlin</code></p>
<p>下面以<code>JavaScript</code>和<code>Python</code>为例，写一个<code>截屏</code>的小demo</p>
<p>详见：<a href="https://github.com/chudaozhe/docker-selenium/tree/master/examples">https://github.com/chudaozhe/docker-selenium/tree/master/examples</a></p>
<h1>代码</h1>
<p><a href="https://github.com/chudaozhe/docker-selenium">https://github.com/chudaozhe/docker-selenium</a></p>
<h1>参考</h1>
<p><a href="https://github.com/SeleniumHQ/docker-selenium">https://github.com/SeleniumHQ/docker-selenium</a></p>
<p>脚本例子 <a href="https://github.com/SeleniumHQ/seleniumhq.github.io/tree/trunk/examples">https://github.com/SeleniumHQ/seleniumhq.github.io/tree/trunk/examples</a></p>
<p><a href="https://www.selenium.dev/zh-cn/documentation/">https://www.selenium.dev/zh-cn/documentation/</a></p></div>]]></description>
            <guid isPermaLink="false">docker部署web自动化工具 —— selenium</guid>
        </item>
        <item>
            <title><![CDATA[Docker的两种安装方式]]></title>
            <description><![CDATA[<link rel="stylesheet" href="https://www.cuiwei.net/static/css/github-markdown.min.css?t=20260430" type="text/css" media="screen" /><div class="markdown-body"><p>这里给新手朋友介绍两种安装方式</p>
<h1>桌面</h1>
<p>有可视化界面的，推荐安装<code>Docker Desktop</code></p>
<p><code>Docker Desktop</code>支持<code>Windows</code>、<code>Linux</code>、<code>Mac</code>，有可视化界面，适合开发环境。这是最简单的安装方式，<a href="https://www.docker.com/products/docker-desktop/">下载 Docker Desktop</a>，下载完，双击，连连下一步即可完成安装</p>
<h1>服务器</h1>
<p>服务器一般是没有可视化界面的，需要手动安装<code>docker</code> 和 <code>docker-compose</code></p>
<p>以CentOS为例</p>
<h3>docker</h3>
<p>配置yum源</p>
<pre><code>vi /etc/yum.repos.d/docker-ce.repo

[docker-ce-stable]
name=Docker CE Stable - $basearch
baseurl=https://mirrors.aliyun.com/docker-ce/linux/centos/$releasever/$basearch/stable
enabled=1
gpgcheck=1
gpgkey=https://mirrors.aliyun.com/docker-ce/linux/centos/gpg</code></pre>
<p>安装，开机启动，启动</p>
<pre><code>yum install docker-ce
systemctl enable docker
service docker start</code></pre>
<h3>Docker Remote API</h3>
<p>如果需要被远程管理，需要开启<code>Docker Remote API</code></p>
<pre><code>vi /lib/systemd/system/docker.service
# ExecStart=/usr/bin/dockerd -H fd:// --containerd=/run/containerd/containerd.sock
ExecStart=/usr/bin/dockerd -H tcp://0.0.0.0:2375 -H unix://var/run/docker.sock</code></pre>
<p>注意，打开docker remote API存在风险，建议将0.0.0.0设置成指定IP</p>
<h3>docker-compose</h3>
<p><code>docker-compose</code> 是一个用于定义和运行多容器 <code>Docker</code> 应用程序的工具。有了它你不需要记忆<code>docker run</code>的一堆参数，只要一个<code>docker-compose.yml</code>文件加几个命令就可以了，如：</p>
<pre><code>docker-compose up -d ⬅️后台运行
docker-compose down ⬅️停止并删除`docker-compose.yml`中的所以容器，及network</code></pre>
<p>以安装v2.5.1版本为例</p>
<pre><code>#从github下载
curl -L "https://github.com/docker/compose/releases/download/v2.5.1/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose

#赋予可执行权限
chmod +x /usr/local/bin/docker-compose</code></pre>
<h1>验证</h1>
<pre><code>[root@iZbp1430s16l9piu268n8rZ blog]# docker -v
Docker version ***, build f0df350
[root@iZbp1430s16l9piu268n8rZ blog]# docker-compose -v
docker-compose version ***, build 5becea4c</code></pre>
<h1>参考</h1>
<p><a href="https://docs.docker.com/engine/install/centos/">https://docs.docker.com/engine/install/centos/</a></p>
<p><a href="https://docs.docker.com/compose/install/">https://docs.docker.com/compose/install/</a></p></div>]]></description>
            <guid isPermaLink="false">Docker的两种安装方式</guid>
        </item>
        <item>
            <title><![CDATA[CentOS挂载光盘作为本地YUM源]]></title>
            <description><![CDATA[<link rel="stylesheet" href="https://www.cuiwei.net/static/css/github-markdown.min.css?t=20260430" type="text/css" media="screen" /><div class="markdown-body"><p>前提：笔记本是CentOS，无法上网，可以插系统盘</p>
<p>编译安装<a href="http://www.cuiwei.net/p/1314028999">NTFS-3G</a>，需要gcc，系统盘里有，故将系统盘作为YUM本地仓库</p>
<p>配置如下</p>
<pre><code>cd /etc/yum.repos.d
mv CentOS-Base.repo CentOS-Base.repobak

vi iso.repo
[iso]
name = iso
enable = yes
gpgcheck = 0
baseurl = file:///media/CentOS_6.4_Final</code></pre>
<p>显示所有仓库</p>
<pre><code>yum repolist</code></pre>
<p>安装gcc</p>
<pre><code>yum install gcc</code></pre></div>]]></description>
            <guid isPermaLink="false">CentOS挂载光盘作为本地YUM源</guid>
        </item>
        <item>
            <title><![CDATA[Linux挂载ntfs数据盘]]></title>
            <description><![CDATA[<link rel="stylesheet" href="https://www.cuiwei.net/static/css/github-markdown.min.css?t=20260430" type="text/css" media="screen" /><div class="markdown-body"><p>NTFS-3G 是一款稳定、功能齐全、可移植、可读写的 NTFS 驱动程序，适用于 Linux、Android、macOS、FreeBSD 和其他操作系统。它提供对 Windows NTFS 文件系统的安全处理。<a href="https://github.com/tuxera/ntfs-3g">Github</a></p>
<h2>安装</h2>
<pre><code>wget https://download.tuxera.com/opensource/ntfs-3g_ntfsprogs-2017.3.23.tgz
tar -xvf ntfs-3g_ntfsprogs-2017.3.23.tgz
cd ntfs-3g*
./configure --prefix=/usr/local
make
make install</code></pre>
<h2>挂载</h2>
<pre><code>mkdir /mnt/windows1
#/dev/sda6是你要挂载发ntfs磁盘，可以通过磁盘管理软件查看
mount -t ntfs-3g /dev/sda6 /mnt/windows1

#如果进程被占用，如下命令可以查，查到后kill掉即可
fuser -m -u /dev/sda6</code></pre>
<h2>取消挂载</h2>
<pre><code>umount /mnt/windows1</code></pre></div>]]></description>
            <guid isPermaLink="false">Linux挂载ntfs数据盘</guid>
        </item>
        <item>
            <title><![CDATA[网站开启ipv6支持全过程]]></title>
            <description><![CDATA[<link rel="stylesheet" href="https://www.cuiwei.net/static/css/github-markdown.min.css?t=20260430" type="text/css" media="screen" /><div class="markdown-body"><p>ipv6 是未来的趋势，现在联通，电信，移动手机卡都支持ipv6，很多家用路由器也支持ipv6</p>
<p>本文用的阿里云ecs基本信息</p>
<table>
<thead>
<tr>
<th>CPU&amp;内存</th>
<th>实例规格</th>
<th>系统</th>
<th>费用</th>
<th>是否支持ipv6</th>
</tr>
</thead>
<tbody>
<tr>
<td>2核1G</td>
<td>ecs.t6-c2m1.large</td>
<td>CentOS 8.5 64位</td>
<td>约13+1元</td>
<td>是</td>
</tr>
</tbody>
</table>
<blockquote>
<p>当前最便宜的ipv6 ecs，一周大约13块，ipv6带宽1M是1元/天，所以我是临到期1天才买的ipv6带宽，做的测试</p>
</blockquote>
<h1>ecs配置</h1>
<h2>ecs支持ipv6</h2>
<p>购买ecs时最好是选择支持ipv6的</p>
<p>分配的 IPv6 地址默认为私网权限。如需公网访问，请前往 IPv6 网关单独购买公网带宽</p>
<p>普通带宽和IPv6公网带宽单独收费，IPv6公网带宽：1M每天0.96元</p>
<p>官方提供了<a href="https://help.aliyun.com/document_detail/108465.html">自动配置IPv6地址</a>的脚本<a href="https://ecs-image-utils.oss-cn-hangzhou.aliyuncs.com/ipv6/rhel/ecs-utils-ipv6?spm=a2c4g.11186623.0.0.e01c6b17x87eCV">ecs-util-ipv6（RHEL）</a>，执行一下就好了</p>
<h2>ecs不支持ipv6</h2>
<p>建议重新开一台支持ipv6的，因为<code>IPv6转换服务</code>太贵</p>
<h1>安全组配置</h1>
<p>开放<code>80</code>端口，授权对象为<code>0.0.0.0/0</code>和<code>::/0</code>，如果允许<code>ping6</code>，把<code>全部 ICMP(IPv6)</code>也打开</p>
<p><img src="https://www.cuiwei.net/data/upload/2022-05-22/165320648098267.jpg" alt="WX202205221601022x.png" /></p>
<h1>域名解析</h1>
<p><img src="https://www.cuiwei.net/data/upload/2022-05-22/165320633461369.jpg" alt="WX202205221557442x.png" /></p>
<h1>nginx配置</h1>
<pre><code>server {
    listen [::]:443 ssl http2 default ipv6only=on;
    listen [::]:80 default ipv6only=on;
    listen 443 ssl http2;
    listen 80;
    server_name ipv6.cuiwei.net;
    index index.html index.htm default.html;
    root        /usr/share/nginx/html;

    ssl_certificate   ssl/ipv6.cuiwei.net.pem;
    ssl_certificate_key  ssl/ipv6.cuiwei.net.key;
    include conf.d/ssl.conf;
}</code></pre>
<h1>测试</h1>
<pre><code>#ping一下 公网的ipv6，应该可以ping通
ping6 2408:4005:************b13c:e181</code></pre>
<p>浏览器访问 <a href="https://ipv6.cuiwei.net/">https://ipv6.cuiwei.net/</a> ，如下</p>
<p><img src="https://www.cuiwei.net/data/upload/2022-05-22/165321669880519.jpg" alt="WX202205221726572x.png" /></p>
<h1>docker配置</h1>
<p>docker也有ipv6的功能，需要手动开启</p>
<p>但是，经过测试发现，宿主机的ipv6，和docker的ipv6没有直接的关系</p>
<p>所以这部分无需配置，不影响使用</p>
<h1>参考</h1>
<p>安全组那里卡了很久，本机无法ping ipv6公网IP，最后<code>提交工单</code>，客服帮解决了</p></div>]]></description>
            <guid isPermaLink="false">网站开启ipv6支持全过程</guid>
        </item>
        <item>
            <title><![CDATA[docker部署青龙面板 —— 薅羊毛]]></title>
            <description><![CDATA[<link rel="stylesheet" href="https://www.cuiwei.net/static/css/github-markdown.min.css?t=20260430" type="text/css" media="screen" /><div class="markdown-body"><p>青龙是一个定时任务管理面板，支持typescript、javaScript、python3和shell。</p>
<p>配合一些脚本可以自动参与京东的一些活动，自动获得京豆</p>
<h1>安装docker</h1>
<p>详见：<a href="http://www.cuiwei.net/p/1896979883">Docker的两种安装方式</a></p>
<h1>部署</h1>
<p>docker-compose.yml</p>
<pre><code>version: '3'

networks:
  web-network:

services:
  docker-qinglong:
    image: "whyour/qinglong:2.12.2"
    hostname: qinglong
    restart: unless-stopped
    tty: true
    volumes:
      - ./qinglong/data:/ql/data
    ports:
      - "5700:5700"
    networks:
      - web-network</code></pre>
<p>启动服务</p>
<pre><code>docker-compose up -d</code></pre>
<p>访问</p>
<pre><code>http://localhost:5700</code></pre>
<p>用户名，密码在<code>./qinglong/data/config/auth.json</code></p>
<h1>设置</h1>
<h3>新建任务 - shufflewzc/faker2</h3>
<p><img src="https://www.cuiwei.net/data/upload/2022-05-20/165304252997768.jpg" alt="20220520182114.png" /></p>
<p>命令</p>
<pre><code>ql repo https://ghproxy.com/https://github.com/shufflewzc/faker2.git "jd_|jx_|gua_|jddj_|getJDCookie" "activity|backUp" "^jd[^_]|USER|ZooFaker_Necklace.js|JDJRValidator_Pure|sign_graphics_validate"</code></pre>
<p>定时规则</p>
<pre><code>1 0 * * *</code></pre>
<p>保存后，手动点一下<code>运行</code>。</p>
<blockquote>
<p>目前这个仓库(<code>github.com/shufflewzc/faker2.git</code>)的默认分支已经清空了，我是手动切换到master分支，然后下载zip包，接着解压后把代码复制到<code>./qinglong/data/repo/shufflewzc_faker2</code>，最后再点<code>运行</code>就可以了</p>
</blockquote>
<h3>设置京东cookie</h3>
<p>浏览器访问<code>https://m.jd.com/</code>，先获取cookie</p>
<p><img src="https://www.cuiwei.net/data/upload/2022-05-20/165304375698369.jpg" alt="20220520184748.png" /></p>
<p>创建名称为<code>JD_COOKIE</code>的环境变量</p>
<p><img src="https://www.cuiwei.net/data/upload/2022-05-20/165304467590323.jpg" alt="20220520190359.png" /></p>
<p>值的格式如下</p>
<pre><code>pt_key=xxx;pt_token=xxxx;pt_pin=xxxx</code></pre>
<h3>其他脚本库</h3>
<p><a href="https://github.com/6dylan6/jdpro">https://github.com/6dylan6/jdpro</a></p>
<h1>友情提示，请注意安全，不用不明js，app，exe！被偷回到解放前！！！</h1></div>]]></description>
            <guid isPermaLink="false">docker部署青龙面板 —— 薅羊毛</guid>
        </item>
        <item>
            <title><![CDATA[docker部署对话式AI工具包 —— Nvidia Nemo]]></title>
            <description><![CDATA[<link rel="stylesheet" href="https://www.cuiwei.net/static/css/github-markdown.min.css?t=20260430" type="text/css" media="screen" /><div class="markdown-body"><p>前几天看到一篇文章 <a href="https://developer.nvidia.com/zh-cn/blog/nvidia-nemo-speech-to-text/">使用 Nvidia Nemo —— 3行代码快速实现语音转文字的应用</a>，感觉还不错，就实践了一下</p>
<p>Nemo 是一个集成自动语音识别（ASR），自然语言处理（NLP），语音合成（TTS)的对话式AI工具包。</p>
<p>首先，找到Github <a href="https://github.com/NVIDIA/NeMo">NVIDIA/NeMo</a>，README里介绍了各种部署方法，实践中我选了docker部署</p>
<h1>docker部署</h1>
<p>值得一提的是<code>NeMo</code>代码中有<code>Dockerfile</code>文件，并且官方也给出了<code>build</code>命令：<code>DOCKER_BUILDKIT=1 docker build -f Dockerfile -t nemo:latest .</code>，不过大概率你是执行不成功的1️⃣，推荐直接用官方镜像</p>
<table>
<thead>
<tr>
<th>资源包</th>
<th>大小</th>
<th>备注</th>
</tr>
</thead>
<tbody>
<tr>
<td><a href="https://catalog.ngc.nvidia.com/orgs/nvidia/containers/nemo">nvcr.io/nvidia/nemo:22.03</a></td>
<td>7.11 GB（展开后16.5 GB）</td>
<td>官方镜像</td>
</tr>
<tr>
<td><a href="https://catalog.ngc.nvidia.com/orgs/nvidia/teams/nemo/models/stt_zh_citrinet_512">stt_zh_citrinet_512</a></td>
<td>142.89 MB</td>
<td>模型1，音频转文字</td>
</tr>
<tr>
<td><a href="https://catalog.ngc.nvidia.com/orgs/nvidia/teams/nemo/models/nmt_zh_en_transformer6x6">nmt_zh_en_transformer6x6</a></td>
<td>860.75 MB</td>
<td>模型2，中文转英文</td>
</tr>
</tbody>
</table>
<p>如果只是要运行视频中演示的代码，没有nvidia显卡也可以，如下</p>
<pre><code>docker run -it --rm -v --shm-size=16g -p 8888:8888 -p 6006:6006 --ulimit memlock=-1 --ulimit stack=67108864 nvcr.io/nvidia/nemo:22.03</code></pre>
<h1>使用</h1>
<p>进入容器后，执行<code>./start-jupyter.sh</code>，会在本地8888端口启动jupyter，通过浏览器访问<code>http://localhost:8888/</code>，要填token，token在控制台可以看到</p>
<p>视频中的代码</p>
<pre><code>import nemo

# 语言转文字
import nemo.collections.asr as nemo_asr

citrinet = nemo_asr.models.EncDecCTCModel.from_pretrained(model_name="stt_zh_citrinet_512")
mandarin_text=citrinet.transcribe(paths2audio_files=["/workspace/nemo/test.wav"])
print(mandarin_text)

# 文字转语音
import nemo.collections.nlp as nemo_nlp

nmt_model = nemo_nlp.models.MTEncDecModel.from_pretrained(model_name="nmt_zh_en_transformer6x6")
nmt_model.translate(mandarin_text)</code></pre>
<p>代码可以保存为<code>.py</code>文件在容器内执行，也可以通过jupyter在线执行</p>
<blockquote>
<p>代码中用到的两个模型无需手动下载，程序会自动下载并加载</p>
</blockquote>
<h1>关于docker使用GPU</h1>
<h3>硬件要求</h3>
<p>只能使用支持 cuda 的 nvidia 显卡，其他不行😭</p>
<p><a href="https://developer.nvidia.com/cuda-gpus">支持cuda 的显卡列表</a></p>
<p><a href="https://www.zhihu.com/question/267189142/answer/1013521287">macOS10.12（Sierra）之后的macOS不能使用NVIDIA显卡，不论外置内置。</a></p>
<p><a href="https://ask.zol.com.cn/q/2135200.html">早在2016年，MacBook Pro就不在使用Nvidia GPU了</a></p>
<h3>软件</h3>
<p><a href="https://www.cuiwei.net/p/1662030718">docker使用GPU</a></p>
<h1>问题</h1>
<p>1️⃣</p>
<h3>你可能遇到</h3>
<ol>
<li><code>archive.ubuntu.com</code>下面的包请求失败</li>
</ol>
<p>尝试修改<code>Dockerfile</code></p>
<pre><code>RUN sed -i'' 's/archive\.ubuntu\.com/us\.archive\.ubuntu\.com/' /etc/apt/sources.list</code></pre>
<p>结果不行，直接修改源</p>
<pre><code>#清华的源
ADD sources.list /etc/apt</code></pre>
<p>结果通过</p>
<ol start="2">
<li>github clone失败</li>
</ol>
<p>build过程中大概要clone 3个库，耗时长，并且很容易失败，挂代理也不行</p>
<h3>可行的办法</h3>
<p>下载<code>Nemo</code>的某个版本，提交到自己的github账号下</p>
<p>还原<code>Dockerfile</code>，然后只把<code>k2</code>相关的注释掉（会报错，暂时无解）</p>
<p>然后通过阿里云的<code>容器镜像服务</code>，在线构建镜像(记得要选择海外机)，可以顺利构建出一个7G多的镜像</p></div>]]></description>
            <guid isPermaLink="false">docker部署对话式AI工具包 —— Nvidia Nemo</guid>
        </item>
        <item>
            <title><![CDATA[python包和虚拟环境管理器 —— Conda]]></title>
            <description><![CDATA[<link rel="stylesheet" href="https://www.cuiwei.net/static/css/github-markdown.min.css?t=20260430" type="text/css" media="screen" /><div class="markdown-body"><p>之前介绍过 <a href="http://www.cuiwei.net/p/1295056725">python venv模块和virtualenv工具的使用</a>，今天这个<code>Conda</code>不仅能创建虚拟环境，还可以管理包依赖</p>
<p>Miniconda 是一个免费的 conda 最小安装程序。它是 Anaconda 的一个小型引导版本，仅​​包含 conda、Python、它们所依赖的包以及少量其他有用的包，包括 pip、zlib 和其他一些包</p>
<p>下载链接 <a href="https://conda.io/en/latest/miniconda.html">https://conda.io/en/latest/miniconda.html</a></p>
<pre><code>创建
$ conda create --name nemo python==3.8
激活
$ conda activate nemo
停用环境
$ conda deactivate</code></pre>
<p><a href="https://conda.io/projects/conda/en/latest/commands.html">https://conda.io/projects/conda/en/latest/commands.html</a></p></div>]]></description>
            <guid isPermaLink="false">python包和虚拟环境管理器 —— Conda</guid>
        </item>
        <item>
            <title><![CDATA[交互式笔记本 —— Jupyter Notebook]]></title>
            <description><![CDATA[<link rel="stylesheet" href="https://www.cuiwei.net/static/css/github-markdown.min.css?t=20260430" type="text/css" media="screen" /><div class="markdown-body"><p>最近看了个视频，讲的什么先不细说，重点是他用的文档很高级，代码和文本，图片混排，代码可以直接执行。如下图：</p>
<p><img src="https://www.cuiwei.net/data/upload/2022-05-18/165286242428360.jpg" alt="0450CCC4194E728DC95CEAB7EF3A8DB9E8F02F6BFE4.jpg" /></p>
<p>进一步了解得知，他用的是<code>Jupyter</code>，交互式笔记本，默认支持<code>python</code>，官网<code>https://jupyter.org</code></p>
<h2>安装</h2>
<h3>安装pip3</h3>
<pre><code>apt-get install python3-pip</code></pre>
<h3>pip</h3>
<p>可以用pip安装</p>
<pre><code>pip install jupyterlab</code></pre>
<p>运行</p>
<pre><code>jupyter lab --allow-root
# nohup jupyter lab --allow-root &amp;</code></pre>
<h3>docker-compose</h3>
<p>也可以用docker-compose</p>
<pre><code>version: '3'

networks:
  web-network:

services:
  docker-notebook:
    image: "jupyter/base-notebook:lab-3.6.2"
    hostname: base-notebook
    restart: always
    tty: true
    ports:
      - "8888:8888"
    networks:
      - web-network</code></pre>
<p>启动服务</p>
<pre><code>docker-compose up -d</code></pre>
<h2>访问</h2>
<pre><code>http://localhost:8888/</code></pre>
<p>如果是docker部署，初次访问会要求填写token，这个token可以在控制台（比如：docker logs ...）查看</p>
<h2>其他语言安装文档</h2>
<p><a href="https://www.cuiwei.net/p/1391718888">Jupyter Notebook 安装 PHP 内核</a></p>
<p><a href="https://www.cuiwei.net/p/1262003552">Jupyter Notebook 安装 GO 内核</a></p>
<h2>在线服务</h2>
<p>完整的Jupyter Notebook 环境</p>
<p><a href="https://mybinder.org/">https://mybinder.org/</a></p>
<p><a href="https://nbviewer.org/">https://nbviewer.org/</a> (只能预览？)</p>
<p><a href="https://notebooks.azure.com">https://notebooks.azure.com</a></p>
<h2>其他</h2>
<p>同类型的还有 <a href="https://colab.research.google.com/">Google Colab</a></p>
<p><a href="https://github.com/jupyter/jupyter/wiki/Jupyter-kernels">Jupyter支持的内核列表</a></p>
<p><a href="https://jupyter-docker-stacks.readthedocs.io/en/latest/using/selecting.html">docker镜像如何选择</a></p></div>]]></description>
            <guid isPermaLink="false">交互式笔记本 —— Jupyter Notebook</guid>
        </item>
        <item>
            <title><![CDATA[android 投屏工具 —— scrcpy]]></title>
            <description><![CDATA[<link rel="stylesheet" href="https://www.cuiwei.net/static/css/github-markdown.min.css?t=20260430" type="text/css" media="screen" /><div class="markdown-body"><p>scrcpy是Genymobile出品的投屏神奇，全平台支持，无需ROOT，有线和无线都支持</p>
<p>功能</p>
<ul>
<li>屏幕录制</li>
<li>镜像时关闭设备屏幕</li>
<li>双向复制粘贴</li>
<li>可配置显示质量</li>
<li>以设备屏幕作为摄像头(V4L2) (仅限 Linux)</li>
<li>模拟物理键盘 (HID) (仅限 Linux)</li>
<li>物理鼠标模拟 (HID) (仅限 Linux)</li>
<li>OTG模式 (仅限 Linux)</li>
</ul>
<h1>安装</h1>
<h3>Linux</h3>
<p>可以直接<code>apt install scrcpy</code>，也可以编译</p>
<pre><code># for Debian/Ubuntu
sudo apt install ffmpeg libsdl2-2.0-0 adb wget \
                 gcc git pkg-config meson ninja-build libsdl2-dev \
                 libavcodec-dev libavdevice-dev libavformat-dev libavutil-dev \
                 libusb-1.0-0 libusb-1.0-0-dev

git clone https://github.com/Genymobile/scrcpy
cd scrcpy
./install_release.sh

#卸载
sudo ninja -Cbuild-auto uninstall</code></pre>
<h3>Mac</h3>
<pre><code>brew install scrcpy</code></pre>
<h3>Windows</h3>
<p>可以直接下载对应的安装包，比如<code>scrcpy-win64-v1.24.zip</code></p>
<h1>使用</h1>
<h3>投屏</h3>
<pre><code>#使用，关闭手机屏幕，不影响投屏
scrcpy -Sw</code></pre>
<h3>屏幕录制</h3>
<pre><code>scrcpy --record file.mp4
scrcpy --no-display --record file.mp4</code></pre>
<p>注意：录制的视频是没有声音的，如果需要声音需要借助<a href="https://github.com/rom1v/sndcpy">sndcpy</a>。sndcpy要求android 10以上版本</p>
<p><a href="https://github.com/Genymobile/scrcpy/issues/14">https://github.com/Genymobile/scrcpy/issues/14</a></p>
<h1>参考</h1>
<p><a href="https://github.com/Genymobile/scrcpy/blob/master/BUILD.md#simple">https://github.com/Genymobile/scrcpy/blob/master/BUILD.md#simple</a></p>
<p><a href="https://github.com/Genymobile/scrcpy/blob/master/README.zh-Hans.md">https://github.com/Genymobile/scrcpy/blob/master/README.zh-Hans.md</a></p></div>]]></description>
            <guid isPermaLink="false">android 投屏工具 —— scrcpy</guid>
        </item>
        <item>
            <title><![CDATA[mqtt 轻量级 broker —— mosquitto]]></title>
            <description><![CDATA[<link rel="stylesheet" href="https://www.cuiwei.net/static/css/github-markdown.min.css?t=20260430" type="text/css" media="screen" /><div class="markdown-body"><p>mqtt broker 之前介绍了 <a href="http://www.cuiwei.net/p/1135009574">RabbitMQ插件之MQTT</a>，今天再介绍一个轻量级的 mosquitto，安装包<code>mosquitto-2.0.14.tar.gz</code>只有几百KB，官方的docker镜像也只有几M，非常小</p>
<p>引用一段官方的介绍</p>
<blockquote>
<p>Eclipse Mosquitto是一个开源（EPL/EDL许可）消息代理，实现了MQTT协议5.0、3.1.1和3.1版本。Mosquitto重量轻，适用于从低功耗单板计算机到全服务器的所有设备。
MQTT协议提供了一种使用发布/订阅模型进行消息传递的轻量级方法。这使得它适用于物联网消息，如低功耗传感器或移动设备，如手机、嵌入式计算机或微控制器。
Mosquitto项目还提供了一个用于实现MQTT客户端的C库，以及非常受欢迎的mosquitto_pub和mosquitto_sub命令行MQTT客户端。</p>
</blockquote>
<p>docker-compose</p>
<pre><code>version: '3'

networks:
  web-network:

services:
  docker-mosquitto:
    image: eclipse-mosquitto:2.0.14
    hostname: mosquitto
    restart: always
    tty: true
    volumes:
      - ./mosquitto/config:/mosquitto/config
      - ../log/mosquitto:/mosquitto/log
      - ../apps/mosquitto/data:/mosquitto/data
    ports:
      - 1883:1883
      - 8883:8883
      - 15675:15675
      - 15676:15676
    networks:
      - web-network

  # mosquitto的web管理后台
  # 通过http://localhost:8088/访问
  docker-management-center:
    image: cedalo/management-center:2.4.2
    restart: always
    environment:
      CEDALO_MC_BROKER_ID: mosquitto
      CEDALO_MC_BROKER_NAME: Mosquitto
      CEDALO_MC_BROKER_URL: mqtt://docker-mosquitto:1883
      CEDALO_MC_BROKER_USERNAME: cw
      CEDALO_MC_BROKER_PASSWORD: 123456
      CEDALO_MC_USERNAME: "admin"
      CEDALO_MC_PASSWORD: "12345"
    ports:
      - 8088:8088
    networks:
      - web-network</code></pre>
<h1>连接方式</h1>
<h2>mqtt</h2>
<p>适合后端使用</p>
<p>mqtt，需要配置</p>
<pre><code>tcp://localhost:1883</code></pre>
<p>mqtt + tls，需要配置</p>
<pre><code>ssl://localhost:8883</code></pre>
<h3>ws</h3>
<p>适合前端使用</p>
<p>ws，需要配置</p>
<pre><code>ws://localhost:15675</code></pre>
<p>wss，需要配置</p>
<pre><code>wss://localhost:15676</code></pre>
<h1>配置</h1>
<h3>创建用户</h3>
<p>安全起见，禁用匿名连接，先创建密码文件</p>
<pre><code>#创建密码文件
mosquitto_passwd -c /mosquitto/config/password_file cw</code></pre>
<p><code>config/mosquitto.conf</code>配置文件</p>
<pre><code>listener 1883

listener 8883
cafile /mosquitto/config/cert/ca.cer
certfile /mosquitto/config/cert/www.cuiwei.net.pem
keyfile /mosquitto/config/cert/www.cuiwei.net.key

listener 15675
protocol websockets

listener 15676
protocol websockets
cafile /mosquitto/config/cert/ca.cer
certfile /mosquitto/config/cert/www.cuiwei.net.pem
keyfile /mosquitto/config/cert/www.cuiwei.net.key

persistence true
persistence_location /mosquitto/data
#log_dest file /mosquitto/log/mosquitto.log
#禁止匿名连接
allow_anonymous false
#cw:123456
password_file /mosquitto/config/password_file

##启用动态安全
#plugin /usr/lib/mosquitto_dynamic_security.so
#plugin_opt_config_file /mosquitto/config/dynamic-security.json</code></pre>
<h1>命令行</h1>
<pre><code>mosquitto -v

订阅
mosquitto_sub -t test -u cw -P 123456

发布
mosquitto_pub -t test -m 123 -u cw -P 123456

订阅2
mosquitto_sub -v -t topic1

发布2
mosquitto_pub -t topic1 -m message1</code></pre>
<h1>其他</h1>
<p>关于调试工具，js库，php库，证书如何获取等，完全可以参考<a href="http://www.cuiwei.net/p/1135009574">RabbitMQ插件之MQTT
</a>，不再赘述。</p>
<h1>文档</h1>
<p><a href="https://docs.cedalo.com/mosquitto/2.0/installation">https://docs.cedalo.com/mosquitto/2.0/installation</a></p>
<p><a href="https://mosquitto.org/documentation/dynamic-security/">https://mosquitto.org/documentation/dynamic-security/</a></p></div>]]></description>
            <guid isPermaLink="false">mqtt 轻量级 broker —— mosquitto</guid>
        </item>
        <item>
            <title><![CDATA[基于 mqtt 的在线聊天系统]]></title>
            <description><![CDATA[<link rel="stylesheet" href="https://www.cuiwei.net/static/css/github-markdown.min.css?t=20260430" type="text/css" media="screen" /><div class="markdown-body"><p>一直以来都想给文章加个评论功能，这几天下定决心做这个事。</p>
<p>传统的评论功能也就那样，这次我想以聊天室的形式做，每篇文章都是一个聊天室，<code>article_id</code>即<code>room_id</code>，一个<code>room_id</code>就是一个<code>topic</code>，用户订阅这个<code>topic</code>，就可以实时收到信息，当然，用户也可以向这个<code>topic</code>发布信息</p>
<p>页面右下角有一个按钮，点击后可以看到历史消息和新消息（如果有的话），如果想发言，填写手机号，验证码登陆即可（登陆，注册合二为一）</p>
<h1>前端</h1>
<h3><code>UI</code>组件</h3>
<p>需要找一个好看的<code>UI</code>组件，最好是<code>Vue</code>的，因为<code>React</code>不熟悉😂</p>
<p>经过一番查找，发现 <a href="https://www.npmjs.com/package/vue-beautiful-chat">vue-beautiful-chat</a> 不错，就用它了</p>
<p>经过使用发现，他的消息格式和当前用户的逻辑不太符合预期，就改了一下，现在已发布到NPM，方便下载 <a href="https://www.npmjs.com/package/@chudaozhe/vue-beautiful-chat">@chudaozhe/vue-beautiful-chat</a></p>
<h3>A library for the MQTT protocol</h3>
<p><a href="https://www.npmjs.com/package/mqtt">mqttjs/MQTT.js</a></p>
<pre><code>yarn add mqtt</code></pre>
<h1>后端</h1>
<h3>mqtt broker选择</h3>
<p>mqtt是一种消息协议，要使用得选一个实现了这个协议的broker</p>
<ul>
<li>RabbitMQ + MQTT插件</li>
</ul>
<p>详见：<a href="https://www.cuiwei.net/p/1135009574">RabbitMQ插件之MQTT</a></p>
<ul>
<li>Mosquitto</li>
</ul>
<p>详见：<a href="http://www.cuiwei.net/p/1644092300">mqtt 轻量级 broker —— mosquitto</a></p>
<p>本次选择了：RabbitMQ + MQTT插件</p>
<h3>A library for the MQTT protocol</h3>
<p><a href="https://github.com/php-mqtt/client">php-mqtt/client</a></p>
<pre><code>composer require php-mqtt/client</code></pre></div>]]></description>
            <guid isPermaLink="false">基于 mqtt 的在线聊天系统</guid>
        </item>
        <item>
            <title><![CDATA[RabbitMQ新旧配置文件格式]]></title>
            <description><![CDATA[<link rel="stylesheet" href="https://www.cuiwei.net/static/css/github-markdown.min.css?t=20260430" type="text/css" media="screen" /><div class="markdown-body"><p>新的配置格式更简单，更易于人类阅读和机器生成。与 RabbitMQ 3.7.0 之前使用的经典配置格式相比，它也相对有限。例如，在配置 LDAP 支持时，可能需要使用深度嵌套的数据结构来表达所需的配置。为了满足这种需求，现代 RabbitMQ 版本允许在单独的文件中同时使用两种格式：</p>
<p>配置文件 rabbitmq.conf 允许配置 RabbitMQ 服务器和插件。从 RabbitMQ 3.7.0 开始，格式为 sysctl 格式。</p>
<p>主配置文件新，旧的变化</p>
<p>新</p>
<pre><code># 一种新的样式格式片段，rabbitmq.conf 文件使用这种格式。
ssl_options.cacertfile           = /path/to/ca_certificate.pem
ssl_options.certfile             = /path/to/server_certificate.pem
ssl_options.keyfile              = /path/to/server_key.pem
ssl_options.verify               = verify_peer
ssl_options.fail_if_no_peer_cert = true</code></pre>
<p>旧</p>
<pre><code>%% 经典格式片段，现在由 advanced.config 文件使用。
[
  {rabbit, [{ssl_options, [{cacertfile,           "/path/to/ca_certificate.pem"},
                           {certfile,             "/path/to/server_certificate.pem"},
                           {keyfile,              "/path/to/server_key.pem"},
                           {verify,               verify_peer},
                           {fail_if_no_peer_cert, true}]}]}
].</code></pre>
<p><a href="https://www.rabbitmq.com/configure.html">https://www.rabbitmq.com/configure.html</a></p></div>]]></description>
            <guid isPermaLink="false">RabbitMQ新旧配置文件格式</guid>
        </item>
        <item>
            <title><![CDATA[使用docker-compose快速部署RabbitMQ]]></title>
            <description><![CDATA[<link rel="stylesheet" href="https://www.cuiwei.net/static/css/github-markdown.min.css?t=20260430" type="text/css" media="screen" /><div class="markdown-body"><p>直接上配置文件</p>
<p>docker-compose.yml</p>
<pre><code>version: '3'

networks:
  web-network:

services:
  docker-rabbitmq:
    environment:
#      RABBITMQ_DEFAULT_VHOST: "/"
      RABBITMQ_DEFAULT_USER: "guest"
      RABBITMQ_DEFAULT_PASS: "guest"
    image: "rabbitmq:3.9.5-management"
    hostname: rabbitmq
    restart: always
    tty: true
    volumes:
      - ./rabbitmq/enabled_plugins:/etc/rabbitmq/enabled_plugins
      - ../apps/rabbitmq/data:/var/lib/rabbitmq
      - ../apps/rabbitmq/log:/var/log/rabbitmq
    ports:
      - 15670:15670
      - 15674:15674
      - 15672:15672
      - 5672:5672
      - 1883:1883
      - 15675:15675
      - 15676:15676
    networks:
      - web-network</code></pre>
<p>插件</p>
<pre><code>cat ./rabbitmq/enabled_plugins
[rabbitmq_management,rabbitmq_prometheus,rabbitmq_stomp,rabbitmq_web_stomp,rabbitmq_web_stomp_examples,rabbitmq_mqtt,rabbitmq_web_mqtt].</code></pre>
<ul>
<li>rabbitmq_management 管理后台1️⃣</li>
<li>rabbitmq_prometheus 监控插件，提供了对Prometheus指标收集的支持2️⃣</li>
<li>rabbitmq_stomp 3️⃣</li>
<li>rabbitmq_web_stomp 在Web应用程序中启用STOMP消息传递4️⃣</li>
<li>rabbitmq_web_stomp_examples 一些简单的Web STOMP示例4️⃣</li>
<li>rabbitmq_mqtt 5️⃣</li>
<li>rabbitmq_web_mqtt 在Web应用程序中启用MQTT消息传递6️⃣</li>
</ul>
<h1>启动服务</h1>
<pre><code>docker-compose up -d</code></pre>
<blockquote>
<p>注意，如果是Linux，第一次启动可能失败。提示<code>../apps/rabbitmq/log</code>目录没有写入权限，这时执行<code>chmod -R 777 log/</code>即可，另一个<code>data</code>目录无需处理</p>
</blockquote>
<h1>相关文档</h1>
<p>1️⃣<a href="https://www.rabbitmq.com/management.html">https://www.rabbitmq.com/management.html</a></p>
<p>2️⃣<a href="https://www.rabbitmq.com/prometheus.html">https://www.rabbitmq.com/prometheus.html</a></p>
<p>3️⃣<a href="https://www.rabbitmq.com/stomp.html">https://www.rabbitmq.com/stomp.html</a></p>
<p>4️⃣<a href="https://www.rabbitmq.com/web-stomp.html">https://www.rabbitmq.com/web-stomp.html</a></p>
<p>5️⃣<a href="https://www.rabbitmq.com/mqtt.html">https://www.rabbitmq.com/mqtt.html</a></p>
<p>6️⃣<a href="https://www.rabbitmq.com/web-mqtt.html">https://www.rabbitmq.com/web-mqtt.html</a></p></div>]]></description>
            <guid isPermaLink="false">使用docker-compose快速部署RabbitMQ</guid>
        </item>
        <item>
            <title><![CDATA[RabbitMQ插件之MQTT]]></title>
            <description><![CDATA[<link rel="stylesheet" href="https://www.cuiwei.net/static/css/github-markdown.min.css?t=20260430" type="text/css" media="screen" /><div class="markdown-body"><p>如何安装<code>rabbitmq</code>，请移步：<a href="http://www.cuiwei.net/p/1371869141">http://www.cuiwei.net/p/1371869141</a></p>
<p>启用mqtt插件</p>
<pre><code>vi enabled_plugins
[...,rabbitmq_mqtt,rabbitmq_web_mqtt].</code></pre>
<p>重启<code>rabbitmq</code>后，访问 <a href="http://localhost:15672/">RabbitMQ Management</a></p>
<p><img src="https://www.cuiwei.net/data/upload/2022-06-27/165631281042863.jpg" alt="WX202206271449532x.png" /></p>
<p>可以看到</p>
<ul>
<li>http/web-mqtt服务(ws)已经启动了，在15675端口上了</li>
<li>https/web-mqtt服务(wss)已经启动了，在15676端口上了</li>
<li>mqtt服务(tcp)已经启动了，在1883端口上</li>
<li>mqtt/ssl服务(ssl)已经启动了，在8883端口上</li>
</ul>
<h1>tcp/ssl</h1>
<p>tcp://localhost:1883</p>
<p>ssl://localhost:8883</p>
<pre><code>cat /etc/rabbitmq/conf.d/15-mqtt-ssl.conf
ssl_options.cacertfile = /etc/rabbitmq/cert/ca.cer
ssl_options.certfile   = /etc/rabbitmq/cert/www.cuiwei.net.pem
ssl_options.keyfile    = /etc/rabbitmq/cert/www.cuiwei.net.key
ssl_options.verify     = verify_peer
ssl_options.fail_if_no_peer_cert  = true

# default TLS-enabled port for MQTT connections
mqtt.listeners.ssl.default = 8883
mqtt.listeners.tcp.default = 1883</code></pre>
<h1>TLS (WSS)</h1>
<p>具体项目中，是使用<code>ws</code>，还是<code>wss</code>，取决于当前域名，如果当前域名是<code>https</code>，就只能使用<code>wss</code>，如果当前域名是<code>http</code>，就只能使用<code>ws</code></p>
<p>这个插件默认支持<code>ws</code>，直接用<code>ws://127.0.0.1:15675/ws</code>就行</p>
<p>wss需要一些配置才能使用<code>wss://127.0.0.1:15676/ws</code>1️⃣</p>
<pre><code>cat /etc/rabbitmq/conf.d/20-web-mqtt-ssl.conf
web_mqtt.ssl.port       = 15676
web_mqtt.ssl.backlog    = 1024
web_mqtt.ssl.cacertfile = /etc/rabbitmq/cert/ca.cer
web_mqtt.ssl.certfile   = /etc/rabbitmq/cert/www.cuiwei.net.pem
web_mqtt.ssl.keyfile    = /etc/rabbitmq/cert/www.cuiwei.net.key</code></pre>
<p>如上，用到3个文件，这些文件和配置https用的是一样的。</p>
<p>如果你用的是阿里云的免费证书（DV单域名证书）</p>
<ul>
<li>先选择<code>nginx</code>下载</li>
</ul>
<p>得到<code>.pem</code>和<code>.key</code>两个文件</p>
<ul>
<li>再选择<code>根证书下载</code></li>
</ul>
<p><img src="https://www.cuiwei.net/data/upload/2022-05-11/165223698058642.jpg" alt="WX202205101900472x.png" /></p>
<p>得到<code>Digicert-OV-DV-root.cer</code>文件，重命名为<code>ca.cer</code></p>
<h1>调试工具</h1>
<h3>MQTTBox</h3>
<p>多年未更新，mqtt+tls不支持</p>
<p>MQTTBox 是一个带有可视化的界面的 MQTT 的客户端工具</p>
<p>主要用来测试。下载链接已经打不开了，应用商店也搜索不到了，可能是多年没更新了，但可以直接打开应用商店链接</p>
<p><a href="https://chrome.google.com/webstore/detail/mqttbox/kaajoficamnjijhkeomgfljpicifbkaf">chrome 网上应用店</a></p>
<p>Github：<a href="https://github.com/workswithweb/MQTTBox">workswithweb/MQTTBox</a></p>
<h4>使用</h4>
<p>增加一个<code>mqtt client</code></p>
<p><img src="https://www.cuiwei.net/data/upload/2022-05-08/165200683929606.jpg" alt="WX202205081825142x.png" /></p>
<p>订阅和发布</p>
<p><img src="https://www.cuiwei.net/data/upload/2022-05-08/165200685271818.jpg" alt="WX202205081845332x.png" /></p>
<h3>MQTTX</h3>
<p>非常方便，4种连接都支持</p>
<p><img src="https://www.cuiwei.net/data/upload/2022-05-12/165236853384338.jpg" alt="WX202205122313192x.jpg" /></p>
<h1>JS</h1>
<p>如下，安装的是最新版，目前最新版是<code>4.3.7</code></p>
<pre><code>yarn add mqtt</code></pre>
<p>测试代码</p>
<pre><code>  mounted() {
    this.initMqtt()
    this.mqttReceive()
  },
  methods: {
    initMqtt() {
      let vm = this
      let commonApi = 'ws://localhost:15675/ws'
      const options = {
        //mqtt客户端的id
        clientId: 'client_' + Math.random().toString(16).substr(2, 8),
        username: 'guest',
        password: 'guest',
        clean: true,
        connectTimeout: 30 * 1000
      }
      vm.client = mqtt.connect(commonApi, options)
      this.client.on('connect', function () {
        console.log('连接成功....')
      })
      //如果连接错误，打印错误
      vm.client.on('error', function (err) {
        console.log('err=&gt;', err)
        vm.client.end()
      })
    },
    mqttReceive() {
      let topic = 'room_' + this.window.IM_ROOM_ID //要接收的主题
      const vm = this
      vm.client.subscribe(topic, function (err) {
        if (!err) {
          console.log('subscribe success!')
        } else {
          console.log('err', err)
        }
      })
      vm.client.on('message', function (topic, message) {
        console.log(message.toString())
        let msg = JSON.parse(message.toString())
        if (!vm.messageList.find((element, index, array) =&gt; element.id === msg.id)) {
          vm.messageList = [...vm.messageList, msg]
        }
      })
    },
    destroyed() {
      if (this.client.end) this.client.end()
    }
  }</code></pre>
<p>另一种，详见 <a href="https://juejin.cn/post/6979414392773279752">https://juejin.cn/post/6979414392773279752</a></p>
<h1>PHP</h1>
<p>如下，安装的是最新版，目前最新版是<code>1.1</code></p>
<pre><code>composer require php-mqtt/client</code></pre>
<p>测试代码</p>
<p><a href="https://github.com/chudaozhe/php-demo/tree/master/rabbitmq/mqtt">https://github.com/chudaozhe/php-demo/tree/master/rabbitmq/mqtt</a></p>
<h1>参考</h1>
<p>1️⃣如果没成功，可以参考下官方文档
<a href="https://www.rabbitmq.com/web-mqtt.html#tls">https://www.rabbitmq.com/web-mqtt.html#tls</a></p>
<p>其中一项是：在线解析证书</p>
<pre><code>cuiwei@weideMacBook-Pro cert %  openssl s_client -connect localhost:15676 -cert www.cuiwei.net.pem -key www.cuiwei.net.key -CAfile ca.cer
CONNECTED(00000005)
depth=2 C = US, O = DigiCert Inc, OU = www.digicert.com, CN = DigiCert Global Root CA
verify return:1
depth=1 C = US, O = DigiCert Inc, OU = www.digicert.com, CN = Encryption Everywhere DV TLS CA - G1
verify return:1
depth=0 CN = www.cuiwei.net
verify return:1
</code></pre></div>]]></description>
            <guid isPermaLink="false">RabbitMQ插件之MQTT</guid>
        </item>
        <item>
            <title><![CDATA[发布一个npm包]]></title>
            <description><![CDATA[<link rel="stylesheet" href="https://www.cuiwei.net/static/css/github-markdown.min.css?t=20260430" type="text/css" media="screen" /><div class="markdown-body"><p>先注册一个账号</p>
<p><a href="https://www.npmjs.com/signup">https://www.npmjs.com/signup</a></p>
<p>然后，在项目目录打开终端</p>
<p>登陆</p>
<pre><code>npm login</code></pre>
<p>这个当你的包名为@your-name/your-package时才会出现，原因是当包名以@your-name开头时，npm publish会默认发布为私有包，但是 npm 的私有包需要付费，所以需要添加如下参数进行发布:</p>
<pre><code>npm publish --access public</code></pre>
<p><a href="https://docs.npmjs.com/packages-and-modules/contributing-packages-to-the-registry/">https://docs.npmjs.com/packages-and-modules/contributing-packages-to-the-registry/</a></p></div>]]></description>
            <guid isPermaLink="false">发布一个npm包</guid>
        </item>
        <item>
            <title><![CDATA[adb 常用命令]]></title>
            <description><![CDATA[<link rel="stylesheet" href="https://www.cuiwei.net/static/css/github-markdown.min.css?t=20260430" type="text/css" media="screen" /><div class="markdown-body"><table>
<thead>
<tr>
<th>下载链接</th>
<th>备注</th>
</tr>
</thead>
<tbody>
<tr>
<td><a href="https://developer.android.google.cn/studio/releases/platform-tools.html">SDK Platform Tools</a></td>
<td>包含常用的<code>adb</code>和<code>fastboot</code></td>
</tr>
</tbody>
</table>
<h3>通过 Wi-Fi 连接到设备（Android 10 及更低版本）</h3>
<p>一般情况下，adb 通过 USB 与设备进行通信，但您也可以通过 Wi-Fi 使用 adb。如要连接到搭载 Android 10 或更低版本的设备，您必须通过 USB 执行一些初始步骤，如下所述：</p>
<ol>
<li>
<p>将 Android 设备和 adb 主机连接到这两者都可以访问的同一 Wi-Fi 网络。</p>
</li>
<li>
<p>如果您要连接到 Wear OS 设备，请关闭手机上与该设备配对的蓝牙。</p>
</li>
<li>
<p>使用 USB 线将设备连接到主机。</p>
</li>
<li>
<p>设置目标设备以监听端口 5555 上的 TCP/IP 连接。</p>
<pre><code>adb tcpip 5555</code></pre>
</li>
<li>
<p>拔掉连接目标设备的 USB 线。</p>
</li>
<li>
<p>找到 Android 设备的 IP 地址。例如，对于 Nexus 设备，您可以在设置 &gt; 关于平板电脑（或关于手机）&gt; 状态 &gt; IP 地址下找到 IP 地址。或者，对于 Wear OS 设备，您可以在设置 &gt; WLAN 设置 &gt; 高级 &gt; IP 地址下找到 IP 地址。</p>
</li>
<li>
<p>通过 IP 地址连接到设备。</p>
<pre><code>adb connect device_ip_address:5555</code></pre>
</li>
<li>
<p>确认主机已连接到目标设备：</p>
<pre><code>$ adb devices
List of devices attached
device_ip_address:5555 device</code></pre>
<p>现在，您可以开始操作了！</p>
</li>
</ol>
<p>如果 adb 连接断开：</p>
<ol>
<li>确保主机仍与 Android 设备连接到同一个 WLAN 网络。</li>
<li>通过再次执行 adb connect 步骤重新连接。</li>
<li>如果上述操作未解决问题，重置 adb 主机：</li>
</ol>
<pre><code>adb kill-server</code></pre>
<p>然后，从头开始操作。</p>
<h3>adb</h3>
<pre><code>安装文件到手机
adb install [-s 设备号] test.apk

推送文件到手机
adb push test.apk /mnt/sdcard/Download/test.apk

拉取手机里的文件(或文件夹)
adb pull /mnt/sdcard/Pictures/Screenshots .

查看日志
 adb logcat *:V |grep xiangle

查看系统信息（mod, mf…）
 adb shell cat /system/build.prop
 adb shell getprop
查看机器的序列号
adb shell getprop ro.serialno
型号
adb shell getprop ro.product.model
制造商
adb shell getprop ro.product.manufacturer

录屏，需要root（无声音）
sudo screenrecord --time-limit 10 --size 2160x3840 /sdcard/launch.mp4
--time-limit：录屏时长，默认180s
--size：视频分辨率</code></pre>
<h3>Monkey测试</h3>
<pre><code>monkey -p net.cuiwei.xiangle -v 500

1,执行 adb devices 确认与手机连接
如下，说明已连接
weis-MacBook-Pro:Downloads cuiw$ adb devices
List of devices attached
c91d54ba    device

2, 进入adb shell    
执行
2.1, adb shell    
2.2, monkey -s 0 -v -v -p net.cuiwei.xiangle --pct-trackball 0 --pct-nav 0 --throttle 300 1500000 &gt;/mnt/sdcard/Download/monkey.log</code></pre>
<h3>删除预装应用</h3>
<p>需要root的方法</p>
<pre><code>adb root
adb disable-verity
adb remount 重新挂载成可读可写
adb shell
cd system/priv-app/</code></pre>
<p>无需root的方法（仅仅是当前用户不显示，一切换用户又出来了）</p>
<pre><code>cuiwei@weideMacBook-Pro ~ % adb devices
List of devices attached
42743f80    device

cuiwei@weideMacBook-Pro ~ % adb shell

shamu:/ $ pm list packages | grep sheets
package:com.google.android.apps.docs.editors.sheets

shamu:/ $ pm uninstall --user 0 com.google.android.apps.docs.editors.sheets
Success</code></pre>
<h1>参考</h1>
<p><a href="https://developer.android.google.cn/studio/command-line/adb">https://developer.android.google.cn/studio/command-line/adb</a></p></div>]]></description>
            <guid isPermaLink="false">adb 常用命令</guid>
        </item>
        <item>
            <title><![CDATA[API 请求签名生成规则]]></title>
            <description><![CDATA[<link rel="stylesheet" href="https://www.cuiwei.net/static/css/github-markdown.min.css?t=20260430" type="text/css" media="screen" /><div class="markdown-body"><p>总结一下常见的 签名生成规则</p>
<h2>规则1</h2>
<h3>客户端</h3>
<p>每个 HTTP 请求中均需要携带以下的 HTTP 标头字段（HTTP Request Header）</p>
<table>
<thead>
<tr>
<th>默认名称</th>
<th>带 RC-前缀</th>
<th>类型</th>
<th>说明</th>
</tr>
</thead>
<tbody>
<tr>
<td>App-Key</td>
<td>RC-App-Key</td>
<td>String</td>
<td>后台分配的 App Key</td>
</tr>
<tr>
<td>Nonce</td>
<td>RC-Nonce</td>
<td>String</td>
<td>随机数，不超过 18 个字符</td>
</tr>
<tr>
<td>Timestamp</td>
<td>RC-Timestamp</td>
<td>String</td>
<td>时间戳，从1970年1月1日0点0分0秒开始到现在的毫秒数</td>
</tr>
<tr>
<td>Signature</td>
<td>RC-Signature</td>
<td>String</td>
<td>数据签名。您需要参考下文的签名计算方法生成该字段的值</td>
</tr>
</tbody>
</table>
<h4>签名计算方法</h4>
<p>将以下三个字符串按顺序（App Secret + Nonce + Timestamp）拼接成一个字符串，进行 SHA1 哈希计算。</p>
<ul>
<li>App Secret：应用 App Key 所对应的 App Secret。</li>
<li>Nonce：随机数</li>
<li>Timestamp：时间戳</li>
</ul>
<p>以下是 PHP 代码示例：</p>
<pre><code>$conf=['abc'=&gt;'defg'];//app_key=&gt;app_secret
//重置随机数种子。
srand((double)microtime()*1000000);
$appsecret = $conf['abc']; // App Secret
$nonce = rand(); // 获取随机数
$timestamp = time()*1000; // 获取时间戳（毫秒）
$header=[
    'RC-App-Key'=&gt;'abc',
    'RC-Nonce'=&gt;$nonce,
    'RC-Timestamp'=&gt;$timestamp,
    'RC-Signature'=&gt;sha1($appsecret.$nonce.$timestamp),
];</code></pre>
<h3>服务端</h3>
<pre><code>$conf=['abc'=&gt;'defg'];//app_key=&gt;app_secret
$appkey=$this-&gt;request-&gt;header('RC-App-Key', '', 'str');
$nonce=$this-&gt;request-&gt;header('RC-Nonce', '', 'str');
$timestamp=$this-&gt;request-&gt;header('RC-Timestamp', '', 'str');
$signature=$this-&gt;request-&gt;header('RC-Signature', '', 'str');

$dif =time() - $timestamp/1000;//±一分钟验证
if($dif &gt; 60 || $dif &lt;-60) die('timestamp error');
//验证签名
$appsecret = $conf[$appkey]??''; // App Secret
if (empty($appsecret)) die('appkey error');
if (sha1($appsecret.$nonce.$timestamp)!==$signature) die('signature error');

//删除1分钟前的记录
$time=time();
$this-&gt;cache()-&gt;zRemRangeByScore('nonces', 0, $time-60);
$r=$this-&gt;cache()-&gt;zScore('nonces', $nonce);
if (false===$r){
    $r=$this-&gt;cache()-&gt;zAdd('nonces', $time, $nonce);
    file_put_contents('./log.txt', $time.' - '.date('Y-m-d H:i:s').PHP_EOL, FILE_APPEND);
}else return $this-&gt;status(0, '无法重复请求');</code></pre>
<h2>规则2</h2>
<pre><code>$appkey='abc';//双方约定的key，不参与http请求，只用于计算签名
$sign=112233;//请求带的签名
$params=[
    'timestamp'=&gt;time(),
    'name'=&gt;111,
];
$params['appkey']=$appkey;
ksort($params);//数组key以字典顺序排序
$str='';
foreach ($params as $key=&gt;$value){
    $str.=$value;
}
//生成签名
$sign2=md5($str);</code></pre></div>]]></description>
            <guid isPermaLink="false">API 请求签名生成规则</guid>
        </item>
        <item>
            <title><![CDATA[绕过Android的SSL Pinning]]></title>
            <description><![CDATA[<link rel="stylesheet" href="https://www.cuiwei.net/static/css/github-markdown.min.css?t=20260430" type="text/css" media="screen" /><div class="markdown-body"><p>安卓 7 以后引入了 SSL Pinning ，最直接影响是：用户所安装的证书不再被系统信任，导致不能抓取 https 流量。</p>
<h1>解决办法</h1>
<h3>Magisk模块 —— Magisk Trust User Certs</h3>
<p><a href="https://github.com/NVISOsecurity/MagiskTrustUserCerts/releases/download/v0.4.1/AlwaysTrustUserCerts.zip">AlwaysTrustUserCerts.zip</a></p>
<p><img src="https://www.cuiwei.net/data/upload/2022-04-26/165098591125332.jpg" alt="Screenshot_20220426225426.jpg" /></p>
<h1>参考</h1>
<p><a href="https://blog.le31ei.top/2020/08/19/bypass-android-ssl-pinning/">https://blog.le31ei.top/2020/08/19/bypass-android-ssl-pinning/</a></p>
<p><a href="https://www.mrskye.cn/archives/dcfd805b/">https://www.mrskye.cn/archives/dcfd805b/</a></p>
<p><a href="https://www.cnblogs.com/yyoba/p/12370510.html">https://www.cnblogs.com/yyoba/p/12370510.html</a></p></div>]]></description>
            <guid isPermaLink="false">绕过Android的SSL Pinning</guid>
        </item>
        <item>
            <title><![CDATA[尝试给 nexus 6 手机 root]]></title>
            <description><![CDATA[<link rel="stylesheet" href="https://www.cuiwei.net/static/css/github-markdown.min.css?t=20260430" type="text/css" media="screen" /><div class="markdown-body"><p>由于机子比较老，市面上常见的工具都试过了，都无法root。经过几天折腾，终于取得root权限，现将整个过程整理出来</p>
<p>基本流程</p>
<p>先给手机解锁，然后找到与自己系统匹配的twrp，刷入系统，再通过twrp把Magisk刷入系统，最终取得root权限的是Magisk，后续哪个命令或app需要root权限，都需要向Magisk申请</p>
<table>
<thead>
<tr>
<th>软件</th>
<th>下载链接</th>
<th>备注</th>
</tr>
</thead>
<tbody>
<tr>
<td>SDK Platform Tools</td>
<td><a href="https://developer.android.google.cn/studio/releases/platform-tools.html">SDK Platform Tools</a></td>
<td>包含常用的<code>adb</code>和<code>fastboot</code></td>
</tr>
<tr>
<td>Google USB 驱动程序</td>
<td><a href="https://developer.android.google.cn/studio/run/win-usb?hl=zh-cn">Google USB 驱动程序</a></td>
<td>在 Windows 系统上对 Google 设备执行 adb 调试时必须安装</td>
</tr>
<tr>
<td>官方镜像</td>
<td><a href="https://dl.google.com/dl/android/aosp/shamu-n6f27m-factory-bf5cce08.zip">7.1.1 (N6F27M, Oct 2017)</a></td>
<td>官方镜像，避免手误，系统删了可以补救</td>
</tr>
<tr>
<td>TWRP</td>
<td><a href="https://dl.twrp.me/shamu/twrp-3.6.1_9-0-shamu.img">twrp-3.6.1_9-0-shamu.img</a></td>
<td>第三方recovery，用于安装Magisk</td>
</tr>
<tr>
<td>Magisk</td>
<td><a href="https://github.com/topjohnwu/Magisk/releases/download/v24.3/Magisk-v24.3.apk">Magisk-v24.3.apk</a></td>
<td>ROOT工具</td>
</tr>
</tbody>
</table>
<h1>解锁</h1>
<p>解OEM锁和bootloader锁</p>
<h3>解OEM锁</h3>
<p><img src="https://www.cuiwei.net/data/upload/2022-04-25/165087333919717.jpg" alt="WX202204251553092x.png" />
<img src="https://www.cuiwei.net/data/upload/2022-04-25/165087938288462.jpg" alt="WX202204251735482x.png" /></p>
<p>建议一直打开OEM解锁，这样你就可以随时进入bootloader了，能够进入bootloader就不怕变砖了。</p>
<h3>解BootLoader锁</h3>
<p>手机开机状态下，在电脑端执行adb命令进入bootloader模式（或者手机关机，通过一起按<code>音量减</code>键和<code>电源</code>键 进入）</p>
<pre><code>adb reboot bootloader</code></pre>
<blockquote>
<p>如果是Windows系统，并且无法连接到设备，可以先安装USB驱动</p>
</blockquote>
<p>手机端若显示如下界面，说明命令生效，按照提示，按下<code>电源</code>键即可进入bootloader模式</p>
<p><img src="https://www.cuiwei.net/data/upload/2022-04-25/165089441022454.jpg" alt="WX202204252142452x.jpg" /></p>
<p>进入bootloader模式后，电脑端执行以下命令，开始解锁</p>
<pre><code>fastboot flashing unlock</code></pre>
<p><img src="https://www.cuiwei.net/data/upload/2022-04-25/165087846661271.jpg" alt="WX202204251720372x.png" /></p>
<p>按照提示，按<code>音量加</code>确认，解锁完成，如下图</p>
<p><img src="https://www.cuiwei.net/data/upload/2022-04-25/165087867494168.jpg" alt="WX202204251724142x.png" /></p>
<h1>刷入TWRP</h1>
<p>手机进入bootloader模式</p>
<pre><code>#电脑端开始刷入twrp
cuiwei@weideMacBook-Pro Downloads % fastboot devices
42743f80    fastboot
cuiwei@weideMacBook-Pro Downloads % fastboot flash recovery twrp-3.6.1_9-0-shamu.img
(bootloader) has-slot:recovery: not found
(bootloader) is-logical:recovery: not found
Sending 'recovery' (12889 KB)                      OKAY [  0.540s]
Writing 'recovery'                                 OKAY [  0.361s]
Finished. Total time: 0.986s</code></pre>
<p>这就成功了</p>
<h1>通过 TWRP 刷入 Magisk</h1>
<p>twrp刷入成功后，手机切换到recovery模式，如下图</p>
<p><img src="https://www.cuiwei.net/data/upload/2022-04-25/165088159065343.jpg" alt="WX202204251812132x.jpg" /></p>
<p>按<code>电源</code>键确认，接着就看到twrp的界面了</p>
<p><img src="https://www.cuiwei.net/data/upload/2022-04-25/165088198867965.jpg" alt="1.jpg" />
<img src="https://www.cuiwei.net/data/upload/2022-04-25/165088204554564.jpg" alt="2.jpg" /></p>
<blockquote>
<p>看到输入密码，或只读之类的，直接忽略，不影响后续操作</p>
</blockquote>
<p>这时，通过adb把Magisk传到手机</p>
<pre><code>cuiwei@weideMacBook-Pro Downloads % adb push Magisk-v24.3.zip /sdcard</code></pre>
<p>接着，在twrp找到install，找到Magisk文件，就可以刷了</p>
<p><img src="https://www.cuiwei.net/data/upload/2022-04-25/165088281323060.jpg" alt="3.jpg" />
<img src="https://www.cuiwei.net/data/upload/2022-04-25/165088285228377.jpg" alt="4.jpg" />
<img src="https://www.cuiwei.net/data/upload/2022-04-25/165088287520954.jpg" alt="5.jpg" />
<img src="https://www.cuiwei.net/data/upload/2022-04-25/165088289429719.jpg" alt="6.jpg" /></p>
<p>刷完Magisk，重启手机，发现没有Magisk这个app，再通过手机浏览器手动下载 <a href="https://github.com/topjohnwu/Magisk/releases/download/v24.3/Magisk-v24.3.apk">Magisk-v24.3.apk</a> 安装一下就好了，不出意外，app已经有root权限了</p>
<p><img src="https://www.cuiwei.net/data/upload/2022-04-25/165088298831739.jpg" alt="thumbnail_Screenshot_20220425154954.png" /></p>
<h1>小插曲</h1>
<p>由于不太懂，一开始全凭感觉操作，结果把系统都清了，如下图几项都打勾清除了😂</p>
<p><img src="https://www.cuiwei.net/data/upload/2022-04-25/165088378546578.jpg" alt="7.jpg" /></p>
<p>结果系统就启不来了，还好有原厂镜像，执行以下步骤恢复</p>
<pre><code>cuiwei@weideMacBook-Pro Downloads % wget https://dl.google.com/dl/android/aosp/shamu-n6f27m-factory-bf5cce08.zip
cuiwei@weideMacBook-Pro Downloads % cd shamu-n6f27m
cuiwei@weideMacBook-Pro shamu-n6f27m % fastboot devices                                
42743f80    fastboot
cuiwei@weideMacBook-Pro shamu-n6f27m % fastboot erase cache
cuiwei@weideMacBook-Pro shamu-n6f27m % fastboot erase userdata
cuiwei@weideMacBook-Pro shamu-n6f27m % ./flash-all.sh</code></pre>
<h1>附录</h1>
<h3>官方镜像和工具</h3>
<p><code>Nexus</code>和<code>Pixel</code>设备的官方出厂镜像</p>
<p><a href="https://developers.google.cn/android/images">https://developers.google.cn/android/images</a></p>
<p>SDK Platform Tools（包含常用的<code>adb</code>和<code>fastboot</code>）</p>
<p><a href="https://developer.android.google.cn/studio/releases/platform-tools.html">https://developer.android.google.cn/studio/releases/platform-tools.html</a></p>
<p>获取 Google USB 驱动程序</p>
<p><a href="https://developer.android.google.cn/studio/run/win-usb?hl=zh-cn">https://developer.android.google.cn/studio/run/win-usb?hl=zh-cn</a></p>
<h3>TWRP</h3>
<p><a href="https://twrp.me/">https://twrp.me/</a></p>
<p><a href="https://twrp.me/motorola/motorolanexus6.html">https://twrp.me/motorola/motorolanexus6.html</a></p>
<p><a href="http://forum.xda-developers.com/showthread.php?t=1943625">http://forum.xda-developers.com/showthread.php?t=1943625</a></p>
<h3>Magisk</h3>
<p><a href="https://github.com/topjohnwu/Magisk">https://github.com/topjohnwu/Magisk</a></p>
<p><a href="https://topjohnwu.github.io/Magisk/install.html">https://topjohnwu.github.io/Magisk/install.html</a></p>
<p>v22.0及之后版本，只需下载1个apk文件</p>
<pre><code>Magisk-v24.3.apk

#如果需要卡刷，直接重命名为Magisk-v24.3.zip即可，然后再复制一份uninstall.zip用于卸载
#卡刷完成后，重新启动并检查是否安装了Magisk应用程序。如果它没有自动安装，请手动安装APK。</code></pre>
<p>v21及之前版本，需要下载两个文件</p>
<pre><code>Magisk-uninstaller-20210117.zip
Magisk-v21.4.zip</code></pre>
<h3>SuperSU（最高支持android 7.1，据说原作者已经不维护了，不推荐）</h3>
<p><a href="https://download.chainfire.eu/696/SuperSU/">https://download.chainfire.eu/696/SuperSU/</a></p>
<h1>参考</h1>
<p><a href="https://blog.csdn.net/m0_60352504/article/details/120072682">安卓手机刷入第三方recovery的两种方法</a></p>
<p><a href="https://blog.csdn.net/m0_60352504/article/details/120085697">卡刷supersu和magisk实现安卓手机获取root权限</a></p>
<p><a href="https://blog.csdn.net/elliottsilence/article/details/59064682">Nexus6 Android原生系统刷机方法</a></p>
<p><a href="https://blog.csdn.net/qq_39736559/article/details/122523399?utm_source=app&amp;app_version=5.3.0">Pixel2刷入原生Google系统并获取ROOT权限</a></p>
<p><a href="https://blog.csdn.net/yi_rui_jie/article/details/123114668">Google Nexus 6P手机刷机+升级+降级+Root详细教程</a></p></div>]]></description>
            <guid isPermaLink="false">尝试给 nexus 6 手机 root</guid>
        </item>
        <item>
            <title><![CDATA[HTTP代理服务器 - Charles]]></title>
            <description><![CDATA[<link rel="stylesheet" href="https://www.cuiwei.net/static/css/github-markdown.min.css?t=20260430" type="text/css" media="screen" /><div class="markdown-body"><h1>macOS 代理设置</h1>
<p><img src="https://www.cuiwei.net/data/upload/2022-04-22/165063229089346.jpg" alt="WX202204222056582x.png" /></p>
<p>安装并信任证书，为了捕获<code>macOS</code>的<code>https</code>流量</p>
<p><img src="https://www.cuiwei.net/data/upload/2022-04-22/165063274475491.jpg" alt="WX202204222105092x.png" />
<img src="https://www.cuiwei.net/data/upload/2022-04-22/165063279470756.jpg" alt="WX202204222101092x.png" /></p>
<p>要捕获哪个域名需要提前设置一下，如果嫌麻烦，可以把<code>host</code>和<code>port</code>都设置为<code>*</code></p>
<p><img src="https://www.cuiwei.net/data/upload/2022-04-22/165063380071499.jpg" alt="WX202204222123002x.png" /></p>
<h3>Map Remote功能介绍</h3>
<p>有时候我们开发一个功能，上线前需要测试一下<code>Android/iOS</code>端是否正常，又不想让他们改域名</p>
<p>举例说明，正式环境的域名是<code>www.cuiwei.net</code>，本地开发环境的域名为<code>blog.cw.net</code>，我们配置一下<code>Map Remote</code>，如下图</p>
<p><img src="https://www.cuiwei.net/data/upload/2022-04-23/165069117036607.jpg" alt="截屏20220423 13.09.31.png" />
<img src="https://www.cuiwei.net/data/upload/2022-04-23/165069119172751.jpg" alt="WX202204231309482x.png" /></p>
<p>配置完成，下面就是见证奇迹的时刻，地址栏我输入的是<code>www.cuiwei.net</code>，实际访问的却是本机的测试代码，完全符合预期🥳</p>
<p><img src="https://www.cuiwei.net/data/upload/2022-04-23/165069205829646.jpg" alt="IMG_3921.jpg" /></p>
<h1>iOS 设备设置</h1>
<p>代理端口8888，并勾上”Enable transparent HTTP proxying”</p>
<p><img src="https://www.cuiwei.net/data/upload/2022-04-22/165063463250376.jpg" alt="WX202204222130502x.png" /></p>
<p>在iOS设备上设置代理，并安装证书</p>
<p>先设置代理<code>192.168.10.4:8888</code>，然后访问<code>chls.pro/ssl</code>或<code>charlesproxy.com/getssl</code>安装证书</p>
<p><img src="https://www.cuiwei.net/data/upload/2022-04-22/165063513626493.jpg" alt="WX202204222142382x.png" />
<img src="https://www.cuiwei.net/data/upload/2022-04-22/165063514941137.jpg" alt="WX202204222143322x.png" /></p>
<p>设置代理</p>
<p><img src="https://www.cuiwei.net/data/upload/2022-04-22/165063588165390.jpg" alt="IMG_3915.jpg" />
<img src="https://www.cuiwei.net/data/upload/2022-04-22/165063589793882.jpg" alt="IMG_3916.jpg" /></p>
<p>安装证书，并信任</p>
<p><img src="https://www.cuiwei.net/data/upload/2022-04-22/165063621225093.jpg" alt="IMG_3917.jpg" />
<img src="https://www.cuiwei.net/data/upload/2022-04-22/165063622878968.jpg" alt="IMG_3918.jpg" />
<img src="https://www.cuiwei.net/data/upload/2022-04-22/165063624419494.jpg" alt="IMG_3920.jpg" /></p>
<p>如果这个设备是第一次连接，电脑上会提示是否允许xx IP连接，同意即可。如果误点了拒绝，也可以在<code>Access Control Settings</code> 找到，手动添加</p>
<p><img src="https://www.cuiwei.net/data/upload/2022-04-22/165063645347451.jpg" alt="WX202204222156062x.png" /></p>
<h1>参考</h1>
<p><a href="https://www.jianshu.com/p/d0a5e6986445">https://www.jianshu.com/p/d0a5e6986445</a></p>
<p><a href="https://www.charlesproxy.com">https://www.charlesproxy.com</a></p>
<p><a href="https://www.charlesproxy.com/documentation/configuration/browser-and-system-configuration/">https://www.charlesproxy.com/documentation/configuration/browser-and-system-configuration/</a></p></div>]]></description>
            <guid isPermaLink="false">HTTP代理服务器 - Charles</guid>
        </item>
        <item>
            <title><![CDATA[Android 逆向工具 - AndroidKiller]]></title>
            <description><![CDATA[<link rel="stylesheet" href="https://www.cuiwei.net/static/css/github-markdown.min.css?t=20260430" type="text/css" media="screen" /><div class="markdown-body"><p>Android Killer 是一款可视化的安卓应用逆向工具，集Apk反编译、Apk打包、Apk签名，编码互转，ADB通信（应用安装-卸载-运行-设备文件管理）等特色功能于一身，支持logcat日志输出，语法高亮，基于关键字（支持单行代码或多行代码段）项目内搜索，可自定义外部工具；吸收融汇多种工具功能与特点，打造一站式逆向工具操作体验，大大简化了安卓应用/游戏修改过程中各类繁琐工作。</p>
<p><img src="https://www.cuiwei.net/data/upload/2022-04-21/165052755631089.jpg" alt="WX202204211229002x.png" /></p>
<h1>前提</h1>
<p><code>Android Killer</code>仅支持<code>Windows</code>系统，在开始之前需要安装jdk，并设置环境变量</p>
<pre><code>JAVA_HOME：JDK的安装路径（如：C:\Program Files\Java\jdk1.8.0_311）

PATH：%JAVA_HOME%\bin</code></pre>
<h3><code>Android Killer</code> 集成了常用的反编译工具</h3>
<p>apktool</p>
<p><a href="https://github.com/iBotPeaches/Apktool">https://github.com/iBotPeaches/Apktool</a></p>
<p>dex2jar</p>
<p><a href="https://github.com/pxb1988/dex2jar">https://github.com/pxb1988/dex2jar</a></p>
<p>jd-gui</p>
<p><a href="https://github.com/java-decompiler/jd-gui">https://github.com/java-decompiler/jd-gui</a></p>
<p>JADX（未集成）</p>
<p><a href="https://github.com/skylot/jadx">https://github.com/skylot/jadx</a></p>
<h1>相关链接</h1>
<p>原始出处：<a href="https://www.pd521.com/forum-43-1.html">https://www.pd521.com/forum-43-1.html</a></p>
<p><a href="https://www.52pojie.cn/thread-319641-1-1.html">https://www.52pojie.cn/thread-319641-1-1.html</a></p>
<p><a href="https://www.52pojie.cn/thread-1400404-1-1.html">https://www.52pojie.cn/thread-1400404-1-1.html</a></p>
<p><a href="https://down.52pojie.cn/Tools/Android_Tools/">https://down.52pojie.cn/Tools/Android_Tools/</a></p></div>]]></description>
            <guid isPermaLink="false">Android 逆向工具 - AndroidKiller</guid>
        </item>
        <item>
            <title><![CDATA[fiddler 的使用]]></title>
            <description><![CDATA[<link rel="stylesheet" href="https://www.cuiwei.net/static/css/github-markdown.min.css?t=20260430" type="text/css" media="screen" /><div class="markdown-body"><h1>经典版</h1>
<p>仅支持<code>Windows</code></p>
<p><a href="https://www.telerik.com/fiddler/fiddler-classic">https://www.telerik.com/fiddler/fiddler-classic</a></p>
<h2>配置 <code>Fiddler Classic</code> 以解密 HTTPS 流量</h2>
<p><img src="https://www.cuiwei.net/data/upload/2022-04-20/165044860344527.jpg" alt="WX202204201754472x.png" />
<img src="https://www.cuiwei.net/data/upload/2022-04-20/165044861451172.jpg" alt="WX202204201755342x.png" /></p>
<blockquote>
<p>注意图中的<code>8866</code>端口，下文会用到</p>
</blockquote>
<h3>插件</h3>
<p>如上配置捕获web页面没问题，但遇到 APP 可能会报错：证书错误，或网络连接失败</p>
<p><img src="https://www.cuiwei.net/data/upload/2022-04-20/165044909485782.jpg" alt="IMG_3904.jpg" /></p>
<p>这时候需要安装插件<code>CertMaker for iOS and Android</code>
<a href="https://www.telerik.com/fiddler/add-ons">https://www.telerik.com/fiddler/add-ons</a></p>
<blockquote>
<p>注意，这个插件可以解决一些证书问题，并不是所有</p>
</blockquote>
<p>安装插件需要先关闭<code>fiddler</code>，安装完再打开，重置所有证书。其他端需要先清除之前的证书，再重新安装</p>
<h4>重置所有证书</h4>
<p><img src="https://www.cuiwei.net/data/upload/2022-04-20/165045734341267.jpg" alt="WX202204201756072x.png" /></p>
<h2>iOS 配置代理</h2>
<p>使用<code>Safari浏览器</code>访问<code>http://{Fiddler所在电脑的ip}:8866/</code></p>
<h4>下载描述文件</h4>
<p><img src="https://www.cuiwei.net/data/upload/2022-04-20/165044967215919.jpg" alt="WX202204201813562x.png" /></p>
<h4>安装描述文件</h4>
<p><img src="https://www.cuiwei.net/data/upload/2022-04-20/165045000744183.jpg" alt="IMG_3908.jpg" /></p>
<h4>信任证书</h4>
<p><img src="https://www.cuiwei.net/data/upload/2022-04-20/165045008251843.jpg" alt="IMG_3907.jpg" /></p>
<h4>设置代理</h4>
<p><img src="https://www.cuiwei.net/data/upload/2022-04-20/165045022970657.jpg" alt="IMG_3903.jpg" />
<img src="https://www.cuiwei.net/data/upload/2022-04-20/165045024672208.jpg" alt="IMG_3905.jpg" /></p>
<h2>Android 配置代理</h2>
<p>步骤也是先安装证书，再设置代理，不再细说</p>
<p>结果，结果不理想，即使是装上<code>CertMaker</code>插件，依然报证书错误，</p>
<p>引用网上的一段话</p>
<blockquote>
<p>7.0之后Android默认不相信用户自己安装的证书，需要程序自己实现是否相信用户证书。在此背景下，fiddler无法抓包https加密的报文，这种情况下解决办法只有两个，一个是不用android7.0以上的设备。。通常模拟器都是6.0目前，所以还可以。还有一个方法就是安装系统的根证书（需要Root权限）。</p>
</blockquote>
<h2>使用</h2>
<p>过滤host</p>
<p><img src="https://www.cuiwei.net/data/upload/2022-04-20/165045066273169.jpg" alt="WX202204201829552x.png" /></p>
<h1>新版</h1>
<p>支持<code>Windows</code>、<code>Linux</code>、<code>macOS</code></p>
<p>收费，提供30天免费试用</p>
<p>新版也有证书问题，好像没有相关插件能解决</p>
<p><a href="https://www.telerik.com/fiddler/fiddler-everywhere">https://www.telerik.com/fiddler/fiddler-everywhere</a></p></div>]]></description>
            <guid isPermaLink="false">fiddler 的使用</guid>
        </item>
        <item>
            <title><![CDATA[php 使用 protobuf]]></title>
            <description><![CDATA[<link rel="stylesheet" href="https://www.cuiwei.net/static/css/github-markdown.min.css?t=20260430" type="text/css" media="screen" /><div class="markdown-body"><p>协议缓冲区（Protocol Buffers）是一种语言中立、平台中立的可扩展机制，用于序列化结构化数据。</p>
<h1>安装</h1>
<pre><code>wget https://github.com/protocolbuffers/protobuf/releases/download/v3.20.0/protobuf-php-3.20.0.tar.gz
tar -zxvf protobuf-php-3.20.0.tar.gz
cd protobuf-3.20.0
./configure --prefix=/usr/local/protobuf
make
make install

#软链
ln -s /usr/local/protobuf/bin/protoc /usr/bin/</code></pre>
<h1>php extension and library</h1>
<p>extension 和 library 二选一</p>
<p>extension</p>
<pre><code>cd ext/google/protobuf
pear package
sudo pecl install protobuf-{VERSION}.tgz</code></pre>
<p>library</p>
<pre><code>composer require google/protobuf</code></pre>
<h1>测试</h1>
<pre><code>vi person.proto
syntax="proto3";
package test;
message Person{
    string name=1;//姓名
    int32  age=2;//年龄
    bool sex=3;//性别
}

#生成php代码
protoc --php_out=./ person.proto

#即
GPBMetadata/Person.php
Test/Person.php

# 使用
&lt;?php
require_once __DIR__ . '/../vendor/autoload.php';
include 'GPBMetadata/Person.php';
include 'Test/Person.php';

//序列化
//$person = new Test\Person();
//$person-&gt;setName("lailaiji");
//$person-&gt;setAge("28");
//$person-&gt;setSex(true);
//$data = $person-&gt;serializeToString();
//file_put_contents('data.bin', $data);

//反序列化
$bindata = file_get_contents('./data.bin');
$person = new Test\Person();
$person-&gt;mergeFromString($bindata);
echo $person-&gt;getName();</code></pre>
<h1>逆向推理</h1>
<p>假如我们拿到一个序列化后的数据，比如：<code>data.bin</code>，怎么解析呢?</p>
<p>首先，使用命令decode一下</p>
<pre><code>root@php-fpm:/var/www/php-demo/protobuf# protoc --decode_raw &lt; data.bin 
1: "\345\274\240\344\270\211"
2: 28
3: 1
4 {
  1: "\345\214\227\344\272\254"
  2: "\346\234\235\351\230\263"
  3: 100000
}</code></pre>
<p>我们大致知道哪个字段是字符串，哪个字段是数字，然后可以写出这样的proto文件</p>
<pre><code>syntax="proto3";
package test;
message Item {
    string field1=1;
    int32 field2=2;
    int32 field3=3;
    Obj field4=4;

    message Obj {
        string field1=1;
        string field2=2;
        int32 field3=3;
    }
}</code></pre>
<p>命名为<code>person.proto</code></p>
<p>接着，生成php代码</p>
<pre><code>protoc --php_out=./ person.proto</code></pre>
<p>接着，写反序列化代码</p>
<pre><code>&lt;?php
require_once __DIR__ . '/../../vendor/autoload.php';
include 'GPBMetadata/Person.php';
include 'Test/Item.php';
include 'Test/Item/Obj.php';

//反序列化
$bindata = file_get_contents('./data.bin');
$person = new Test\Item();
$person-&gt;mergeFromString($bindata);
echo $person-&gt;serializeToJsonString();</code></pre>
<p>最后，运行代码，输出</p>
<pre><code>{"field1":"张三","field2":28,"field3":1,"field4":{"field1":"北京","field2":"朝阳","field3":100000}}</code></pre>
<p>这就清晰多了，字段的值也看到了，再次修改<code>person.proto</code>文件，把字段改成更有意义的</p>
<h1>常用命令</h1>
<pre><code>protoc --php_out=./ person.proto

protoc --decode_raw &lt; data.bin </code></pre>
<h1>应用场景</h1>
<ul>
<li>直播平台的弹幕</li>
<li>用到json的地方都可以用，比如接口的响应数据</li>
</ul>
<h1>参考</h1>
<p><a href="https://www.jianshu.com/p/ce098058edf0">https://www.jianshu.com/p/ce098058edf0</a></p>
<p><a href="https://developers.google.com/protocol-buffers">https://developers.google.com/protocol-buffers</a></p>
<p><a href="https://github.com/protocolbuffers/protobuf/tree/main/php">https://github.com/protocolbuffers/protobuf/tree/main/php</a></p></div>]]></description>
            <guid isPermaLink="false">php 使用 protobuf</guid>
        </item>
        <item>
            <title><![CDATA[vmware 安装 android-x86]]></title>
            <description><![CDATA[<link rel="stylesheet" href="https://www.cuiwei.net/static/css/github-markdown.min.css?t=20260430" type="text/css" media="screen" /><div class="markdown-body"><p><img src="https://www.cuiwei.net/data/upload/2022-04-21/165054668859540.jpg" alt="WX202204212110362x.png" /></p>
<p>总结：国内很多app都闪退</p>
<h1>卡在console界面的解决办法</h1>
<h4>重启</h4>
<p><img src="https://www.cuiwei.net/data/upload/2022-04-21/165054202138160.jpg" alt="df4c44b301d147bd8ff956ad88b0d593.png" /></p>
<h4>选择调试模式</h4>
<p><img src="https://www.cuiwei.net/data/upload/2022-04-21/165054203975780.jpg" alt="e68db4a80612481fb891190e01bfe355.png" /></p>
<h4>以读写方式重新挂载目录/mnt</h4>
<p><code>mount -o remount,rw /mnt</code></p>
<p><img src="https://www.cuiwei.net/data/upload/2022-04-21/165054222014780.jpg" alt="5da7c365345e48808cdd477f27943a13.png" /></p>
<p>然后，编辑<code>/mnt/grub/menu.lst</code>文件</p>
<p>把<code>quiet</code>改成<code>nomodeset xforcevesa_</code>，保存后重启即可</p>
<p>编辑前</p>
<p><img src="https://www.cuiwei.net/data/upload/2022-04-21/165054235658316.jpg" alt="acda544adb7440caabd5128eedfd7c56.png" /></p>
<p>编辑后</p>
<p><img src="https://www.cuiwei.net/data/upload/2022-04-21/165054237093034.jpg" alt="28173740bfb34674ad309dedd7871d7f.png" /></p>
<h1>相关链接</h1>
<p><a href="https://blog.csdn.net/Iamzhouyd/article/details/122796439">https://blog.csdn.net/Iamzhouyd/article/details/122796439</a></p>
<p><a href="https://www.android-x86.org">https://www.android-x86.org</a></p></div>]]></description>
            <guid isPermaLink="false">vmware 安装 android-x86</guid>
        </item>
        <item>
            <title><![CDATA[iOS项目的依赖管理器 - CocoaPods]]></title>
            <description><![CDATA[<link rel="stylesheet" href="https://www.cuiwei.net/static/css/github-markdown.min.css?t=20260430" type="text/css" media="screen" /><div class="markdown-body"><p><code>CocoaPods</code>是<code>Swift</code>和<code>Objective-C</code> <code>Cocoa</code>项目的依赖管理器。类似 <code>PHP</code> 的 <code>composer</code>, <code>Java</code> 的 <code>Maven</code></p>
<h1>安装</h1>
<pre><code>$ brew install cocoapods</code></pre>
<p>或者</p>
<pre><code>$ sudo gem install cocoapods</code></pre>
<h3>加速镜像</h3>
<p><a href="https://mirrors.tuna.tsinghua.edu.cn/help/CocoaPods/">https://mirrors.tuna.tsinghua.edu.cn/help/CocoaPods/</a></p>
<pre><code>cd ~/.cocoapods/repos
#可能不需要移除
pod repo remove master
#很慢，最终master目录3.2G
pod repo add master https://mirrors.tuna.tsinghua.edu.cn/git/CocoaPods/Specs.git</code></pre>
<h1>使用</h1>
<p>在项目根目录创建文件<code>Podfile</code>，类似</p>
<pre><code>vi Podfile
# Uncomment the next line to define a global platform for your project
platform :ios, '9.0'

target 'ui-tableView' do
  # Comment the next line if you don't want to use dynamic frameworks
  use_frameworks!

  # Pods for ui-tableView

  pod "Masonry"
  pod "MJExtension"
end

target '2ui-tableViewDelegate' do
  use_frameworks!
  pod "MJExtension"
end</code></pre>
<p>然后执行<code>pod install</code>，不出意外会创建一个<code>项目名.xcworkspace</code>文件，用<code>Xcode</code>打开即可</p>
<h1>参考</h1>
<p><a href="https://www.cocoapods.org">https://www.cocoapods.org</a></p></div>]]></description>
            <guid isPermaLink="false">iOS项目的依赖管理器 - CocoaPods</guid>
        </item>
        <item>
            <title><![CDATA[使用 ControlFlag 扫描出 PHP 代码中的错误]]></title>
            <description><![CDATA[<link rel="stylesheet" href="https://www.cuiwei.net/static/css/github-markdown.min.css?t=20260430" type="text/css" media="screen" /><div class="markdown-body"><p>ControlFlag是一个开源的、利用机器学习来发现任意代码库中的错误的项目，起初它专注于发现C/C++代码中的错误，但随着其新的V1.1版本的发布，开始支持发现PHP代码当中的错误。</p>
<h1>安装</h1>
<blockquote>
<p>注意gcc和cmake的版本，太低不行1️⃣</p>
</blockquote>
<pre><code>#下载安装包
https://github.com/IntelLabs/control-flag/releases/tag/v1.1

cd control-flag-1.1
cmake .
make -j
make test

#创建日志目录
[root@nfsFileSystem control-flag-1.1]# mkdir log</code></pre>
<h1>扫描</h1>
<h3>扫描php</h3>
<pre><code>#准备一个错误的代码
vi /vagrant/php/test.php
&lt;?php
if (x = 7) y = x;

if($a=!3) echo 22;

#扫描
[root@nfsFileSystem control-flag-1.1]# scripts/scan_for_anomalies.sh -d /vagrant/php -t /vagrant/php_controlflag_if_stmts.ts -o log -l 3
Training: start.
Trie L1 build took: 13.641s
Trie L2 build took: 16.266s
Training: complete.
Storing logs in log

#查看扫描结果
[vagrant@nfsFileSystem control-flag-1.1]$ grep "Potential anomaly" -C 5 log/thread_0.log 
[TID=139824646551296] Scanning File: /vagrant/php/test.php
Level:ONE Expression:(parenthesized_expression (binary_expression ("=") (variable_name (name))(unary_op_expression (integer)))) not found in training dataset: Source file: /vagrant/php/test.php:3:2:($a=!3)
Expression is Okay
Level:TWO Expression:(parenthesized_expression (assignment_expression left: (variable_name (name)) right: (unary_op_expression (integer)))) not found in training dataset: Source file: /vagrant/php/test.php:3:2:($a=!3)
Expression is Potential anomaly
Did you mean:(parenthesized_expression (assignment_expression left: (variable_name (name)) right: (unary_op_expression (integer)))) with editing cost:0 and occurrences: 0
Did you mean:(parenthesized_expression (binary_expression left: (variable_name (name)) right: (unary_op_expression (integer)))) with editing cost:2 and occurrences: 217
Did you mean:(parenthesized_expression (assignment_expression left: (variable_name (name)) right: (variable_name (name)))) with editing cost:2 and occurrences: 3</code></pre>
<p>从扫描结果看，代码<code>if($a=!3) echo 22;</code>提示了<code>Expression is Potential anomaly</code>，也给出了几条它的猜测</p>
<p>相反，代码<code>if (x = 7) y = x;</code>就没扫出来问题，提示<code>Expression is Okay</code></p>
<p>其实我私下扫过几个完整的 php 项目，也想了很多 php 的错误语法，令人失望的是基本都扫不出来，有些虽然提示了<code>Expression is Potential anomaly</code>，也基本是误报</p>
<p>简单总结：没什么用</p>
<h3>扫描c</h3>
<pre><code>[root@nfsFileSystem control-flag-1.1]# scripts/scan_for_anomalies.sh -d /vagrant/code/ -t /vagrant/c_lang_if_stmts_6000_gitrepos_small.ts -o log
Training: start.
Trie L1 build took: 11.483s
Trie L2 build took: 6.254s
Training: complete.
Storing logs in log
Scan progress:2/2 ... in progress</code></pre>
<h1>问题</h1>
<p>1️⃣ gcc版本太低（比如<code>7.3.1</code>）会报类似以下错误，我换<code>8.3.1</code>后正常</p>
<pre><code>CMake Error in src/CMakeLists.txt:
  Target "cf_base" requires the language dialect "CXX17" , but CMake does not
  know the compile flags to use to enable it.</code></pre></div>]]></description>
            <guid isPermaLink="false">使用 ControlFlag 扫描出 PHP 代码中的错误</guid>
        </item>
        <item>
            <title><![CDATA[多个 docker-compose 共享网络，共享卷]]></title>
            <description><![CDATA[<link rel="stylesheet" href="https://www.cuiwei.net/static/css/github-markdown.min.css?t=20260430" type="text/css" media="screen" /><div class="markdown-body"><p>server/docker-compose1.yml</p>
<pre><code>version: '3'

networks:
  web-network:

volumes:
  www-data:

services:
  docker-nginx:
    image: nginx:1.21.3
    hostname: nginx
    ports:
      - "81:80"
    restart: always
    tty: true
    volumes:
      - www-data:/var/www/html
    networks:
      - web-network</code></pre>
<p>server/docker-compose2.yml</p>
<pre><code>version: '3'

# 外部网络
networks:
  server_web-network:
    external: true

# 外部卷 https://stackoverflow.com/questions/54051130/share-volumes-between-separate-docker-compose-files
volumes:
  server_www-data:
    external: true

services:
  docker-nginx2:
    image: nginx:1.21.3
    hostname: nginx2
    ports:
      - "82:80"
    restart: always
    tty: true
    volumes:
      - server_www-data:/var/www/html
    networks:
      - server_web-network</code></pre></div>]]></description>
            <guid isPermaLink="false">多个 docker-compose 共享网络，共享卷</guid>
        </item>
        <item>
            <title><![CDATA[Docker 可视化管理工具 - Portainer]]></title>
            <description><![CDATA[<link rel="stylesheet" href="https://www.cuiwei.net/static/css/github-markdown.min.css?t=20260430" type="text/css" media="screen" /><div class="markdown-body"><p>Portainer 是一个简单的 web 界面，用于管理 Docker 容器。</p>
<p>docker-compose.yml</p>
<pre><code>version: '3'

networks:
  web-network:

volumes:
  portainer_data:

services:
  portainer:
    image: portainer/portainer-ce:2.11.1-alpine
    command: -H unix:///var/run/docker.sock
    ports:
      - "9000:9000"
    restart: always
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
      - portainer_data:/data</code></pre>
<p>启动服务</p>
<pre><code>docker-compose up -d</code></pre>
<p>浏览器访问</p>
<p><a href="http://localhost:9000">http://localhost:9000</a></p>
<p><img src="https://www.cuiwei.net/data/upload/2022-04-08/164940863382840.jpg" alt="portainer.JPG" /></p></div>]]></description>
            <guid isPermaLink="false">Docker 可视化管理工具 - Portainer</guid>
        </item>
        <item>
            <title><![CDATA[docker nginx反向代理 nginx-proxy]]></title>
            <description><![CDATA[<link rel="stylesheet" href="https://www.cuiwei.net/static/css/github-markdown.min.css?t=20260430" type="text/css" media="screen" /><div class="markdown-body"><p>我们知道nginx本身是有 反向代理 功能的，下面介绍的<code>nginx-proxy</code>是 docker 场景下，不用写传统的反向代理配置，即可实现 反向代理的效果</p>
<p>下面来看一个<code>docker-compose.yml</code></p>
<pre><code>version: '3'

networks:
  web-network:

services:
  docker-nginx:
    image: nginx:1.21.3
    restart: always
    environment:
      - VIRTUAL_PORT=80
      - VIRTUAL_HOST=whoami.local,192.168.10.4,localhost,127.0.0.1,m.cw.net
    networks:
      - web-network

  docker-2048:
    image: alexwhen/docker-2048
    environment:
      - VIRTUAL_PORT=80
      - VIRTUAL_HOST=2048.cw.net
    networks:
      - web-network

  nginx-proxy:
    image: nginxproxy/nginx-proxy:1.0.0
    restart: always
    ports:
      - 80:80
      - 443:443
    volumes:
      - /var/run/docker.sock:/tmp/docker.sock:ro
    networks:
      - web-network</code></pre>
<p>如上<code>docker-nginx</code>，<code>docker-2048</code>是两个独立的应用，都使用的80端口，在没有添加nginx配置文件的前提下，两个都可以使用80端口访问，分别是<code>m.cw.net</code>，<code>2048.cw.net</code></p>
<p>很明显<code>m.cw.net</code>，<code>2048.cw.net</code>域名是假的，修改 hosts</p>
<pre><code>vi /etc/hosts
127.0.0.1 2048.cw.net m.cw.net</code></pre>
<h2>使用avahi，无需修改hosts</h2>
<pre><code>version: "3.7"

networks:
  web-network:

services:
  docker-nginx:
    image: traefik/whoami:v1.10.1
    restart: always
    environment:
      - VIRTUAL_PORT=80
      - VIRTUAL_HOST=whoami.local
    networks:
      - web-network

  docker-2048:
    image: traefik/whoami:v1.10.1
    environment:
      - VIRTUAL_PORT=80
      - VIRTUAL_HOST=2048.local
    networks:
      - web-network

  nginx-proxy:
    image: nginxproxy/nginx-proxy:1.3.1
    restart: always
    ports:
      - 80:80
      - 443:443
    volumes:
      - /var/run/docker.sock:/tmp/docker.sock:ro
    networks:
      - web-network

  avahi-helper:
    # 这个容器会将以 .local 结尾的 Host 广播出去
    # 在局域网的用户就都能访问到了
    image: hardillb/nginx-proxy-avahi-helper
    restart: unless-stopped
    volumes:
      - /var/run/docker.sock:/tmp/docker.sock
      - /run/dbus/system_bus_socket:/run/dbus/system_bus_socket
    networks:
      - web-network</code></pre>
<h2>高级 - acme-companion</h2>
<p>配合 <code>acme-companion</code>，可以自动给域名生成免费的证书，内部使用的<code>acme.sh</code>，需要部署到服务器，并且80端口空闲</p>
<pre><code>version: '3'

networks:
  web-network:

volumes:
  conf:
  vhost:
  html:
  certs:
  acme:

services:
  docker-nginx:
    image: nginx:1.21.3
    container_name: nginx
    restart: always
    environment:
      - VIRTUAL_PORT=80
      - VIRTUAL_HOST=acme.cw.net
      - LETSENCRYPT_HOST=acme.cw.net
      - LETSENCRYPT_EMAIL=chudaozhe@outlook.com
    networks:
      - web-network

  nginx-proxy:
    image: nginxproxy/nginx-proxy:1.0.0
    container_name: nginx-proxy
    restart: always
    ports:
      - 80:80
      - 443:443
    volumes:
      - conf:/etc/nginx/conf.d
      - vhost:/etc/nginx/vhost.d
      - html:/usr/share/nginx/html
      - certs:/etc/nginx/certs:ro
      - /var/run/docker.sock:/tmp/docker.sock:ro
    networks:
      - web-network

  acme-companion:
    image: nginxproxy/acme-companion:2.2.0
    container_name: nginx-proxy-acme
    volumes_from:
      - nginx-proxy
    volumes:
      - certs:/etc/nginx/certs:rw
      - acme:/etc/acme.sh
      - /var/run/docker.sock:/var/run/docker.sock:ro
    networks:
      - web-network</code></pre></div>]]></description>
            <guid isPermaLink="false">docker nginx反向代理 nginx-proxy</guid>
        </item>
        <item>
            <title><![CDATA[使用 acme.sh 生成免费的 https 证书]]></title>
            <description><![CDATA[<link rel="stylesheet" href="https://www.cuiwei.net/static/css/github-markdown.min.css?t=20260430" type="text/css" media="screen" /><div class="markdown-body"><p>可以从 zerossl 生成免费的证书，有效期90天</p>
<h1>安装</h1>
<pre><code>curl https://get.acme.sh | sh -s email=你的email</code></pre>
<p>如上，是最简单的安装方法，但可能不会成功（因为这个域名访问不了<code>raw.githubusercontent.com</code>），最稳妥的方法是下面这种</p>
<pre><code>git clone https://github.com/acmesh-official/acme.sh.git
cd acme.sh
[root@iZbp1430s16l9piu268n8rZ acme.sh]# ./acme.sh --install -m 你的email
[Fri Apr  8 19:05:22 CST 2022] It is recommended to install socat first.
[Fri Apr  8 19:05:22 CST 2022] We use socat for standalone server if you use standalone mode.
[Fri Apr  8 19:05:22 CST 2022] If you don't use standalone mode, just ignore this warning.
[Fri Apr  8 19:05:22 CST 2022] Installing to /root/.acme.sh
[Fri Apr  8 19:05:22 CST 2022] Installed to /root/.acme.sh/acme.sh
[Fri Apr  8 19:05:22 CST 2022] Installing alias to '/root/.bashrc'
[Fri Apr  8 19:05:22 CST 2022] OK, Close and reopen your terminal to start using acme.sh
[Fri Apr  8 19:05:22 CST 2022] Installing alias to '/root/.cshrc'
[Fri Apr  8 19:05:23 CST 2022] Installing alias to '/root/.tcshrc'
[Fri Apr  8 19:05:23 CST 2022] Installing cron job
no crontab for root
no crontab for root
[Fri Apr  8 19:05:23 CST 2022] Good, bash is found, so change the shebang to use bash as preferred.
[Fri Apr  8 19:05:24 CST 2022] OK</code></pre>
<p>验证是否安装成功</p>
<blockquote>
<p>注意，你可能需要关闭当前ssh链接，重新打开终端才能生效</p>
</blockquote>
<pre><code>[vagrant@nfsFileSystem ~]$ acme.sh -h
https://github.com/acmesh-official/acme.sh
v3.0.2
Usage: acme.sh &lt;command&gt; ... [parameters ...]
Commands:
  -h, --help               Show this help message.
  -v, --version            Show version info.
  --install                Install acme.sh to your system.
  --uninstall              Uninstall acme.sh, and uninstall the cron job.
  --upgrade                Upgrade acme.sh to the latest code from https://github.com/acmesh-official/acme.sh.
  --issue                  Issue a cert.
...</code></pre>
<p>安装成功后会在当前用户下创建一条计划任务，用来检测所有证书是否需要更新，或者 应用本身是否需要更新</p>
<pre><code>19 0 * * * "/home/vagrant/.acme.sh"/acme.sh --cron --home "/home/vagrant/.acme.sh" &gt; /dev/null</code></pre>
<h1>生成证书</h1>
<p>生成证书需要验证你的域名所有权。acme.sh 实现了 acme 协议支持的所有验证协议，一般有 http 和 dns 两种验证方式。
这里介绍几种常用的验证方式。</p>
<h3>http 方式，需要在你的网站根目录下放置一个文件</h3>
<pre><code>acme.sh --issue -d portainer.cuiwei.net --webroot /data/www/portainer/web/ --debug</code></pre>
<p>执行上面的命令会在<code>/data/www/portainer/web/</code>目录自动生成一个类似<code>.well-known/acme-challenge/tvPCfBH2s54bKxC_ORkcDnEQcBAf1wanJGdDgxltvMc</code>这样的文件</p>
<p>如果没意外，会生成<code>portainer.cuiwei.net.key</code>，<code>portainer.cuiwei.net.cer</code>文件，个人习惯<code>.cer</code>后缀改为<code>.pem</code>，手动复制两个文件（portainer.cuiwei.net.key，portainer.cuiwei.net.pem）到指定位置，就可以使用了</p>
<h3>手动 dns 方式，手动在域名上添加一条 txt 解析记录</h3>
<p>不需要服务器，在本地就能生成</p>
<p>一、</p>
<pre><code>acme.sh --issue -d m.cuiwei.net --dns --yes-I-know-dns-manual-mode-enough-go-ahead-please</code></pre>
<p><img src="https://www.cuiwei.net/data/upload/2022-04-06/164922384316655.jpg" alt="11.JPG" /></p>
<p>二、如上图，请将 TXT 记录添加到您的 DNS 记录中。每次更新证书时都需要执行此步骤。使用 DNS api 模式，这一步可以自动化。</p>
<p>三、</p>
<pre><code>acme.sh --renew -d m.cuiwei.net --yes-I-know-dns-manual-mode-enough-go-ahead-please</code></pre>
<p><img src="https://www.cuiwei.net/data/upload/2022-04-06/164923622023241.jpg" alt="acmerenew.jpg" /></p>
<p>手动复制两个文件（<code>m.cuiwei.net.key</code>,<code>m.cuiwei.net.pem</code>）到指定位置，就可以使用了</p>
<p><a href="https://github.com/acmesh-official/acme.sh/wiki/DNS-manual-mode">https://github.com/acmesh-official/acme.sh/wiki/DNS-manual-mode</a></p>
<h1>参考</h1>
<p><a href="https://github.com/acmesh-official/acme.sh/wiki/说明">https://github.com/acmesh-official/acme.sh/wiki/说明</a></p>
<p><a href="https://github.com/acmesh-official/acme.sh/wiki/How-to-issue-a-cert">https://github.com/acmesh-official/acme.sh/wiki/How-to-issue-a-cert</a></p></div>]]></description>
            <guid isPermaLink="false">使用 acme.sh 生成免费的 https 证书</guid>
        </item>
        <item>
            <title><![CDATA[php 生成 RSS 订阅]]></title>
            <description><![CDATA[<link rel="stylesheet" href="https://www.cuiwei.net/static/css/github-markdown.min.css?t=20260430" type="text/css" media="screen" /><div class="markdown-body"><p>rss文件本身是xml，只要找到它的规范，使用php 数组转xml 就可以了</p>
<h1>代码实现</h1>
<pre><code>$data=[
    'title'=&gt;'写代码的崔哥',
    'link'=&gt;'https://www.cuiwei.net/',
    'description'=&gt;'一名PHP程序员，涉猎广泛：PHP，运维，前端，Android，iOS。会不定期给大家分享一些技术干货',
    'language'=&gt;'zh-cn',
    'pubDate'=&gt;gmdate ('l d F Y H:i:s', time()).' GMT',
    'lastBuildDate'=&gt;gmdate ('l d F Y H:i:s', time()).' GMT',
    'docs'=&gt;'https://www.rssboard.org/rss-specification',
    'generator'=&gt;'cwf 1.0',
    'managingEditor'=&gt;'chudaozhe@outlook.com (cw)',
    'webMaster'=&gt;'chudaozhe@outlook.com (cw)',
];
$data['item']=[
    [
        'title' =&gt; 'aaa',
        'link' =&gt; 'https://www.cuiwei.net/p/1203489253',
        'description' =&gt; '描述。。。',
        'pubDate' =&gt; gmdate('l d F Y H:i:s', time()).' GMT',
        'guid' =&gt; '1203489253',
    ],
    [
        'title' =&gt; 'bbb',
        'link' =&gt; 'https://www.cuiwei.net/p/1126078311',
        'description' =&gt; '描述。。。',
        'pubDate' =&gt; gmdate('l d F Y H:i:s', time()).' GMT',
        'guid' =&gt; '1126078311',
    ],
];

$xml = new \SimpleXMLElement('&lt;?xml version="1.0" encoding="utf-8"?&gt;&lt;rss version="2.0"&gt;&lt;/rss&gt;');
array_to_xml(['channel'=&gt;$data], $xml);
echo $xml-&gt;asXML();

function array_to_xml($array, $xml=null){
    if (is_array($array)){
        foreach($array as $key=&gt;$value){
            if(is_int($key)){
                if($key==0){
                    $node=$xml;
                }else{
                    $parent=$xml-&gt;xpath('..')[0];
                    $node=$parent-&gt;addChild($xml-&gt;getName());
                }
            }else{
                $node=$xml-&gt;addChild($key);
            }
            array_to_xml($value, $node);
        }
    }else{
        $xml[0]=$array;
    }
}</code></pre>
<p>结果</p>
<pre><code>&lt;?xml version="1.0" encoding="utf-8"?&gt;
&lt;rss version="2.0"&gt;
    &lt;channel&gt;
        &lt;title&gt;写代码的崔哥&lt;/title&gt;
        &lt;link&gt;https://www.cuiwei.net/&lt;/link&gt;
        &lt;description&gt;一名PHP程序员，涉猎广泛：PHP，运维，前端，Android，iOS。会不定期给大家分享一些技术干货&lt;/description&gt;
        &lt;language&gt;zh-cn&lt;/language&gt;
        &lt;pubDate&gt;Sunday 03 April 2022 07:50:50 GMT&lt;/pubDate&gt;
        &lt;lastBuildDate&gt;Sunday 03 April 2022 07:50:50 GMT&lt;/lastBuildDate&gt;
        &lt;docs&gt;https://www.rssboard.org/rss-specification&lt;/docs&gt;
        &lt;generator&gt;cwf 1.0&lt;/generator&gt;
        &lt;managingEditor&gt;chudaozhe@outlook.com (cw)&lt;/managingEditor&gt;
        &lt;webMaster&gt;chudaozhe@outlook.com (cw)&lt;/webMaster&gt;
        &lt;item&gt;
            &lt;title&gt;aaa&lt;/title&gt;
            &lt;link&gt;https://www.cuiwei.net/p/1203489253&lt;/link&gt;
            &lt;description&gt;描述。。。&lt;/description&gt;
            &lt;pubDate&gt;Sunday 03 April 2022 07:50:50 GMT&lt;/pubDate&gt;
            &lt;guid&gt;1203489253&lt;/guid&gt;
        &lt;/item&gt;
        &lt;item&gt;
            &lt;title&gt;bbb&lt;/title&gt;
            &lt;link&gt;https://www.cuiwei.net/p/1126078311&lt;/link&gt;
            &lt;description&gt;描述。。。&lt;/description&gt;
            &lt;pubDate&gt;Sunday 03 April 2022 07:50:50 GMT&lt;/pubDate&gt;
            &lt;guid&gt;1126078311&lt;/guid&gt;
        &lt;/item&gt;
    &lt;/channel&gt;
&lt;/rss&gt;</code></pre>
<h1>参考</h1>
<p><a href="https://www.rssboard.org/rss-specification">https://www.rssboard.org/rss-specification</a></p>
<p><a href="https://www.php.net/manual/zh/book.simplexml.php">https://www.php.net/manual/zh/book.simplexml.php</a></p></div>]]></description>
            <guid isPermaLink="false">php 生成 RSS 订阅</guid>
        </item>
        <item>
            <title><![CDATA[Redis 实现限流]]></title>
            <description><![CDATA[<link rel="stylesheet" href="https://www.cuiwei.net/static/css/github-markdown.min.css?t=20260430" type="text/css" media="screen" /><div class="markdown-body"><p>下面介绍两种方法</p>
<h1>zset</h1>
<pre><code>function uuid(){
    $str = "123456790abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
    $uuid = "";
    for ($i = 0; $i &lt; 10; $i++) {
        $uuid .= $str[mt_rand(0, strlen($str) - 1)];
    }
    return $uuid;
}

$time = time();
$key='limit:100';
$val = uuid();

$redis = new Redis();
$redis-&gt;connect("docker-redis", 6379);
$ret = $redis-&gt;multi(2)
    -&gt;zRemRangeByScore($key, 0, $time - 60)
    -&gt;zAdd($key, time(), $val)
    -&gt;zCard($key)
    -&gt;exec();

if ($ret[2] &gt; 10) {
    echo 'false, 每分钟最多访问10次';
    return false;
}
echo 'ok';</code></pre>
<h1>redis-cell</h1>
<p>Redis 4.0提供了一个限流Redis模块，称为Redis-Cell。该模块使用了漏斗算法，并提供了原子的限流指定。</p>
<p><a href="https://github.com/brandur/redis-cell/releases/download/v0.3.0/redis-cell-v0.3.0-x86_64-unknown-linux-gnu.tar.gz">https://github.com/brandur/redis-cell/releases/download/v0.3.0/redis-cell-v0.3.0-x86_64-unknown-linux-gnu.tar.gz</a></p>
<pre><code>vi redis.conf
loadmodule /data/modules/libredis_cell.so

root@3afcc7091943:/data# redis-cli
127.0.0.1:6379&gt; CL.THROTTLE user123 15 30 60 1

CL.THROTTLE user123 15 30 60 1
               ▲     ▲  ▲  ▲ ▲
               |     |  |  | └───── apply 1 token (default if omitted)
               |     |  └──┴─────── 30 tokens / 60 seconds
               |     └───────────── 15 max_burst
               └─────────────────── key "user123"

$redis = new Redis();
$redis-&gt;connect("docker-redis", 6379);
//10次1秒
for($i=0;$i&lt;120;$i++) {
    $ret = $redis-&gt;rawCommand("CL.THROTTLE", 'limit', 100, 10, 1, 1);
    echo date('Y-m-d H:i:s').': '.($ret[0]===0?'pass':'no').PHP_EOL;
}</code></pre></div>]]></description>
            <guid isPermaLink="false">Redis 实现限流</guid>
        </item>
        <item>
            <title><![CDATA[Redis GEO地理位置]]></title>
            <description><![CDATA[<link rel="stylesheet" href="https://www.cuiwei.net/static/css/github-markdown.min.css?t=20260430" type="text/css" media="screen" /><div class="markdown-body"><p>Redis GEO 主要用于存储地理位置信息，并对存储的信息进行操作，该功能在 Redis 3.2 版本新增。</p>
<p>基于此可以实现附近的人，附近的店铺等功能</p>
<pre><code>$redis = new Redis();
$redis-&gt;connect("docker-redis", 6379);

//$r=$redis-&gt;geoadd('citys', 114.09981,33.585519, 'taiwei');
//$r=$redis-&gt;geoadd('citys', 114.070524,33.59067, 'dongwaitan');
//$r=$redis-&gt;geoadd('citys', 113.971066,33.577242, 'luohexi');

//两个成员之间的距离
//$r=$redis-&gt;geodist('citys', 'taiwei', 'luohexi', 'km');
//$r=$redis-&gt;geodist('citys', 'taiwei', 'dongwaitan', 'm');

//$r=$redis-&gt;geopos('citys', 'taiwei');
$r=$redis-&gt;geohash('citys', 'luohexi');

//$r=$redis-&gt;georadiusbymember('citys', 'taiwei', 500, 'km');
//$r = $redis-&gt;georadius('citys', 114.09981, 33.585519, '500', 'km', [
//    'count' =&gt; 10,
////    'store'=&gt;'citys2',
//    'storedist'=&gt;'citys3',
//    'desc',
////    'WITHCOORD',
////    'WITHDIST',
////    'WITHHASH'
//]);
//var_dump($redis-&gt;rawCommand('georadius', 'citys', '114', '30', '100', 'km', 'ASC'));
//删除成员
//$r=$redis-&gt;zRem('citys', 'taiwei');
var_dump($r);</code></pre>
<p>有序集合</p>
<pre><code>$redis = new Redis();
$redis-&gt;connect("docker-redis", 6379);

//$ok=$redis-&gt;zAdd('list', -1, 'a', -2, 'b', -3, 'c',0, 'd', 1, 'e', 2, 'f', 3, 'g');
//var_dump($ok);exit;
//$ok=$redis-&gt;zAdd('list2', -1, 'a', -2, 'b', -3, 'c');
//var_dump($ok);exit;

//$list=$redis-&gt;zRange('list', 1, 3);//分数升序，取索引1~3之间的值。b,a,d
//$list=$redis-&gt;zRange('list', 0, -1, ['withscores'=&gt;true]);//分数升序，取全部
//$list=$redis-&gt;zRevRange('list', 1, 3);//分数降序，取索引1~3之间的值。f,e,d
//$list=$redis-&gt;zRevRange('list', 0, -1);//分数降序，取全部
//$list=$redis-&gt;zrangebyscore('list', 0, 3);//分数范围内的成员，分数从低到高排序，['d', 'e', 'f', 'g']
//$list=$redis-&gt;zRevRangeByScore('list', 3, 0);//分数范围内的成员，分数从高到低排序，['g','f', 'e', 'd']

$list=$redis-&gt;zRangeByLex('list', '-', '+');//获取全部成员

//$score=$redis-&gt;zScore('list', 'g');//取分数，3
//$count=$redis-&gt;zCount('list', -1, 1);//分数范围内的总数，start&lt;end，3（['a', 'd', 'e']）
//$count=$redis-&gt;zCard('list');//成员总数
//var_dump($count);exit;

//$r=$redis-&gt;zRem('list', 'a');//删除a
//$r=$redis-&gt;zIncrBy('list', 10, 'g');//增加分数，一次加10，可以统计接口的访问此时

//$rank=$redis-&gt;zRank('list', 'd');//分数升序，获取某个key的排名，排名从0开始
//$rank=$redis-&gt;zRevRank('list', 'd');//分数降序，获取某个key的排名，排名从0开始
//var_dump($rank);exit;

//求交集
//$ok=$redis-&gt;zInterStore('list-list2', ['list', 'list2'], [1, 1], 'MIN');
//并集
//$ok=$redis-&gt;zUnionStore('aaa', ['list', 'list2'], [1, 1], 'MIN');
//var_dump($ok);exit;

//$ok=$redis-&gt;zRemRangeByScore('list', -3, 0);//删除 分数范围内的成员
//$ok=$redis-&gt;zRemRangeByRank('list', 0, 1);//下标参数start和stop都以0为底，0处是分数最小的那个元素。这些索引也可是负数，表示位移从最高分处开始数。例如，-1是分数最高的元素，-2是分数第二高的，依次类推。
//var_dump($ok);exit;
var_dump($list);
</code></pre></div>]]></description>
            <guid isPermaLink="false">Redis GEO地理位置</guid>
        </item>
        <item>
            <title><![CDATA[Nextcloud 应用推荐]]></title>
            <description><![CDATA[<link rel="stylesheet" href="https://www.cuiwei.net/static/css/github-markdown.min.css?t=20260430" type="text/css" media="screen" /><div class="markdown-body"><p>上篇介绍了 <a href="http://www.cuiwei.net/p/1477777115">搭建 Nextcloud 私有云</a> </p>
<p>Nextcloud <a href="https://apps.nextcloud.com/">应用商店</a>提供了很多好用的应用，其中有些应用还提供了<code>Android</code>，<code>iOS</code> APP，下面推荐几个</p>
<h1>Talk</h1>
<p>语音，视频通话，支持网页，Android，iOS</p>
<p><a href="https://apps.nextcloud.com/apps/spreed">https://apps.nextcloud.com/apps/spreed</a></p>
<p><img src="https://www.cuiwei.net/data/upload/2022-03-28/164845873290448.jpg" alt="talk.JPG" /></p>
<h1>Mail</h1>
<p>收/发邮件，需要配置 IMAP，SMTP</p>
<p><a href="https://apps.nextcloud.com/apps/mail">https://apps.nextcloud.com/apps/mail</a></p>
<p><img src="https://www.cuiwei.net/data/upload/2022-03-28/164845902986182.jpg" alt="mail.JPG" /></p>
<h1>Calendar</h1>
<p>日历，可以设置邮件提醒</p>
<p><a href="https://apps.nextcloud.com/apps/calendar">https://apps.nextcloud.com/apps/calendar</a></p>
<p><img src="https://www.cuiwei.net/data/upload/2022-03-28/164845889599025.jpg" alt="calendar.JPG" /></p>
<h1>Contacts</h1>
<p>联系人</p>
<p><a href="https://apps.nextcloud.com/apps/contacts">https://apps.nextcloud.com/apps/contacts</a></p>
<p><img src="https://www.cuiwei.net/data/upload/2022-03-28/164845891855545.jpg" alt="contacts.JPG" /></p>
<h1>Nextcloud Office</h1>
<p>办公</p>
<p><a href="https://apps.nextcloud.com/apps/richdocuments">https://apps.nextcloud.com/apps/richdocuments</a></p>
<p><img src="https://www.cuiwei.net/data/upload/2022-03-28/164845866081810.jpg" alt="office.JPG" />
<img src="https://www.cuiwei.net/data/upload/2022-03-28/164845866026309.jpg" alt="office1.JPG" />
<img src="https://www.cuiwei.net/data/upload/2022-03-28/164845866065707.jpg" alt="office2.JPG" />
<img src="https://www.cuiwei.net/data/upload/2022-03-28/164845866099272.jpg" alt="office3.JPG" />
<img src="https://www.cuiwei.net/data/upload/2022-03-28/164845866089904.jpg" alt="office4.JPG" /></p>
<h1>Notes</h1>
<p>笔记，支持markdown</p>
<p><a href="https://apps.nextcloud.com/apps/notes">https://apps.nextcloud.com/apps/notes</a></p>
<p><img src="https://www.cuiwei.net/data/upload/2022-03-28/164845887620094.jpg" alt="notes.JPG" /></p>
<h1>Carnet</h1>
<p>笔记本，类似Google keep</p>
<p><a href="https://apps.nextcloud.com/apps/carnet">https://apps.nextcloud.com/apps/carnet</a></p>
<p><img src="https://www.cuiwei.net/data/upload/2022-03-28/164845852486669.jpg" alt="carnet.JPG" /></p>
<h1>Draw.io</h1>
<p>在线图表工具，文件直接保存到 Nextcloud</p>
<p><a href="https://apps.nextcloud.com/apps/drawio">https://apps.nextcloud.com/apps/drawio</a></p>
<p><img src="https://www.cuiwei.net/data/upload/2022-03-28/164845894020004.jpg" alt="draw1.jpg" />
<img src="https://www.cuiwei.net/data/upload/2022-03-28/164845894075494.jpg" alt="draw2.png" /></p>
<h1>Mind Map</h1>
<p>思维导图</p>
<p><a href="https://apps.nextcloud.com/apps/files_mindmap">https://apps.nextcloud.com/apps/files_mindmap</a></p>
<p><img src="https://www.cuiwei.net/data/upload/2022-03-28/164845859558198.jpg" alt="mindmap1.JPG" />
<img src="https://www.cuiwei.net/data/upload/2022-03-28/164845859526491.jpg" alt="mindmap2.JPG" /></p>
<h1>Forms</h1>
<p>表单，类似 问卷星</p>
<p><a href="https://apps.nextcloud.com/apps/forms">https://apps.nextcloud.com/apps/forms</a></p>
<p><img src="https://www.cuiwei.net/data/upload/2022-03-28/164845881077216.jpg" alt="forms.JPG" /></p>
<h1>Passwords</h1>
<p>密码管理</p>
<p><a href="https://apps.nextcloud.com/apps/passwords">https://apps.nextcloud.com/apps/passwords</a></p>
<p><img src="https://www.cuiwei.net/data/upload/2022-03-28/164845876695640.jpg" alt="passwords.JPG" /></p>
<h1>Tasks</h1>
<p>任务，类似微软的todo</p>
<p><a href="https://apps.nextcloud.com/apps/tasks">https://apps.nextcloud.com/apps/tasks</a></p>
<p><img src="https://www.cuiwei.net/data/upload/2022-03-28/164845879345025.jpg" alt="tasks.JPG" /></p>
<h1>Deck</h1>
<p>看板，类似Tower</p>
<p><a href="https://apps.nextcloud.com/apps/deck">https://apps.nextcloud.com/apps/deck</a></p>
<p><img src="https://www.cuiwei.net/data/upload/2022-03-28/164845896486366.jpg" alt="deck.JPG" /></p>
<h1>Announcement center</h1>
<p>公告</p>
<p><a href="https://apps.nextcloud.com/apps/announcementcenter">https://apps.nextcloud.com/apps/announcementcenter</a></p>
<p><img src="https://www.cuiwei.net/data/upload/2022-03-28/164845883769904.jpg" alt="announcement2.JPG" />
<img src="https://www.cuiwei.net/data/upload/2022-03-28/164845884680659.jpg" alt="announcement.JPG" /></p>
<h1>News</h1>
<p>订阅资讯</p>
<p><a href="https://apps.nextcloud.com/apps/news">https://apps.nextcloud.com/apps/news</a></p>
<p><img src="https://www.cuiwei.net/data/upload/2022-04-03/164897489845880.jpg" alt="QQ202204031633242x.png" /></p>
<h1>Impersonate</h1>
<p>模拟，管理员 模拟 其他用户身份</p>
<p><a href="https://apps.nextcloud.com/apps/impersonate">https://apps.nextcloud.com/apps/impersonate</a></p>
<p><img src="https://www.cuiwei.net/data/upload/2022-03-28/164845849562906.jpg" alt="impersonate.JPG" /></p></div>]]></description>
            <guid isPermaLink="false">Nextcloud 应用推荐</guid>
        </item>
        <item>
            <title><![CDATA[搭建 Nextcloud 私有云]]></title>
            <description><![CDATA[<link rel="stylesheet" href="https://www.cuiwei.net/static/css/github-markdown.min.css?t=20260430" type="text/css" media="screen" /><div class="markdown-body"><p>Nextcloud 的主要功能是本地网盘，在此基础上提供一个开放平台，开发者可以提交自己的应用。包括 通话、办公、日历、联系人、邮件、笔记、图表、万能表单、密码管理、任务、看板等</p>
<p>Nextcloud官方提供了多种安装方式，下面介绍两种常用的</p>
<h1>普通方式</h1>
<p>Nextcloud 是一个php项目，所以可以像其他php项目一样部署。</p>
<p>下载安装包，<code>https://download.nextcloud.com/server/releases/nextcloud-23.0.3.zip</code></p>
<p>解压到支持php的目录<code>/var/www/nextcloud</code>，分配一个域名<code>nextcloud.cw.net</code></p>
<p>访问<code>nextcloud.cw.net</code>，根据页面提示，填写管理员信息，mysql相关信息，下一步</p>
<p>过一会你可能会看到页面报错了，超时了</p>
<p>但是你访问<code>nextcloud.cw.net/index.php/login</code>会发现，已经可以登录了！</p>
<h1>docker 部署</h1>
<p>选择镜像<code>nextcloud:23.0.3-fpm-alpine</code>，这里面已经包含完整的项目，所以不需要再下载安装包了</p>
<p>部分<code>docker-compose.yml</code></p>
<pre><code>  docker-nextcloud:
    image: nextcloud:23.0.3-fpm-alpine
    container_name: docker-nextcloud
    restart: always
    tty: true
    volumes:
      - ./data/www/backup:/var/www/backup
      - nextcloud:/var/www/html
    environment:
      - REDIS_HOST=docker-redis
#      - REDIS_HOST_PORT=6379
#      - REDIS_HOST_PASSWORD=
      - MYSQL_HOST=docker-mysql
      - MYSQL_DATABASE=nextcloud
      - MYSQL_USER=nextcloud
      - MYSQL_PASSWORD=nextcloud
#      - POSTGRES_HOST=docker-postgres
#      - POSTGRES_DB=nextcloud
#      - POSTGRES_USER=nextcloud
#      - POSTGRES_PASSWORD=nextcloud
    networks:
      - web-network

  docker-cron:
    image: nextcloud:23.0.3-fpm-alpine
    container_name: docker-cron
    restart: always
    tty: true
    volumes:
      - nextcloud:/var/www/html
    entrypoint: /cron.sh
    networks:
      - web-network</code></pre>
<p>详见 <a href="https://github.com/chudaozhe/docker-nextcloud">https://github.com/chudaozhe/docker-nextcloud</a></p>
<h2>自动配置</h2>
<p>如果您需要在多台服务器上安装 Nextcloud，您通常不希望按照 数据库配置中所述分别设置每个实例。为此，Nextcloud 提供了自动配置功能。</p>
<p><a href="https://docs.nextcloud.com/server/latest/admin_manual/configuration_server/automatic_configuration.html">https://docs.nextcloud.com/server/latest/admin_manual/configuration_server/automatic_configuration.html</a></p>
<p>效果如下图，只需填写用户名和密码即可安装</p>
<p><img src="https://www.cuiwei.net/data/upload/2022-04-07/164930458984007.jpg" alt="WX202204062236552x.png" /></p>
<h1>应用推荐</h1>
<p><a href="http://www.cuiwei.net/p/1849104191">http://www.cuiwei.net/p/1849104191</a></p>
<h2>离线安装应用</h2>
<p>在<a href="https://apps.nextcloud.com">应用市场</a>下载安装包，将文件解压至Nextcloud下的apps目录<code>/var/www/html/apps</code></p>
<p>如talk应用，<code>spreed-v13.0.4.tar.gz</code>，解压后<code>/var/www/html/apps/spreed</code>，最后在后台点安装就很快了</p>
<h1>备份</h1>
<p>nextcloud的数据（除了数据库）都持久化在<code>nextcloud</code>卷中，不易迁移。如果要获取里面的数据可以这样</p>
<pre><code>docker exec -it docker-nextcloud /bin/sh
/var/www/html # cd ..
/var/www # tar -cvzf backup/html.tar.gz html/</code></pre>
<p>或者这样</p>
<pre><code>//将容器内的目录cp到本机`~/Downloads/nextcloud`目录
docker cp $ID:/var/www/html ~/Downloads/nextcloud</code></pre>
<h1>参考</h1>
<p><a href="https://github.com/nextcloud/docker/tree/master/.examples/docker-compose/with-nginx-proxy/mariadb/fpm">https://github.com/nextcloud/docker/tree/master/.examples/docker-compose/with-nginx-proxy/mariadb/fpm</a></p></div>]]></description>
            <guid isPermaLink="false">搭建 Nextcloud 私有云</guid>
        </item>
        <item>
            <title><![CDATA[为 Elastic Stack 配置安全性]]></title>
            <description><![CDATA[<link rel="stylesheet" href="https://www.cuiwei.net/static/css/github-markdown.min.css?t=20260430" type="text/css" media="screen" /><div class="markdown-body"><p>安全性需求取决于您是在笔记本电脑上本地开发还是在生产环境中保护所有通信。由于安全需求各不相同，以下场景提供了配置<code>Elastic Stack</code>的选项。</p>
<p><img src="https://www.cuiwei.net/storage/uploads/2025-03-27/174307389975786.jpg" alt="elasticsecurityoverview.png" /></p>
<h2>Elastic Stack 和 ELK 的区别</h2>
<p>其实是一个东西。ELK实际上是三个技术栈的简称，这三款软件分别是ElasticSearch、LogStach、Kibana。在这个生态圈慢慢发展过程中，加入了一个新成员 Beats。那么这个时候，按照之前的称呼，是不是应该称之为ELKB呢？仔细想想，如果这个技术栈中以后再加入一个新成员呢?所以，正式改名为Elastic Stack。</p>
<h2>设置最低安全性</h2>
<h3>ElasticSearch</h3>
<pre><code>vi config/elasticsearch.yml
#discovery.type: single-node
xpack.security.enabled: true

#生成密码
#自动，密码将随机生成并打印到控制台
elasticsearch-setup-passwords auto

#交互式，为每个内置账号手动设置密码
elasticsearch-setup-passwords interactive
</code></pre>
<h3>Kibana</h3>
<pre><code>#配置 kibana 连接 elasticsearch 使用的用户名
vi config/kibana.yml
elasticsearch.username: "kibana_system"

#创建 Kibana 密钥库
kibana-keystore create

#将用户的密码添加kibana_system到 Kibana 密钥库：
kibana-keystore add elasticsearch.password

出现提示时，输入kibana_system用户的密码。</code></pre>
<p>重启 Kibana ，访问<code>http://localhost:5601</code>,使用用户名<code>elastic</code>+密码登录</p>
<h2>设置基本安全性</h2>
<h2>设置基本安全性和 HTTPS</h2>
<h2>常见问题</h2>
<ol>
<li>在kibana中，使用连接器时，提示“必须配置加密密钥才能使用 Alerting。”</li>
</ol>
<p>解决办法</p>
<pre><code>./bin/kibana-encryption-keys generate

//输出一下内容，复制进配置文件config/kibana.yml
Settings:
xpack.encryptedSavedObjects.encryptionKey: dd57efa22caab0c7ee969b4983d9e5c5
xpack.reporting.encryptionKey: bf371ec410d2c98cd0a6e3851b294c3e
xpack.security.encryptionKey: 5286b13e0e7781ed275c4ebb83dac4a4</code></pre>
<p><img src="https://www.cuiwei.net/storage/uploads/2025-03-27/174307975645331.jpg" alt="微信截图_20250327204652.png" /></p>
<h2>参考</h2>
<p><a href="https://www.elastic.co/guide/en/elasticsearch/reference/7.17/configuring-stack-security.html">https://www.elastic.co/guide/en/elasticsearch/reference/7.17/configuring-stack-security.html</a></p>
<p><a href="https://www.elastic.co/guide/en/elasticsearch/reference/7.17/security-minimal-setup.html">https://www.elastic.co/guide/en/elasticsearch/reference/7.17/security-minimal-setup.html</a></p></div>]]></description>
            <guid isPermaLink="false">为 Elastic Stack 配置安全性</guid>
        </item>
        <item>
            <title><![CDATA[logstash 配置]]></title>
            <description><![CDATA[<link rel="stylesheet" href="https://www.cuiwei.net/static/css/github-markdown.min.css?t=20260430" type="text/css" media="screen" /><div class="markdown-body"><h1>输入</h1>
<pre><code>input {
    #标准输入
    stdin {
        codec =&gt; "plain"
    }

    http {
        host =&gt; "0.0.0.0"
        port =&gt; "8099"
    }

    #rsyslog
    syslog{
        type =&gt; "system-syslog"
        port =&gt; 514
    }

    #beats系列，如filebeat
    beats {
        port =&gt; 5044
        host =&gt; "0.0.0.0"
    }

    #从文件读取数据
    file{
        path =&gt; ['/var/log/nginx/access.log']  #要输入的文件路径
        type =&gt; 'nginx_access_log'
        start_position =&gt; "beginning"
    }
    # path  可以用/var/log/*.log,/var/log/**/*.log，如果是/var/log则是/var/log/*.log
    # type 通用选项. 用于激活过滤器
    # start_position 选择logstash开始读取文件的位置，begining或者end。
    # 还有一些常用的例如：discover_interval，exclude，sincedb_path,sincedb_write_interval等可以参考官网

    #rsyslog 通过网络将系统日志消息读取为事件
    syslog{
        port =&gt;"514"
        type =&gt; "syslog"
    }
    # port 指定监听端口(同时建立TCP/UDP的514端口的监听)
    #从syslogs读取需要实现配置rsyslog：
    # cat /etc/rsyslog.conf   加入一行
    # *.* @172.17.128.200:514　  #指定日志输入到这个端口，然后logstash监听这个端口，如果有新日志输入则读取
    # service rsyslog restart   #重启日志服务

    #kafka 将 kafka topic 中的数据读取为事件
    kafka{
        bootstrap_servers=&gt; "kafka01:9092,kafka02:9092,kafka03:9092"
        topics =&gt; ["access_log"]
        #group_id =&gt; "logstash-file"
        codec =&gt; "json"
    }
    # bootstrap_servers 用于建立群集初始连接的Kafka实例的URL列表。
    # topics  要订阅的主题列表，kafka topics
    # group_id 消费者所属组的标识符，默认为logstash。kafka中一个主题的消息将通过相同的方式分发到Logstash的group_id
    # codec 通用选项，用于输入数据的编解码器。
}</code></pre>
<p>还有很多的input插件类型，可以参考官方文档来配置。</p>
<h1>输出</h1>
<pre><code>output {
    elasticsearch {
        hosts =&gt; ["elasticsearch:9200"]
        index =&gt; "system-syslog-%{+YYYY.MM}"
    }
    file {
       path =&gt; "/var/log/nginx/%{host}/save.txt"
       codec =&gt; line { format =&gt; "%{message}" }
    }
    kafka {
        codec =&gt; json
        topic_id =&gt; "mytopic"
    }
    stdout { codec =&gt; rubydebug}
}
</code></pre>
<h1>调试</h1>
<pre><code>logstash -e 'input { stdin{} } filter { grok { patterns_dir =&gt; "/usr/share/logstash/patterns" match =&gt; { "message" =&gt; "%{NGINX_ACCESS}" } }} output { stdout {} }' 

#接着输入
172.19.0.1 - - [08/Mar/2022:08:20:29 +0000] "GET / HTTP/1.1" 404 153 "-" "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:97.0) Gecko/20100101 Firefox/97.0" "-"
#响应
{
          "bytes" =&gt; "153",
           "host" =&gt; "centos8.localdomain",
       "@version" =&gt; "1",
           "verb" =&gt; "GET",
          "agent" =&gt; "\"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:97.0) Gecko/20100101 Firefox/97.0\"",
        "request" =&gt; "/",
    "httpversion" =&gt; "1.1",
     "@timestamp" =&gt; 2022-03-18T09:47:04.498Z,
        "message" =&gt; "172.19.0.1 - - [08/Mar/2022:08:20:29 +0000] \"GET / HTTP/1.1\" 404 153 \"-\" \"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:97.0) Gecko/20100101 Firefox/97.0\" \"-\"",
      "forwarder" =&gt; "\"-\"",
       "clientip" =&gt; "172.19.0.1",
          "ident" =&gt; "-",
      "timestamp" =&gt; "08/Mar/2022:08:20:29 +0000",
       "response" =&gt; "404",
       "referrer" =&gt; "\"-\""
}

#/usr/share/logstash/patterns/nginx
NGINX_ACCESS %{IPORHOST:clientip} (?:-|(%{WORD}.%{WORD})) %{USER:ident} \[%{HTTPDATE:timestamp}\] "(?:%{WORD:verb} %{NOTSPACE:request}(?: HTTP/%{NUMBER:httpversion})?|%{DATA:rawrequest})" %{NUMBER:response} (?:%{NUMBER:bytes}|-) %{QS:referrer} %{QS:agent} %{QS:forwarder}

默认patterns：/usr/share/logstash/vendor/bundle/jruby/2.5.0/gems/logstash-patterns-core-4.3.2/patterns</code></pre>
<p>调试工具：<a href="http://grokdebug.herokuapp.com/">http://grokdebug.herokuapp.com/</a></p>
<p><code>Kibana</code> 自带 <code>Grok Debugger</code></p>
<p><img src="https://www.cuiwei.net/storage/uploads/2025-04-27/174573857836002.jpg" alt="微信截图_20250427152015.png" /></p>
<h1>代码</h1>
<p><a href="https://github.com/chudaozhe/efk/tree/master/logstash">https://github.com/chudaozhe/efk/tree/master/logstash</a></p>
<h1>相关链接</h1>
<p><a href="https://www.cnblogs.com/wzxmt/p/11031110.html">https://www.cnblogs.com/wzxmt/p/11031110.html</a></p>
<p><a href="https://www.jmsite.cn/blog-855.html">https://www.jmsite.cn/blog-855.html</a></p>
<p><a href="https://www.elastic.co/guide/en/logstash/7.17/input-plugins.html">https://www.elastic.co/guide/en/logstash/7.17/input-plugins.html</a></p>
<p><a href="https://www.elastic.co/guide/en/logstash/7.17/filter-plugins.html">https://www.elastic.co/guide/en/logstash/7.17/filter-plugins.html</a></p>
<p><a href="https://www.elastic.co/guide/en/logstash/7.17/output-plugins.html">https://www.elastic.co/guide/en/logstash/7.17/output-plugins.html</a></p></div>]]></description>
            <guid isPermaLink="false">logstash 配置</guid>
        </item>
        <item>
            <title><![CDATA[yum 安装 logstash]]></title>
            <description><![CDATA[<link rel="stylesheet" href="https://www.cuiwei.net/static/css/github-markdown.min.css?t=20260430" type="text/css" media="screen" /><div class="markdown-body"><p>下载并安装公钥</p>
<pre><code>sudo rpm --import https://artifacts.elastic.co/GPG-KEY-elasticsearch</code></pre>
<p>设置源</p>
<pre><code>
cat &gt; /etc/yum.repos.d/logstash.repo &lt;&lt;EOF
[logstash-7.x]
name=Elastic repository for 7.x packages
baseurl=https://artifacts.elastic.co/packages/7.x/yum
gpgcheck=1
gpgkey=https://artifacts.elastic.co/GPG-KEY-elasticsearch
enabled=1
autorefresh=1
type=rpm-md
EOF</code></pre>
<p>安装</p>
<pre><code>yum install logstash
#查看安装目录
rpm -ql logstash
#创建软链接
ln -s /usr/share/logstash/bin/logstash /bin/</code></pre>
<p>测试</p>
<pre><code>logstash -e 'input { stdin { } } output { stdout {} }' 
#运行成功后输入任意内容测试</code></pre>
<p>验证配置文件是否正确</p>
<pre><code>logstash -f etc/conf.d/ -t  # 这里出现OK就说明没有问题</code></pre>
<p>自动重载<code>--config.reload.automatic</code>默认3s</p>
<pre><code>nohup bin/logstash -f etc/conf.d/ --config.reload.automatic &amp;</code></pre>
<h1>参考</h1>
<p><a href="https://www.cnblogs.com/dotqin/p/13638382.html">https://www.cnblogs.com/dotqin/p/13638382.html</a></p>
<p><a href="https://www.elastic.co/guide/en/logstash/7.17/installing-logstash.html">https://www.elastic.co/guide/en/logstash/7.17/installing-logstash.html</a></p></div>]]></description>
            <guid isPermaLink="false">yum 安装 logstash</guid>
        </item>
        <item>
            <title><![CDATA[集中化的日志管理]]></title>
            <description><![CDATA[<link rel="stylesheet" href="https://www.cuiwei.net/static/css/github-markdown.min.css?t=20260430" type="text/css" media="screen" /><div class="markdown-body"><p>把日志放到node节点的主机目录上，在到主机目录上配置rsyslog收集到专门的日志服务器。
从这个日志服务器启一个logstash或者filebeat写入es。
不建议直接从每个节点直接写入es。因为日志量大的时候可能es就会被弄死，另外这么多的filebeat也是要占用不少资源的。
如果觉得麻烦，就每个node写个文件监控。自动添加rsyslog的配置然后重启rsyslog。
这样可以保证日志不丢，还能有序插入es不会因为业务高峰把es弄死，还可以利用logstash再进行一些日志格式化的需求。
目前用这个方案，把istio的所有envoy访问日志、traefik、应用程序日志收集到es上稳定的很。现在每15分钟大概150万条记录。</p>
<h1>流程图</h1>
<p><img src="https://www.cuiwei.net/data/upload/2022-03-19/164768514295371.jpg" alt="log.JPG" /></p>
<p>上图包含多种架构，详见下文分解</p>
<h1>项目日志</h1>
<p>如php项目，每次请求都会记录多条日志，用于监控项目的运行情况</p>
<p>1.最简单的办法是 给你用的框架写一个日志驱动，把日志主动提交到<code>数据收集器</code>,比如 <a href="http://www.cuiwei.net/p/1411721580"><code>fluentd</code></a></p>
<p>常见架构</p>
<pre><code>php项目日志-&gt;fluentd-&gt;elasticsearch1️⃣

php项目日志-&gt;fluentd-&gt;kafka-&gt;logstash-&gt;elasticsearch2️⃣</code></pre>
<p><img src="https://www.cuiwei.net/data/upload/2022-03-17/164750034545008.jpg" alt="php.JPG" /></p>
<h1>系统服务日志</h1>
<p>如nginx, mysql, php</p>
<p>常见架构</p>
<pre><code>system log-&gt;rsyslog-&gt;logstash-&gt;elasticsearch3️⃣

system log-&gt;filebeat-&gt;logstash-&gt;elasticsearch4️⃣

system log-&gt;filebeat-&gt;redis-&gt;logstash-&gt;elasticsearch5️⃣

system log-&gt;filebeat-&gt;kafka-&gt;logstash-&gt;elasticsearch6️⃣

# 上图未体现出来的
system log-&gt;fluent bit-&gt;logstash-&gt;elasticsearch

system log-&gt;fluent bit-&gt;redis-&gt;logstash-&gt;elasticsearch

system log-&gt;fluent bit-&gt;kafka-&gt;logstash-&gt;elasticsearch

#不推荐的，如果你们的流量低，服务器配置还可以，可以这么做
系统服务和logstash/fluentd安装在一台服务器，直接输出到elasticsearch7️⃣</code></pre>
<p><img src="https://www.cuiwei.net/data/upload/2022-03-17/164750507676297.jpg" alt="WX202203171614472x.png" /></p>
<h1>相关文章</h1>
<p><a href="http://www.cuiwei.net/p/1886813055">http://www.cuiwei.net/p/1886813055</a></p>
<p><a href="https://www.cnblogs.com/tanwentao/p/15749435.html">https://www.cnblogs.com/tanwentao/p/15749435.html</a></p>
<p>1️⃣ <a href="http://www.cuiwei.net/p/1886813055">http://www.cuiwei.net/p/1886813055</a></p>
<p>3️⃣ <a href="http://www.cuiwei.net/p/1827808682">http://www.cuiwei.net/p/1827808682</a></p>
<p>4️⃣ <a href="http://www.cuiwei.net/p/1119335331">http://www.cuiwei.net/p/1119335331</a></p>
<p>5️⃣6️⃣ <a href="https://github.com/chudaozhe/grafana-dashboard-nginx-logs">https://github.com/chudaozhe/grafana-dashboard-nginx-logs</a></p>
<p>7️⃣ <a href="http://www.cuiwei.net/p/1376701836">http://www.cuiwei.net/p/1376701836</a></p></div>]]></description>
            <guid isPermaLink="false">集中化的日志管理</guid>
        </item>
        <item>
            <title><![CDATA[Docker - Android 用于移动网站测试和 Android 项目]]></title>
            <description><![CDATA[<link rel="stylesheet" href="https://www.cuiwei.net/static/css/github-markdown.min.css?t=20260430" type="text/css" media="screen" /><div class="markdown-body"><p><img src="https://www.cuiwei.net/data/upload/2022-03-15/164732616244168.jpg" alt="20220315143330.jpg" /></p>
<p><a href="https://www.youtube.com/watch?v=pQdpjuYwvp8">https://www.youtube.com/watch?v=pQdpjuYwvp8</a></p>
<p><a href="https://github.com/budtmo/docker-android">https://github.com/budtmo/docker-android</a></p>
<p>启动服务</p>
<pre><code>docker-compose up -d</code></pre>
<p>localhost:4444/grid/console</p></div>]]></description>
            <guid isPermaLink="false">Docker - Android 用于移动网站测试和 Android 项目</guid>
        </item>
        <item>
            <title><![CDATA[Docker for Android SDK，带有预安装的构建工具和模拟器镜像]]></title>
            <description><![CDATA[<link rel="stylesheet" href="https://www.cuiwei.net/static/css/github-markdown.min.css?t=20260430" type="text/css" media="screen" /><div class="markdown-body"><h1>必要条件</h1>
<p>注意：要使用模拟器功能需要系统支持<code>kvm</code>，所以<code>Windows</code>和<code>Mac OS</code>系统只能使用虚拟机，推荐<code>Ubuntu</code></p>
<p>Your machine need to support virtualization. To check it:</p>
<pre><code>sudo apt install cpu-checker
kvm-ok</code></pre>
<p>不同的版本可能会有差异，我亲测可用的版本如下</p>
<pre><code>androidsdk/android-31
Ubuntu Desktop 20.04.4 LTS
scrcpy v1.23</code></pre>
<h1>系统设置</h1>
<h3>修改Ubuntu镜像源</h3>
<p><a href="https://developer.aliyun.com/mirror/ubuntu">https://developer.aliyun.com/mirror/ubuntu</a> 或者 <a href="https://mirror.tuna.tsinghua.edu.cn/help/ubuntu/">https://mirror.tuna.tsinghua.edu.cn/help/ubuntu/</a></p>
<pre><code>/etc/apt/sources.list
# 默认注释了源码镜像以提高 apt update 速度，如有需要可自行取消注释
deb https://mirrors.tuna.tsinghua.edu.cn/ubuntu/ focal main restricted universe multiverse
# deb-src https://mirrors.tuna.tsinghua.edu.cn/ubuntu/ focal main restricted universe multiverse
deb https://mirrors.tuna.tsinghua.edu.cn/ubuntu/ focal-updates main restricted universe multiverse
# deb-src https://mirrors.tuna.tsinghua.edu.cn/ubuntu/ focal-updates main restricted universe multiverse
deb https://mirrors.tuna.tsinghua.edu.cn/ubuntu/ focal-backports main restricted universe multiverse
# deb-src https://mirrors.tuna.tsinghua.edu.cn/ubuntu/ focal-backports main restricted universe multiverse
deb https://mirrors.tuna.tsinghua.edu.cn/ubuntu/ focal-security main restricted universe multiverse
# deb-src https://mirrors.tuna.tsinghua.edu.cn/ubuntu/ focal-security main restricted universe multiverse

# 预发布软件源，不建议启用
# deb https://mirrors.tuna.tsinghua.edu.cn/ubuntu/ focal-proposed main restricted universe multiverse
# deb-src https://mirrors.tuna.tsinghua.edu.cn/ubuntu/ focal-proposed main restricted universe multiverse</code></pre>
<h1>安装软件</h1>
<h3>docker</h3>
<p>详见：<a href="http://www.cuiwei.net/p/1896979883/">Docker的两种安装方式</a></p>
<h3>scrcpy</h3>
<p>详见：<a href="https://www.cuiwei.net/p/1921459862">android 投屏工具 —— scrcpy</a></p>
<h3>androidsdk</h3>
<p><a href="https://hub.docker.com/u/androidsdk">https://hub.docker.com/u/androidsdk</a></p>
<pre><code>docker pull androidsdk/android-31</code></pre>
<h1>使用步骤</h1>
<pre><code>docker run --network host -it --rm --device /dev/kvm androidsdk/android-31:latest bash

root@cw-VirtualBox:/opt/android-sdk-linux# sdkmanager --list
Installed packages:=====================] 100% Computing updates...             
  Path                                        | Version | Description                                | Location                                   
  -------                                     | ------- | -------                                    | -------                                    
  build-tools;32.0.0                          | 32.0.0  | Android SDK Build-Tools 32                 | build-tools/32.0.0                         
  cmdline-tools;latest                        | 6.0     | Android SDK Command-line Tools (latest)    | cmdline-tools/latest                       
  emulator                                    | 31.2.8  | Android Emulator                           | emulator                                   
  patcher;v4                                  | 1       | SDK Patch Applier v4                       | patcher/v4                                 
  platform-tools                              | 32.0.0  | Android SDK Platform-Tools                 | platform-tools                             
  platforms;android-31                        | 1       | Android SDK Platform 31                    | platforms/android-31                       
  system-images;android-31;google_apis;x86_64 | 8       | Google APIs Intel x86 Atom_64 System Image | system-images/android-31/google_apis/x86_64

avdmanager create avd -n first_avd --abi google_apis/x86_64 -k "system-images;android-31;google_apis;x86_64" --force

emulator -avd first_avd -no-window -no-audio &amp;
adb devices

root@cw-VirtualBox:/home/cw# scrcpy
</code></pre>
<p><img src="https://www.cuiwei.net/data/upload/2022-03-15/164732046431194.jpg" alt="WX202203151253372x.png" /></p></div>]]></description>
            <guid isPermaLink="false">Docker for Android SDK，带有预安装的构建工具和模拟器镜像</guid>
        </item>
        <item>
            <title><![CDATA[基于 docker-compose 的 RocketMQ]]></title>
            <description><![CDATA[<link rel="stylesheet" href="https://www.cuiwei.net/static/css/github-markdown.min.css?t=20260430" type="text/css" media="screen" /><div class="markdown-body"><h1>构建镜像</h1>
<pre><code>git clone git@github.com:apache/rocketmq-docker.git

cd image-build
sh build-image.sh 4.9.3 alpine
sh build-image-dashboard.sh 1.0.0 centos</code></pre>
<h1>docker-compose</h1>
<p><a href="https://github.com/chudaozhe/docker-rocketmq">https://github.com/chudaozhe/docker-rocketmq</a></p>
<h1>访问仪表盘</h1>
<p><a href="http://localhost:6765/">http://localhost:6765/</a></p>
<p><img src="https://www.cuiwei.net/data/upload/2022-03-12/164706239674823.jpg" alt="aa.JPG" /></p>
<h1>php extension and library</h1>
<p>这部分是不成熟的，未经测试</p>
<h3>php extension</h3>
<p><a href="https://github.com/lpflpf/rocketmq-client-php">https://github.com/lpflpf/rocketmq-client-php</a></p>
<p>只有70多个star, 代码最后更新是两年前</p>
<h3>library</h3>
<p><a href="https://help.aliyun.com/document_detail/255816.html">https://help.aliyun.com/document_detail/255816.html</a>
<a href="https://help.aliyun.com/document_detail/43490.html">https://help.aliyun.com/document_detail/43490.html</a></p>
<p>这个需要使用阿里云的云服务，最便宜的一个月也得几百块</p></div>]]></description>
            <guid isPermaLink="false">基于 docker-compose 的 RocketMQ</guid>
        </item>
        <item>
            <title><![CDATA[vscode 之 php 插件及设置]]></title>
            <description><![CDATA[<link rel="stylesheet" href="https://www.cuiwei.net/static/css/github-markdown.min.css?t=20260430" type="text/css" media="screen" /><div class="markdown-body"><p><code>phpstorm</code>非常好，但是不支持容器开发</p>
<h3>Remote - Containers</h3>
<p>连接到容器</p>
<h3>Remote - SSH</h3>
<p>ssh连接到远程服务器</p>
<h3>PHP DocBlocker</h3>
<p>注释插件，<code>/**</code></p>
<h3>PHP Intelephense</h3>
<p>很多功能，如：点击函数名跳转</p>
<h3>php-formatter</h3>
<p>php 格式化</p>
<h3>mac端使用命令行打开vscode</h3>
<ul>
<li>打开vscode</li>
<li>command + shift + p 打开命令面板</li>
<li>输入shell（选择&quot;install code command in PATH&quot;）
<pre><code>code &lt;dir&gt;</code></pre></li>
</ul></div>]]></description>
            <guid isPermaLink="false">vscode 之 php 插件及设置</guid>
        </item>
        <item>
            <title><![CDATA[rsyslog 收集 nginx 日志到专门的日志服务器]]></title>
            <description><![CDATA[<link rel="stylesheet" href="https://www.cuiwei.net/static/css/github-markdown.min.css?t=20260430" type="text/css" media="screen" /><div class="markdown-body"><p>当然，你可以使用<code>filebeat</code>, <code>logstash</code>, <code>fluentd</code>等，但相比之下<code>rsyslog</code>是系统自带的，资源占用低</p>
<h1>第一种方法，配置 nginx</h1>
<p><a href="https://nginx.org/en/docs/syslog.html">https://nginx.org/en/docs/syslog.html</a></p>
<pre><code>vi /etc/nginx/nginx.conf
    # access_log  /var/log/nginx/access.log  main;
    access_log syslog:server=logstash:514,facility=local7,tag=nginx_access_log,severity=info;
    error_log syslog:server=logstash:514,facility=local7,tag=nginx_error_log,severity=info;
</code></pre>
<p>service nginx reload</p>
<h1>第二种方法，配置 rsyslog</h1>
<pre><code>cd /etc/rsyslog.d
vi nginx-log.conf
$ModLoad imfile
$InputFilePollInterval 1
$WorkDirectory /var/spool/rsyslog
$PrivDropToGroup adm

##Nginx访问日志文件路径，根据实际情况修改:
$InputFileName /var/log/nginx/access.log
$InputFileTag nginx-access:
$InputFileStateFile stat-nginx-access
$InputFileSeverity info
$InputFilePersistStateInterval 25000
$InputRunFileMonitor

##Nginx错误日志文件路径，根据实际情况修改:
$InputFileName /var/log/nginx/error.log
$InputFileTag nginx-error:
$InputFileStateFile stat-nginx-error
$InputFileSeverity error
$InputFilePersistStateInterval 25000
$InputRunFileMonitor

#日志输出到logstash
*.* @logstash:514</code></pre>
<p>保存后，重启rsyslog使生效
service rsyslog restart</p>
<p>logstash 输入配置</p>
<pre><code>input {
    syslog{
        type =&gt; "system-syslog"
        port =&gt; 514
    }
}</code></pre>
<p><a href="https://www.cnblogs.com/xiejava/p/12452434.html">https://www.cnblogs.com/xiejava/p/12452434.html</a></p></div>]]></description>
            <guid isPermaLink="false">rsyslog 收集 nginx 日志到专门的日志服务器</guid>
        </item>
        <item>
            <title><![CDATA[php 使用 Kafka]]></title>
            <description><![CDATA[<link rel="stylesheet" href="https://www.cuiwei.net/static/css/github-markdown.min.css?t=20260430" type="text/css" media="screen" /><div class="markdown-body"><p>kafka没有重试机制不支持消息重试，也没有死信队列，因此使用kafka做消息队列时，如果遇到了消息在业务处理时出现异常，就会很难进行下一步处理。应对这种场景，需要自己实现消息重试的功能。</p>
<h1>自己实现重试机制</h1>
<p><img src="https://www.cuiwei.net/data/upload/2022-03-17/164748708121817.jpg" alt="kafka.png" /></p>
<h1>cli</h1>
<pre><code>#停止
kafka-server-stop.sh
#前台启动
kafka-server-start.sh config/server.properties
#守护进程
kafka-server-start.sh -daemon config/server.properties
#新建名为test2的topic，包含2个分区，1个副本
kafka-topics.sh --bootstrap-server kafka:9092 --create --partitions 2 --replication-factor 1 --topic test2
#查看topic列表
kafka-topics.sh --bootstrap-server kafka:9092 --list
#查看topic详情
kafka-topics.sh --bootstrap-server kafka:9092 --describe --topic test2

#删除topic
kafka-topics.sh --bootstrap-server kafka:9092 --delete --topic test2

#消费组
kafka-consumer-groups.sh --bootstrap-server kafka:9092 --list
#消费组详情
kafka-consumer-groups.sh --bootstrap-server kafka:9092 --describe --group test

#命令行生产者
kafka-console-producer.sh --broker-list kafka:9092 --topic test
a
b
c
CTRL + C 结束

#命令行消费者
kafka-console-consumer.sh --bootstrap-server kafka:9092 --topic test --from-beginning
</code></pre>
<h1>High-level consumer 和 Low-level consumer</h1>
<p>从开放的功能服务看，highlevel的配置与使用相对简单，分布式管理功能由Kafka集群与zookeeper自行管理，消费者只要从头或者从最新处获取数据即可，服务重启，能从上次消费位置开始，leader故障，能够自行rebalance。而lowlevel更为为灵活也更复杂，这些都是开放给自己的应用来管理。</p>
<p><a href="https://www.cnblogs.com/liliuguang/p/14627513.html">https://www.cnblogs.com/liliuguang/p/14627513.html</a></p>
<h1>php扩展</h1>
<pre><code># Dockerfile
RUN apt-get update &amp;&amp; apt-get install -y librdkafka-dev
RUN pecl install rdkafka-6.0.0 \
    &amp;&amp; docker-php-ext-enable rdkafka</code></pre>
<h1>kafka服务端，及web管理工具</h1>
<pre><code>https://github.com/chudaozhe/docker-kafka</code></pre>
<p><a href="https://blog.csdn.net/sz85850597/article/details/86749783">https://blog.csdn.net/sz85850597/article/details/86749783</a></p>
<p><a href="https://www.jianshu.com/p/9b83f612c300">https://www.jianshu.com/p/9b83f612c300</a></p></div>]]></description>
            <guid isPermaLink="false">php 使用 Kafka</guid>
        </item>
        <item>
            <title><![CDATA[Grafana nginx日志仪表盘]]></title>
            <description><![CDATA[<link rel="stylesheet" href="https://www.cuiwei.net/static/css/github-markdown.min.css?t=20260430" type="text/css" media="screen" /><div class="markdown-body"><p>本文主要介绍一款nginx 日志仪表盘 —— <a href="https://grafana.com/grafana/dashboards/11190">AKA ES Nginx Logs</a>，非常的酷炫，基于Grafana</p>
<p>先看下效果</p>
<p><img src="https://www.cuiwei.net/data/upload/2022-03-12/164707469972351.jpg" alt="all1.jpg" />
<img src="https://www.cuiwei.net/data/upload/2022-03-12/164707471936315.jpg" alt="all2.jpg" /></p>
<h1>docker-compose</h1>
<p><a href="https://github.com/chudaozhe/grafana-dashboard-nginx-logs">https://github.com/chudaozhe/grafana-dashboard-nginx-logs</a></p>
<h1>Grafana 设置</h1>
<pre><code>http://localhost:3000/
admin
admin</code></pre>
<p>设置es数据源</p>
<p><img src="https://www.cuiwei.net/data/upload/2022-03-06/164655112483799.jpg" alt="q.JPG" />
<img src="https://www.cuiwei.net/data/upload/2022-03-06/164655178674077.jpg" alt="w.JPG" />
<img src="https://www.cuiwei.net/data/upload/2022-03-06/164655179337764.jpg" alt="e.JPG" />
<img src="https://www.cuiwei.net/data/upload/2022-03-06/164655179992219.jpg" alt="f.JPG" /></p>
<p>导入仪表盘</p>
<p><img src="https://www.cuiwei.net/data/upload/2022-03-06/164655206454456.jpg" alt="g.JPG" />
<img src="https://www.cuiwei.net/data/upload/2022-03-06/164655213668105.jpg" alt="j.JPG" /></p>
<p>Grafana 插件</p>
<p>安装两个插件</p>
<p><img src="https://www.cuiwei.net/data/upload/2022-03-06/164655241712580.jpg" alt="k.JPG" /></p>
<p>找到仪表盘，看效果
<img src="https://www.cuiwei.net/data/upload/2022-03-06/164655253082597.jpg" alt="l.JPG" /></p></div>]]></description>
            <guid isPermaLink="false">Grafana nginx日志仪表盘</guid>
        </item>
        <item>
            <title><![CDATA[Beats - 轻量型数据采集器]]></title>
            <description><![CDATA[<link rel="stylesheet" href="https://www.cuiwei.net/static/css/github-markdown.min.css?t=20260430" type="text/css" media="screen" /><div class="markdown-body"><p>Beats 是轻量型数据采集器，Beats 是一个免费且开放的平台，集合了多种单一用途数据采集器。它们从成百上千或成千上万台机器和系统向 Logstash 或 Elasticsearch 发送数据。</p>
<h1>docker-compose</h1>
<p><a href="https://github.com/chudaozhe/docker-beats">https://github.com/chudaozhe/docker-beats</a></p>
<h1>filebeat</h1>
<p>主要收集并输出文件日志
<img src="https://www.cuiwei.net/data/upload/2022-03-05/164646643932956.jpg" alt="ffff.JPG" /></p>
<h1>metricbeat</h1>
<p>将系统和服务的指标和统计数据(例如 CPU、内存、Redis 等等)发送至 Elasticsearch(或 Logstash)
<img src="https://www.cuiwei.net/data/upload/2022-03-05/164646645271095.jpg" alt="mmm.JPG" /></p>
<h1>packetbeat</h1>
<p>Packetbeat 是一款轻量型网络数据包分析器，能够将主机和容器中的数据发送至 Logstash 或 Elasticsearch。</p>
<h1>auditbeat</h1>
<p>收集您 Linux 审计框架的数据，监控文件完整性。Auditbeat 实时采集这些事件，然后发送到 Elastic Stack 其他部分做进一步分析。</p>
<h1>heartbeat</h1>
<p>通过主动探测来监测服务的可用性。通过给定 URL 列表，Heartbeat 仅仅询问：网站运行正常吗？Heartbeat 会将此信息和响应时间发送至 Elastic 的其他部分，以进行进一步分析。</p>
<p><img src="https://www.cuiwei.net/data/upload/2022-04-02/164889638688820.jpg" alt="0.JPG" />
<img src="https://www.cuiwei.net/data/upload/2022-04-02/164889618383237.jpg" alt="1.JPG" />
<img src="https://www.cuiwei.net/data/upload/2022-04-02/164889619450571.jpg" alt="2.JPG" /></p>
<h1>设置仪表盘</h1>
<pre><code>vi filebeat.yml(metricbeat.yml/packetbeat.yml/auditbeat.yml/heartbeat.yml)
setup.kibana:
  host: "kibana:5601"

filebeat setup --dashboards
metricbeat setup --dashboards
packetbeat setup --dashboards
auditbeat setup --dashboards</code></pre>
<h4>heartbeat 的仪表盘（作废，见上图）</h4>
<p><del>注意：<code>heartbeat</code> 的仪表盘需要单独下载，手动导入</del></p>
<p><a href="https://github.com/elastic/uptime-contrib/tree/master/dashboards/7.x/http_dashboard.ndjson">https://github.com/elastic/uptime-contrib/tree/master/dashboards/7.x/http_dashboard.ndjson</a></p>
<p><del>导入前需要确认下索引模式，默认 <code>http_dashboard.ndjson</code> 中的索引模式是 <code>heartbeat-*</code> , 如果和你的数据不一致，需要批量替换</del></p>
<p><del>找到此路径：<code>Stack Management -&gt; 已保存对象</code>。然后点击“导入”按钮，导入成功后就可以在 仪表板 找到了</del></p>
<p><img src="https://www.cuiwei.net/data/upload/2022-03-05/164646600130947.jpg" alt="ddd.JPG" /></p>
<p><del>点击就可以使用了，这里就不展示效果图了，很多字段每显示出来</del></p></div>]]></description>
            <guid isPermaLink="false">Beats - 轻量型数据采集器</guid>
        </item>
        <item>
            <title><![CDATA[filebeat 的使用]]></title>
            <description><![CDATA[<link rel="stylesheet" href="https://www.cuiwei.net/static/css/github-markdown.min.css?t=20260430" type="text/css" media="screen" /><div class="markdown-body"><p>FileBeat 是一款轻量型日志采集器，当您要面对成百上千、甚至成千上万的服务器、虚拟机和容器生成的日志时，请告别 SSH 吧。Filebeat 将为您提供一种轻量型方法，用于转发和汇总日志与文件，让简单的事情不再繁杂。</p>
<p>记住：</p>
<ul>
<li>设置源有两种方式，<code>Input</code>和<code>Module</code>二选一即可</li>
<li><code>FileBeat</code> 支持多输入，单输出</li>
</ul>
<h3>Input</h3>
<p>如下：容器<code>Container</code>，标准输入<code>Stdin</code> </p>
<pre><code>#------------------------------ Container input --------------------------------
- type: container
  enabled: true
  # Paths for container logs that should be crawled and fetched.
  paths:
    - /var/lib/docker/containers/*/*.log
  # Configure stream to filter to a specific stream: stdout, stderr or all (default)
  #stream: all

#----------------------------- Stdin input -------------------------------
- type: stdin
  enabled: true</code></pre>
<h3>Module</h3>
<p>以 Nginx Module为例</p>
<p>Nginx日志格式如下：</p>
<p><a href="https://github.com/kubernetes/ingress-nginx/blob/nginx-0.28.0/docs/user-guide/nginx-configuration/log-format.md">https://github.com/kubernetes/ingress-nginx/blob/nginx-0.28.0/docs/user-guide/nginx-configuration/log-format.md</a></p>
<pre><code>log_format upstreaminfo
     '$remote_addr - $remote_user [$time_local] "$request" '
     '$status $body_bytes_sent "$http_referer" "$http_user_agent" '
     '$request_length $request_time [$proxy_upstream_name] [$proxy_alternative_upstream_name] $upstream_addr '
     '$upstream_response_length $upstream_response_time $upstream_status $req_id';</code></pre>
<pre><code>#查看Filebeat支持模块
filebeat modules list

#启用nginx模块
filebeat modules enable nginx

#禁用nginx模块
filebeat modules disable nginx

vi modules.d/nginx.yml

- module: nginx
  access:
    enabled: true

  error:
    enabled: true
    var.paths: ["/var/log/nginx/error.log"]

  ingress_controller:
    enabled: false
    var.paths: [ "/var/log/nginx/access.log" ]
</code></pre>
<h1>更多示例</h1>
<p><a href="https://github.com/chudaozhe/docker-beats/tree/master/filebeat">https://github.com/chudaozhe/docker-beats/tree/master/filebeat</a></p>
<h1>参考</h1>
<p><a href="https://www.cnblogs.com/h--d/p/13180025.html">https://www.cnblogs.com/h--d/p/13180025.html</a></p>
<p><a href="https://www.cnblogs.com/h--d/p/13172062.html">https://www.cnblogs.com/h--d/p/13172062.html</a></p></div>]]></description>
            <guid isPermaLink="false">filebeat 的使用</guid>
        </item>
        <item>
            <title><![CDATA[elasticsearch、kibana时区问题]]></title>
            <description><![CDATA[<link rel="stylesheet" href="https://www.cuiwei.net/static/css/github-markdown.min.css?t=20260430" type="text/css" media="screen" /><div class="markdown-body"><h1>问题描述</h1>
<pre><code>#索引mappings
{
    "mappings": {
        "properties": {
            "datetime": {
                "type": "date",
                "format": "yyyy-MM-dd HH:mm:ss"
            }
        }
    }
}</code></pre>
<p>通过<code>elasticsearch/elasticsearch</code>向<code>elasticsearch</code>添加数据</p>
<pre><code>{
    "datetime": "2022-02-28 12:26:30"
}</code></pre>
<p>通过kibana看一下
<img src="https://www.cuiwei.net/data/upload/2022-02-28/164602615336455.jpg" alt="WX202202281328572x.png" /></p>
<p>发现两个问题</p>
<ul>
<li>
<p>时区问题，快了8小时</p>
</li>
<li>
<p>日期格式看着不习惯</p>
</li>
</ul>
<p>进一步测试发下，通过<code>ElasticSearch Head</code>插件查看是没问题的。最终把问题定位到<code>Kibana</code>上</p>
<h1>解决办法</h1>
<p>先找到 <code>Management-&gt;Stack Management-&gt;Kibana-&gt;高级设置</code></p>
<p>时区问题 可以通过修改时区解决。默认使用 浏览器检测到的时区，修改为<code>Etc/UTC</code>就可以了
<img src="https://www.cuiwei.net/data/upload/2022-02-28/164602642727921.jpg" alt="WX202202281333342x.png" /></p>
<p>格式问题 可以修改<code>dateFormat</code>的值为<code>YYYY-MM-D HH:mm:ss</code>
<img src="https://www.cuiwei.net/data/upload/2022-02-28/164602650793438.jpg" alt="WX202202281334532x.png" /></p>
<p>两处修改都保存后，即可看到都正常了
<img src="https://www.cuiwei.net/data/upload/2022-02-28/164602660769038.jpg" alt="WX202202281335522x.png" /></p>
<p><img src="https://www.cuiwei.net/data/upload/2022-02-28/164602661884670.jpg" alt="WX202202281336332x.png" /></p></div>]]></description>
            <guid isPermaLink="false">elasticsearch、kibana时区问题</guid>
        </item>
        <item>
            <title><![CDATA[fluentd 的使用]]></title>
            <description><![CDATA[<link rel="stylesheet" href="https://www.cuiwei.net/static/css/github-markdown.min.css?t=20260430" type="text/css" media="screen" /><div class="markdown-body"><p>td-agent 是基于 fluentd 核心功能开发，td-agent 优先考虑稳定性而不是新功能。1️⃣</p>
<p>Fluentd 有9种类型的插件，其中<code>Input</code>和<code>Output</code>是最常用的</p>
<p><code>Input</code>和<code>Output</code>一般是成对出现的，如果要测试<code>Input</code>，<code>Output</code>可以选<code>stdout</code>；如果要测试<code>Output</code>，<code>Input</code>可以选<code>in_http</code>。这两个组合是比较直观的</p>
<h1>输入</h1>
<h3>in_forward</h3>
<pre><code>#php客户端（fluent/logger）使用此源
&lt;source&gt;
    @type forward
    @id in_forward
    port 24224
    bind 0.0.0.0
&lt;/source&gt;</code></pre>
<h3>in_http</h3>
<pre><code># http://&lt;ip&gt;:9880/debug.test?json={"hehe":"uu"}
&lt;source&gt;
    @type http
    @id in_http
    port 9880
    body_size_limit 32m
    keepalive_timeout 10s
&lt;/source&gt;</code></pre>
<h3>in_monitor_agent</h3>
<pre><code># http://&lt;ip&gt;:24220/api/plugins.json
&lt;source&gt;
    @type monitor_agent
    @id in_monitor_agent
    bind 0.0.0.0
    port 24220
&lt;/source&gt;</code></pre>
<h3>in_tail</h3>
<pre><code>&lt;source&gt;
    @type tail
    @id in_tail
    path /var/log/nginx/access.log
    pos_file /var/log/nginx/access.log.pos
    tag nginx.access
    &lt;parse&gt;
        @type nginx
        keep_time_key true
    &lt;/parse&gt;
&lt;/source&gt;</code></pre>
<h1>输出</h1>
<h3>copy</h3>
<pre><code>#输出到目录文件的同时，也输出到标准输出
&lt;match debug.copy&gt;
    @type copy
    &lt;store&gt;
        @type file
        path /var/log/fluent/myapp2
        compress gzip
        &lt;buffer&gt;
            timekey 1d
            timekey_use_utc true
            timekey_wait 10m
        &lt;/buffer&gt;
    &lt;/store&gt;

    &lt;store&gt;
        @type stdout
    &lt;/store&gt;
&lt;/match&gt;</code></pre>
<h3>elasticsearch</h3>
<pre><code>&lt;match nginx.access&gt;
    @type elasticsearch
    @id out_es_nginx
    host elasticsearch
    port 9200
    index_name td.${tag}
    &lt;buffer tag&gt;
        timekey 1m
        timekey           1d
        timekey_wait      10m
        flush_mode        interval
        flush_interval    30s
    &lt;/buffer&gt;
&lt;/match&gt;</code></pre>
<h3>file</h3>
<pre><code>&lt;match debug.file&gt;
    @type file
    #输出到此目录
    path /var/log/fluent/myapp
    compress gzip
    &lt;buffer&gt;
        timekey 1d
        timekey_use_utc true
        timekey_wait 10m
    &lt;/buffer&gt;
&lt;/match&gt;</code></pre>
<h3>stdout</h3>
<pre><code>#标准输出，如果使用的docker，使用docker logs 可以看到
&lt;match debug.stdout&gt;
    @type stdout
&lt;/match&gt;</code></pre>
<h1>fluentd-ui</h1>
<p>fluentd-ui是一个基于浏览器的fluentd和td-agent管理器2️⃣</p>
<p>我首先尝试的docker部署，即<code>fluentd</code>和<code>fluentd-ui</code>是两个独立的容器。最终发现<code>fluentd-ui</code>内包含一个<code>fluentd</code>，无法链接外部的<code>fluentd</code>。这样<code>fluentd-ui</code>的意义就不大了，没有继续研究。。。</p>
<pre><code>git clone git@github.com:fluent/fluentd-ui.git
#构建镜像
docker build -t registry.cn-hangzhou.aliyuncs.com/cuiw/fluentd-ui:20220301 .
</code></pre>
<h1>参考</h1>
<p>1️⃣ <a href="https://cloud.tencent.com/developer/article/1622211">https://cloud.tencent.com/developer/article/1622211</a></p>
<p>2️⃣ <a href="https://docs.fluentd.org/deployment/fluentd-ui">https://docs.fluentd.org/deployment/fluentd-ui</a></p>
<p><a href="https://www.cnblogs.com/fanren224/p/8457181.html">https://www.cnblogs.com/fanren224/p/8457181.html</a></p></div>]]></description>
            <guid isPermaLink="false">fluentd 的使用</guid>
        </item>
        <item>
            <title><![CDATA[elasticsearch 分词]]></title>
            <description><![CDATA[<link rel="stylesheet" href="https://www.cuiwei.net/static/css/github-markdown.min.css?t=20260430" type="text/css" media="screen" /><div class="markdown-body"><p>安装中文、拼音分词</p>
<pre><code>https://github.com/medcl/elasticsearch-analysis-ik
https://github.com/medcl/elasticsearch-analysis-pinyin</code></pre>
<p>下载和<code>elasticsearch</code>对应的版本，解压后移到<code>plugins</code>目录</p>
<pre><code>root@57d58faf9b1e:/usr/share/elasticsearch/plugins# ls
ik  pinyin</code></pre>
<p>重启<code>elasticsearch</code>使生效</p>
<p>测试一下</p>
<pre><code>默认分词
curl -H "Content-Type: application/json" -XPOST 'localhost:9200/_analyze?pretty' -d'
{
  "analyzer": "standard",
  "text":"22强烈推荐11"
}'

ik中文分词
curl -H "Content-Type: application/json" -XPOST 'localhost:9200/_analyze?pretty' -d'
{
  "analyzer": "ik_max_word",
  "text":"22强烈推荐11"
}'

拼音分词
curl -H "Content-Type: application/json" -XPOST 'localhost:9200/_analyze?pretty' -d'
{
  "analyzer": "pinyin",
  "text":"22强烈推荐11"
}'
</code></pre>
<p>创建索引<code>article</code>，内容如下</p>
<pre><code>{
  "settings": {
    "index":{
      "number_of_shards": "1",
      "number_of_replicas": "0",
      "analysis" : {
        "analyzer" : {
          "default" : {
            "tokenizer" : "ik_max_word"
          },
          "pinyin_analyzer" : {
            "tokenizer" : "my_pinyin"
          }
        },
        "tokenizer" : {
          "my_pinyin" : {
            "keep_separate_first_letter" : "false",
            "lowercase" : "true",
            "type" : "pinyin",
            "limit_first_letter_length" : "16",
            "keep_original" : "true",
            "keep_full_pinyin" : "true"
          }
        }
      }
    }
  },
  "mappings": {
    "properties": {
      "title": {
        "type": "text",
        "analyzer":"ik_max_word",
        "fields" : {
          "pinyin" : {
            "type" : "text",
            "term_vector" : "with_positions_offsets",
            "analyzer" : "pinyin_analyzer",
            "boost" : 10.0
          }
        }
      },
      "content": {
        "type": "text",
        "analyzer":"ik_max_word"
      },
      "create_time": {
        "type": "long"
      },
      "id": {
        "type": "long"
      },
      "update_time": {
        "type": "long"
      }
    }
  }
}
</code></pre>
<p>php</p>
<p>导入数据后，就可以测试了</p>
<pre><code>    public function search($keyword, $page=1, $max=10) {
        $params = [
            'index' =&gt; 'article',
            'body' =&gt; [
                'query' =&gt; ['multi_match' =&gt; ['query' =&gt; $keyword, 'fields'=&gt;['title', 'title.pinyin', 'content']]],
                '_source'=&gt;['id', 'title', 'content', 'create_time'],
                'highlight'=&gt;['fields'=&gt;['title'=&gt;new \stdClass(), 'content'=&gt;new \stdClass()]],

                "sort"=&gt;['_doc'],
                'from'=&gt;($page-1)*$max,//from, size相当于sql的limit
                'size'=&gt;$max,
            ]
        ];
        return $this-&gt;cache()-&gt;search($params);
    }</code></pre>
<h2>进阶</h2>
<p>自定义分词词典</p>
<pre><code>//在ik的配置目录增加my.dic
echo '朝阳公园'&gt;./elasticsearch/plugins/ik/config/my.dic

//加载自定义词典
vi ./elasticsearch/plugins/ik/config/IKAnalyzer.cfg.xml
...
&lt;entry key="ext_dict"&gt;my.dic&lt;/entry&gt;
...
//最后，重启es即可</code></pre>
<p>另外，我们看到配置里还有个扩展停止词字典，这个是用来辅助断句的。我们可以看一下自带的一个扩展停止词字典：</p>
<pre><code>$ head -n 5 extra_stopword.dic
也
了
仍
从
以</code></pre>
<p>也就是IK分词器遇到这些词就认为前面的词语不会与这些词构成词语。</p>
<p>IK分词也支持远程词典，远程词典的好处是支持热更新。词典格式和本地的一致，都是一行一个分词（换行符用 \n），还要求填写的URL满足：</p>
<blockquote>
<p>该 http 请求需要返回两个头部(header)，一个是 Last-Modified，一个是 ETag，这两者都是字符串类型，只要有一个发生变化，该插件就会去抓取新的分词进而更新词库。</p>
</blockquote>
<h2>参考</h2>
<p><a href="https://www.lmlphp.com/user/930/article/item/13938/">https://www.lmlphp.com/user/930/article/item/13938/</a></p></div>]]></description>
            <guid isPermaLink="false">elasticsearch 分词</guid>
        </item>
        <item>
            <title><![CDATA[elasticsearch 3种分页方式]]></title>
            <description><![CDATA[<link rel="stylesheet" href="https://www.cuiwei.net/static/css/github-markdown.min.css?t=20260430" type="text/css" media="screen" /><div class="markdown-body"><p>本文将提供原始<code>restfull api</code>和<code>elasticsearch-php</code>的示例</p>
<h1>from + size</h1>
<p>不适合深度分页，<code>max_result_window</code>默认10k</p>
<p>size, from类似sql语句里的limit, 如<code>limit {from} {size}</code>;</p>
<ul>
<li>size：每页条数</li>
<li>from：从第几个行开始，默认0</li>
</ul>
<h4>curl</h4>
<pre><code>curl -H "Content-Type: application/json" -XGET 'localhost:9200/article/_search?pretty' -d'
{
  "size": 2,
  "from": 0
}'</code></pre>
<h4>php</h4>
<pre><code>    public function search($from=0, $size=10) {
        $params = [
            'index' =&gt; 'article',
            'body' =&gt; [
                "sort"=&gt;['_doc'],
                'from'=&gt;$from,
                'size'=&gt;$size,
            ]
        ];
        return $this-&gt;cache()-&gt;search($params);
    }</code></pre>
<h1>scroll</h1>
<ul>
<li>using [from] is not allowed in a scroll context;</li>
</ul>
<h4>curl</h4>
<pre><code>#首页
curl -H "Content-Type: application/json" -XGET 'localhost:9200/article/_search?pretty&amp;scroll=1m' -d'
{
  "size": 2
}'
请求成功，结果里会有 _scroll_id 字段

#下一页
其中scroll_id的值是上一个请求中_scroll_id的值
注意：这次请求的接口和上一次的不一样

curl -H "Content-Type: application/json" -XGET 'localhost:9200/_search/scroll?pretty' -d'
{
  "scroll":"1m",
  "scroll_id":"FGluY2x1ZGVfY29udGV4dF91dWlkDXF1ZXJ5QW5kRmV0Y2gBFmNCWTM3c2U3Um5XYm1rS1FKU0xaVVEAAAAAAAAAYRZMX3BLM2FCOVFLaUhVbVk1STBnRTB3"
}'</code></pre>
<h4>php</h4>
<pre><code>    public function search($scroll='1m', $size=10) {
        $params = [
            'index' =&gt; 'article',
            "scroll" =&gt; $scroll,
            'body' =&gt; [
                "sort"=&gt;['_doc'],
                'size'=&gt;$size,
            ]
        ];
        return $this-&gt;cache()-&gt;search($params);
    }

    public function scroll($scroll_id, $scroll='1m') {
        return $this-&gt;cache()-&gt;scroll(['scroll_id'=&gt;$scroll_id, 'scroll'=&gt;$scroll]);
    }</code></pre>
<h1>search_after</h1>
<p>使用search_after</p>
<ul>
<li>必须要设置from=0</li>
<li>必须指定排序字段</li>
<li>search_after的值是上一个请求最后一条记录中某些字段的值，具体参考排序字段</li>
</ul>
<h4>curl</h4>
<pre><code>#首页
curl -H "Content-Type: application/json" -XGET 'localhost:9200/article/_search?pretty' -d'
{
  "size": 2,
  "from": 0,
  "sort": [
    {
      "_id": {
        "order": "desc"
      }
    }
  ]
}'
请求成功，结果里会有 sort 字段

#下一页
其中search_after的值是上一个请求最后一条记录中sort的值

curl -H "Content-Type: application/json" -XGET 'localhost:9200/article/_search?pretty' -d'
{
  "size": 2,
  "from": 0,
  "search_after": [7],
  "sort": [
    {
      "_id": {
        "order": "desc"
      }
    }
  ]
}'</code></pre>
<h4>php</h4>
<pre><code>    public function search($search_after = [], $size = 10) {
        $params = [
            'index' =&gt; 'article',
            'body' =&gt; [
                "sort" =&gt; ['_doc'],
                'from' =&gt; 0,
                'size' =&gt; $size,
            ]
        ];
        if (count($search_after) &gt; 0) $params['body']['search_after'] = $search_after;
        return $this-&gt;cache()-&gt;search($params);
    }</code></pre>
<h1>参考</h1>
<p><a href="https://www.elastic.co/guide/cn/elasticsearch/php/current/index.html">https://www.elastic.co/guide/cn/elasticsearch/php/current/index.html</a></p></div>]]></description>
            <guid isPermaLink="false">elasticsearch 3种分页方式</guid>
        </item>
        <item>
            <title><![CDATA[elasticsearch 快照和恢复]]></title>
            <description><![CDATA[<link rel="stylesheet" href="https://www.cuiwei.net/static/css/github-markdown.min.css?t=20260430" type="text/css" media="screen" /><div class="markdown-body"><h1>前提</h1>
<p>如果你想使用快照和存储，你必须设置<code>path.repo</code>。</p>
<pre><code>vi elasticsearch.yml
path.repo: ["/usr/share/elasticsearch/backup"]</code></pre>
<p>然后重启<code>elasticsearch</code>使生效</p>
<h1>存储</h1>
<h5>创建存储</h5>
<ul>
<li>存储名称：<code>datasvr</code></li>
<li>存储路径：<code>/usr/share/elasticsearch/backup</code></li>
<li>max_restore_bytes_per_sec：40M</li>
<li>max_snapshot_bytes_per_sec：40M</li>
<li>是否压缩<code>compress</code>：是
<pre><code>curl -H "Content-Type: application/json" -XPUT 'http://localhost:9200/_snapshot/datasvr' -d ' {"type":"fs","settings":{"location":"/usr/share/elasticsearch/backup","compress":true}}'</code></pre></li>
</ul>
<h5>浏览器中查看存储信息</h5>
<pre><code>http://localhost:9200/_snapshot/datasvr/</code></pre>
<h5>删除存储</h5>
<ul>
<li>存储名称：<code>datasvr</code></li>
</ul>
<p>仓库被注销时，ElasticSearch 只删除仓库存储快照的引用位置，快照本身没有被删除并且在原来的位置</p>
<pre><code>curl -X DELETE "http://localhost:9200/_snapshot/datasvr"</code></pre>
<h1>快照</h1>
<p>快照是基于存储的，一个存储下面可以创建多个快照</p>
<h5>创建快照(备份索引)</h5>
<ul>
<li>存储名称：<code>datasvr</code></li>
<li>快照名称：<code>snapshot_1</code></li>
</ul>
<pre><code>备份指定索引
curl -H "Content-Type:application/json" -XPUT 'http://localhost:9200/_snapshot/datasvr/snapshot_1' -d '{"indices": "article"}'

备份全部索引
curl -H "Content-Type:application/json" -XPUT 'http://localhost:9200/_snapshot/datasvr/snapshot_1'</code></pre>
<h5>查看仓库存储的所有快照</h5>
<pre><code>浏览器中查看备份仓库所有快照信息
http://localhost:9200/_snapshot/datasvr/_all

浏览器中查看备份仓库某个快照信息
http://localhost:9200/_snapshot/datasvr/snapshot_1
</code></pre>
<h5>删除快照</h5>
<pre><code>curl -X DELETE "http://localhost:9200/_snapshot/datasvr/snapshot_1"</code></pre>
<h5>恢复快照</h5>
<pre><code>恢复全部
curl -H "Content-Type:application/json" -XPOST 'http://localhost:9200/_snapshot/datasvr/snapshot_1/_restore'

恢复指定索引
curl -H "Content-Type:application/json" -XPOST 'http://localhost:9200/_snapshot/datasvr/snapshot_1/_restore' -d '{"indices": "article"}'</code></pre>
<p><a href="https://www.elastic.co/guide/en/elasticsearch/reference/7.17/snapshot-restore.html">https://www.elastic.co/guide/en/elasticsearch/reference/7.17/snapshot-restore.html</a></p></div>]]></description>
            <guid isPermaLink="false">elasticsearch 快照和恢复</guid>
        </item>
        <item>
            <title><![CDATA[Error: Failed to download metadata for repo 'appstream': Cannot download repomd.xml]]></title>
            <description><![CDATA[<link rel="stylesheet" href="https://www.cuiwei.net/static/css/github-markdown.min.css?t=20260430" type="text/css" media="screen" /><div class="markdown-body"><h2>背景信息</h2>
<p>CentOS 8操作系统版本结束了生命周期（EOL），Linux社区已不再维护该操作系统版本。2021年12月31日CentOS 8 EOL。按照社区规则，CentOS 8的源地址http://mirror.centos.org/centos/8/内容已移除，目前第三方的镜像站中均已移除CentOS 8的源。阿里云的源http://mirrors.cloud.aliyuncs.com和http://mirrors.aliyun.com也无法同步到CentOS 8的源。当您在阿里云上继续使用默认配置的CentOS 8的源会发生报错。报错原文如下</p>
<pre><code>[root@iZbp1430s16l9piu268n8rZ data]# yum install git
CentOS Linux 8 - AppStream                                                                                                                               14 kB/s | 2.3 kB     00:00    
Errors during downloading metadata for repository 'appstream':
  - Status code: 404 for http://mirrors.cloud.aliyuncs.com/centos/8/AppStream/x86_64/os/repodata/repomd.xml (IP: 100.100.2.148)
Error: Failed to download metadata for repo 'appstream': Cannot download repomd.xml: Cannot download repodata/repomd.xml: All mirrors were tried</code></pre>
<h2>操作步骤</h2>
<ol>
<li>
<p>登录CentOS 8系统的ECS实例。</p>
</li>
<li>
<p>运行以下命令备份之前的repo文件。</p>
<pre><code>rename '.repo' '.repo.bak' /etc/yum.repos.d/*.repo</code></pre>
</li>
<li>
<p>运行以下命令下载最新的repo文件。</p>
<pre><code>wget https://mirrors.aliyun.com/repo/Centos-vault-8.5.2111.repo -O /etc/yum.repos.d/Centos-vault-8.5.2111.repo
wget https://mirrors.aliyun.com/repo/epel-archive-8.repo -O /etc/yum.repos.d/epel-archive-8.repo</code></pre>
</li>
<li>
<p>运行以下命令替换repo文件中的链接。</p>
<pre><code>sed -i 's/mirrors.cloud.aliyuncs.com/url_tmp/g'  /etc/yum.repos.d/Centos-vault-8.5.2111.repo &amp;&amp;  sed -i 's/mirrors.aliyun.com/mirrors.cloud.aliyuncs.com/g' /etc/yum.repos.d/Centos-vault-8.5.2111.repo &amp;&amp; sed -i 's/url_tmp/mirrors.aliyun.com/g' /etc/yum.repos.d/Centos-vault-8.5.2111.repo
sed -i 's/mirrors.aliyun.com/mirrors.cloud.aliyuncs.com/g' /etc/yum.repos.d/epel-archive-8.repo</code></pre>
</li>
<li>
<p>运行以下命令重新创建缓存。</p>
<pre><code>yum clean all &amp;&amp; yum makecache</code></pre>
</li>
</ol>
<h2>非阿里云用户</h2>
<ol>
<li>
<p>先备份</p>
<pre><code>rename '.repo' '.repo.bak' /etc/yum.repos.d/*.repo</code></pre>
</li>
<li>
<p>下载新的 CentOS-Base.repo 到 /etc/yum.repos.d/</p>
<pre><code>wget -O /etc/yum.repos.d/CentOS-Base.repo https://mirrors.aliyun.com/repo/Centos-vault-8.5.2111.repo</code></pre>
</li>
<li>
<p>运行 <code>yum makecache</code> 生成缓存</p>
</li>
</ol>
<h2>参考</h2>
<p><a href="https://developer.aliyun.com/mirror/centos">https://developer.aliyun.com/mirror/centos</a></p>
<p><a href="https://help.aliyun.com/document_detail/405635.html">https://help.aliyun.com/document_detail/405635.html</a></p></div>]]></description>
            <guid isPermaLink="false">Error: Failed to download metadata for repo &apos;appstream&apos;: Cannot download repomd.xml</guid>
        </item>
        <item>
            <title><![CDATA[Android 模拟器 Genymotion]]></title>
            <description><![CDATA[<link rel="stylesheet" href="https://www.cuiwei.net/static/css/github-markdown.min.css?t=20260430" type="text/css" media="screen" /><div class="markdown-body"><pre><code>https://www.genymotion.com/</code></pre>
<p><img src="https://www.cuiwei.net/data/upload/2022-02-09/164438657481264.jpg" alt="微信图片_20220209140120.png" /></p>
<p>默认无法安装apk包，需要安装<code>Genymotion-ARM-Translation_for_8.0.zip</code>，安装方式是直接拖</p>
<pre><code>https://github.com/m9rco/Genymotion_ARM_Translation</code></pre></div>]]></description>
            <guid isPermaLink="false">Android 模拟器 Genymotion</guid>
        </item>
        <item>
            <title><![CDATA[Android tcpdump的使用]]></title>
            <description><![CDATA[<link rel="stylesheet" href="https://www.cuiwei.net/static/css/github-markdown.min.css?t=20260430" type="text/css" media="screen" /><div class="markdown-body"><h1>下载安装</h1>
<pre><code>https://www.androidtcpdump.com/android-tcpdump/downloads

#传到手机上
adb push tcpdump /data/local

adb shell
cd /data/local
chmod 777 tcpdump</code></pre>
<h1>使用</h1>
<p>执行命令，结果保存到SD卡test.pcap文件中</p>
<pre><code>tcpdump -i any -p -s 0 -w /sdcard/test.pcap</code></pre>
<p>这时可以使用一下要调试的app，然后<code>ctrl+c</code>结束调试，把test.pcap下载的本机</p>
<pre><code>adb pull /sdcard/test.pcap</code></pre>
<p>最后使用<code>Wireshark</code>打开即可</p></div>]]></description>
            <guid isPermaLink="false">Android tcpdump的使用</guid>
        </item>
        <item>
            <title><![CDATA[Frida 的使用]]></title>
            <description><![CDATA[<link rel="stylesheet" href="https://www.cuiwei.net/static/css/github-markdown.min.css?t=20260430" type="text/css" media="screen" /><div class="markdown-body"><pre><code>#版本选择很重要
frida              12.11.18
frida-tools        5.3.0
frida-server       12.8.10</code></pre>
<h1>服务端（如手机</h1>
<p>android 手机需要root，或直接用模拟器</p>
<pre><code>#下载frida-server，需要选择对应的版本1️⃣ 
https://github.com/frida/frida/releases

#传到手机上
adb push frida-server /data/local

adb shell
cd /data/local
chmod 777 frida-server
./frida-server</code></pre>
<h1>客户端（如本机</h1>
<pre><code>pip3 install frida
pip3 install frida-tools

#或者安装指定版本
pip3 install frida==12.11.18 -i https://pypi.tuna.tsinghua.edu.cn/simple/
pip3 install frida-tools==5.3.0 -i https://pypi.tuna.tsinghua.edu.cn/simple/

#端口转发
adb forward tcp:27042 tcp:27042
adb forward tcp:27043 tcp:27043</code></pre>
<h1>获取sslkey</h1>
<p>确保手机端已经启动<code>frida-server</code>服务，然后本机执行</p>
<pre><code>frida -U -f net.cuiwei.xiangle -l ./sslkeyfilelog.js --no-pause</code></pre>
<p>如果没意外，即可看到如下输出</p>
<p><img src="https://www.cuiwei.net/data/upload/2022-02-09/164438001895305.jpg" alt="WX202202091213182x.png" /></p>
<p>如上图，把选中的内容即sslkey，保存到sslkey.txt，最后添加到<code>Wireshark</code>即可</p>
<p>sslkeyfilelog.js</p>
<pre><code>function startTLSKeyLogger(SSL_CTX_new, SSL_CTX_set_keylog_callback) {
    console.log("start----")
    function keyLogger(ssl, line) {
        console.log(new NativePointer(line).readCString());
    }
    const keyLogCallback = new NativeCallback(keyLogger, 'void', ['pointer', 'pointer']);

    Interceptor.attach(SSL_CTX_new, {
        onLeave: function(retval) {
            const ssl = new NativePointer(retval);
            const SSL_CTX_set_keylog_callbackFn = new NativeFunction(SSL_CTX_set_keylog_callback, 'void', ['pointer', 'pointer']);
            SSL_CTX_set_keylog_callbackFn(ssl, keyLogCallback);
        }
    });
}
startTLSKeyLogger(
    Module.findExportByName('libssl.so', 'SSL_CTX_new'),
    Module.findExportByName('libssl.so', 'SSL_CTX_set_keylog_callback')
)
// https://codeshare.frida.re/@k0nserv/tls-keylogger/</code></pre>
<p>1️⃣选择frida-server
模拟器一般是x86架构，需要下载 frida-server-12.9.8-android-x86.xz</p>
<p>真机一般是arm架构，需要下载frida-server-12.9.8-android-arm.xz</p>
<p>查看系统架构</p>
<pre><code>adb shell 
su
cat /proc/cupinfo
或者
adb shell getprop ro.product.cpu.abi</code></pre>
<h1>参考</h1>
<p><a href="https://www.cnblogs.com/gqv2009/p/13612157.html">https://www.cnblogs.com/gqv2009/p/13612157.html</a></p>
<p><a href="https://blog.csdn.net/qq_39551311/article/details/111184961">https://blog.csdn.net/qq_39551311/article/details/111184961</a></p></div>]]></description>
            <guid isPermaLink="false">Frida 的使用</guid>
        </item>
        <item>
            <title><![CDATA[Wireshark 的使用]]></title>
            <description><![CDATA[<link rel="stylesheet" href="https://www.cuiwei.net/static/css/github-markdown.min.css?t=20260430" type="text/css" media="screen" /><div class="markdown-body"><p>Wireshark 可以实时捕获网络数据包并自动解析，也可以分析本地的pcap数据包文件</p>
<p>下载安装</p>
<p><a href="https://www.wireshark.org/#download">https://www.wireshark.org/#download</a></p>
<p>默认情况下无法解析https的数据，如下图</p>
<p><img src="https://www.cuiwei.net/data/upload/2022-02-09/164437854733974.jpg" alt="https无法解析" /></p>
<p>添加sslkey后，就可以解析了，如下图</p>
<p><img src="https://www.cuiwei.net/data/upload/2022-02-09/164437864282147.jpg" alt="WX202202082255492x.png" /></p>
<p>添加sslkey的方法，如下图</p>
<p><img src="https://www.cuiwei.net/data/upload/2022-02-09/164437867291114.jpg" alt="WX202202082301592x.png" /></p>
<p>分析</p>
<p><img src="https://www.cuiwei.net/data/upload/2022-02-09/164437868949349.jpg" alt="WX202202082323062x.png" /></p>
<h1>如何获取sslkey</h1>
<h3>pc</h3>
<p>设置<code>SSLKEYLOGFILE</code>环境变量，详见</p>
<p><a href="https://www.cnblogs.com/aucy/p/9082429.html">https://www.cnblogs.com/aucy/p/9082429.html</a></p>
<h3>android</h3>
<p>借助Frida，详见</p>
<p><a href="http://www.cuiwei.net/p/1975687145">http://www.cuiwei.net/p/1975687145</a></p></div>]]></description>
            <guid isPermaLink="false">Wireshark 的使用</guid>
        </item>
        <item>
            <title><![CDATA[android tcpdump、frida 和 wireshark 的综合使用]]></title>
            <description><![CDATA[<link rel="stylesheet" href="https://www.cuiwei.net/static/css/github-markdown.min.css?t=20260430" type="text/css" media="screen" /><div class="markdown-body"><h1>准备</h1>
<ul>
<li>种种原因真机无法root，只能选择模拟器<code>Genymotion</code> <a href="http://www.cuiwei.net/p/1668622821">http://www.cuiwei.net/p/1668622821</a></li>
<li>Android tcpdump 获取数据包 <a href="http://www.cuiwei.net/p/1997814490">http://www.cuiwei.net/p/1997814490</a></li>
<li>Frida 获取sslkey <a href="http://www.cuiwei.net/p/1975687145">http://www.cuiwei.net/p/1975687145</a></li>
<li>Wireshark 分析数据包 <a href="http://www.cuiwei.net/p/1158124443">http://www.cuiwei.net/p/1158124443</a></li>
</ul>
<h1>步骤</h1>
<p>1.开始获取数据包</p>
<pre><code>adb shell
cd /data/local
#执行命令，结果保存到SD卡test.pcap文件中
tcpdump -i any -p -s 0 -w /sdcard/test.pcap</code></pre>
<p>详见：<a href="http://www.cuiwei.net/p/1997814490">http://www.cuiwei.net/p/1997814490</a></p>
<p>2.新开一个窗口，开启<code>frida-server</code>服务</p>
<pre><code>adb shell
cd /data/local
./frida-server</code></pre>
<p>3.新开一个窗口，获取<code>sslkey</code></p>
<pre><code>frida -U -f net.cuiwei.xiangle -l ./sslkeyfilelog.js --no-pause</code></pre>
<p>详见：<a href="http://www.cuiwei.net/p/1975687145">http://www.cuiwei.net/p/1975687145</a></p>
<h1>参考</h1>
<p><a href="https://www.52pojie.cn/forum.php?mod=viewthread&amp;tid=1405917">https://www.52pojie.cn/forum.php?mod=viewthread&amp;tid=1405917</a></p></div>]]></description>
            <guid isPermaLink="false">android tcpdump、frida 和 wireshark 的综合使用</guid>
        </item>
        <item>
            <title><![CDATA[MongoDB的使用]]></title>
            <description><![CDATA[<link rel="stylesheet" href="https://www.cuiwei.net/static/css/github-markdown.min.css?t=20260430" type="text/css" media="screen" /><div class="markdown-body"><h1>使用docker-compose部署mongo和mongo-express</h1>
<p>docker-compose.yml</p>
<pre><code>version: '3.8'

# 使用外部网络
# docker network create server_web-network
networks:
  server_web-network:
    external: true

services:
  docker-mongo:
    image: mongo:5.0.5
    restart: always
    environment:
      MONGO_INITDB_ROOT_USERNAME: root
      MONGO_INITDB_ROOT_PASSWORD: 123456
    ports:
      - 27017:27017 #为了在宿主机使用vs code连接mongo
    volumes:
      - ./data:/data/db
    networks:
      - server_web-network

  mongo-express:
    image: mongo-express
    restart: always
    ports:
      - 8081:8081
    environment:
      ME_CONFIG_MONGODB_ADMINUSERNAME: root
      ME_CONFIG_MONGODB_ADMINPASSWORD: 123456
      ME_CONFIG_MONGODB_URL: mongodb://root:123456@docker-mongo:27017/
    networks:
      - server_web-network</code></pre>
<p>启动服务</p>
<pre><code>docker-compose up -d</code></pre>
<p>访问mongo-express</p>
<pre><code>http://localhost:8081/</code></pre>
<h1>php extension and library</h1>
<p>虽然可以单独使用扩展，但强烈建议用户一起使用扩展和库。该库提供了与其他 MongoDB 语言驱动程序一致的高级 API。</p>
<p>extension</p>
<pre><code># Dockerfile
...
RUN pecl install mongodb-1.12.0 \
    &amp;&amp; docker-php-ext-enable mongodb
...</code></pre>
<p>library</p>
<pre><code>composer require mongodb/mongodb</code></pre>
<h1>php demo</h1>
<pre><code>&lt;?php
require_once __DIR__ . '/../../vendor/autoload.php';

$collection = (new MongoDB\Client('mongodb://root:123456@docker-mongo/'))-&gt;images-&gt;shop1;

$document=[
    'content' =&gt; 'uuuu2',
    'uri'=&gt;'ss.jpg',
    'create_time' =&gt; time(),
    'update_time' =&gt; 0,
];
$updateResult = $collection-&gt;updateOne(
    ['_id' =&gt; md5(11)],
    ['$set' =&gt; $document],
    ['upsert' =&gt; true]
);

printf("Matched %d document(s)\n", $updateResult-&gt;getMatchedCount());
printf("Modified %d document(s)\n", $updateResult-&gt;getModifiedCount());
printf("Upserted %d document(s)\n", $updateResult-&gt;getUpsertedCount());

$upsertedDocument = $collection-&gt;findOne([
    '_id' =&gt; $updateResult-&gt;getUpsertedId(),
]);

var_dump($upsertedDocument);

//$document = $collection-&gt;findOne(['_id' =&gt; md5(11)]);
//
//var_dump($document-&gt;content);
//echo json_encode($document, JSON_UNESCAPED_UNICODE);</code></pre>
<h1>备份/恢复</h1>
<p>mongodump 和mongorestore 是mongodb自带的工具，如果本地没有安装mongodb，可以单独下载 <a href="https://www.mongodb.com/try/download/database-tools">https://www.mongodb.com/try/download/database-tools</a></p>
<h3>备份</h3>
<pre><code>docker exec -it {容器id} /bin/sh
cd /data/db
mkdir dump
mongodump -d {库名} -o ./dump -u root -p 123456 --authenticationDatabase admin
</code></pre>
<h3>恢复</h3>
<pre><code>mongorestore -h 127.0.0.1:27017 -u root -p 123456 --authenticationDatabase admin --dir=/data/dump</code></pre>
<h1>参考</h1>
<p><a href="https://docs.mongodb.com/drivers/php/">https://docs.mongodb.com/drivers/php/</a></p>
<p><a href="https://docs.mongodb.com/php-library/current/tutorial/crud/">https://docs.mongodb.com/php-library/current/tutorial/crud/</a></p></div>]]></description>
            <guid isPermaLink="false">MongoDB的使用</guid>
        </item>
        <item>
            <title><![CDATA[在 Kubernetes 上安装 KubeSphere]]></title>
            <description><![CDATA[<link rel="stylesheet" href="https://www.cuiwei.net/static/css/github-markdown.min.css?t=20260430" type="text/css" media="screen" /><div class="markdown-body"><p>KubeSphere 是一款基于 Kubernetes 的开源企业级容器平台，同时也提供定制化服务，服务收费</p>
<p>KubeSphere和Kubernetes Dashboard是一类的，比后者界面更漂亮</p>
<p><img src="https://www.cuiwei.net/data/upload/2021-12-25/164042393937333.jpg" alt="WX202112251654312x.jpg" /></p>
<h1>前提</h1>
<p>可以参考官网的文档 <a href="https://kubesphere.io/zh/docs/installing-on-kubernetes/introduction/prerequisites/">准备工作</a></p>
<p>其中准备默认 StorageClass 是安装 KubeSphere 的前提条件，详见
<a href="http://www.cuiwei.net/p/1755648759">k8s 使用 StorageClass 动态生成 NFS 类型的 PV</a></p>
<h1>部署 KubeSphere</h1>
<p>1、安装</p>
<pre><code>kubectl apply -f https://github.com/kubesphere/ks-installer/releases/download/v3.2.1/kubesphere-installer.yaml
kubectl apply -f https://github.com/kubesphere/ks-installer/releases/download/v3.2.1/cluster-configuration.yaml</code></pre>
<p>2、检查安装日志</p>
<pre><code>kubectl logs -n kubesphere-system $(kubectl get pod -n kubesphere-system -l app=ks-install -o jsonpath='{.items[0].metadata.name}') -f</code></pre>
<p>3、使用 <code>kubectl get pod --all-namespaces</code> 查看所有 Pod 是否在 KubeSphere 的相关命名空间中正常运行。如果是，请通过以下命令检查控制台的端口（默认为 30880）：</p>
<pre><code>kubectl get svc/ks-console -n kubesphere-system</code></pre>
<p>4、确保在安全组中打开了端口 30880，并通过 NodePort (IP:30880) 使用默认帐户和密码 (admin/P@88w0rd) 访问 Web 控制台。</p>
<h1>参考</h1>
<p><a href="https://kubesphere.io/zh/docs/quick-start/minimal-kubesphere-on-k8s/">https://kubesphere.io/zh/docs/quick-start/minimal-kubesphere-on-k8s/</a></p></div>]]></description>
            <guid isPermaLink="false">在 Kubernetes 上安装 KubeSphere</guid>
        </item>
        <item>
            <title><![CDATA[k8s 使用 StorageClass 动态生成 NFS 类型的 PV]]></title>
            <description><![CDATA[<link rel="stylesheet" href="https://www.cuiwei.net/static/css/github-markdown.min.css?t=20260430" type="text/css" media="screen" /><div class="markdown-body"><h1>关于旧版</h1>
<p><a href="https://github.com/kubernetes-retired/external-storage/tree/master/nfs-client/deploy">https://github.com/kubernetes-retired/external-storage/tree/master/nfs-client/deploy</a></p>
<blockquote>
<p>Compatible with kubernetes v1.5.x, v1.6.x, v1.7.x, v1.8.x, v1.9.x, v1.10.x, v1.11.x, v1.12.x, v1.13.x, v1.14.x
Requests/depends on k8s.io/* repos with version kubernetes-1.14</p>
</blockquote>
<p>如上，最多支持kubernetes v1.14.x。我的kubernetes版本是v1.22.0，一开始我没注意到这个，折腾了好久。。。</p>
<h1>新版</h1>
<p><a href="https://github.com/kubernetes-sigs/nfs-subdir-external-provisioner">https://github.com/kubernetes-sigs/nfs-subdir-external-provisioner</a></p>
<p>下载master分支，里面有个<code>deploy</code>目录</p>
<pre><code>├── class.yaml
├── deployment.yaml
├── objects(此目录这里用不到)
├── rbac.yaml
├── test-claim.yaml
└── test-pod.yaml</code></pre>
<p>修改<code>deployment.yaml</code></p>
<pre><code>...
          image: registry.cn-hangzhou.aliyuncs.com/cuiw/k8s.gcr.io_sig-storage_nfs-subdir-external-provisioner:v4.0.2
...
            - name: NFS_SERVER
              #nfs的服务器ip
              value: 192.168.10.99
            - name: NFS_PATH
              #nfs的共享目录
              value: /nfs/data
      volumes:
        - name: nfs-client-root
          nfs:
            #nfs的服务器ip
            server: 192.168.10.99
            #nfs的共享目录
            path: /nfs/data
...</code></pre>
<p>其他文件可以不改</p>
<h4>安装</h4>
<pre><code>kubectl apply -f .</code></pre>
<h4>测试</h4>
<pre><code>会生成一个目录default-test-claim-pvc-e77ac9e8-da08-43ae-9c10-63c45b9674f1，里面有个SUCCESS文件，说明成功了
[root@nfsFileSystem data]# cat /nfs/data/default-test-claim-pvc-e77ac9e8-da08-43ae-9c10-63c45b9674f1/SUCCESS </code></pre>
<h4>默认 StorageClass</h4>
<p>如果安装KubeSphere，它要求默认 StorageClass</p>
<pre><code>kubectl patch storageclass managed-nfs-storage -p '{"metadata": {"annotations":{"storageclass.kubernetes.io/is-default-class":"true"}}}'</code></pre>
<h4>清除</h4>
<pre><code>kubectl delete -f .</code></pre>
<h1>参考</h1>
<p><a href="https://www.ibm.com/docs/zh/cloud-paks/cp-data/4.0?topic=storage-setting-up-nfs">https://www.ibm.com/docs/zh/cloud-paks/cp-data/4.0?topic=storage-setting-up-nfs</a></p></div>]]></description>
            <guid isPermaLink="false">k8s 使用 StorageClass 动态生成 NFS 类型的 PV</guid>
        </item>
        <item>
            <title><![CDATA[python venv模块和virtualenv工具的使用]]></title>
            <description><![CDATA[<link rel="stylesheet" href="https://www.cuiwei.net/static/css/github-markdown.min.css?t=20260430" type="text/css" media="screen" /><div class="markdown-body"><p>virtualenv：Python虚拟环境管理工具。</p>
<p>venv：Python标准库内置的虚拟环境管理工具，Python 3.3加入，Python 3.5开始作为管理虚拟环境的推荐工具，用法类似virtualenv，唯一不同的是创建虚拟环境的方式。</p>
<p>Python 2.x时，创建虚拟环境需要安装第三方的virtualenv，但Python 3.3之后，标准库里内置了venv模块，可以用来创建虚拟环境。</p>
<p>如果你使用Python 3.3及以上版本，推荐使用标准库内置的venv 模块替代virtualenv。</p>
<p>如果你使用Python 2，就只能选择virtualenv，你需要额外安装它。pip install virtualenv</p>
<h1>venv 的使用</h1>
<ol>
<li>
<p>新建项目目录</p>
<pre><code>mkdir /demo
cd /demo</code></pre>
</li>
<li>
<p>创建虚拟环境</p>
<pre><code>#安装新的虚拟环境并为虚拟环境相关文件定义 env 文件夹
python3 -m venv ./venv</code></pre>
</li>
<li>
<p>激活虚拟环境</p>
<pre><code>#激活后，我们应该看到终端将发生变化，并以（venv）开头
source ./venv/bin/activate</code></pre>
</li>
<li>
<p>检查 venv 上的二进制文件</p>
<pre><code>(venv) $ which pip
/demo/venv/bin/pip
(venv) $ which python3
/demo/venv/bin/python3</code></pre>
</li>
</ol>
<p>我们可以在虚拟环境中找到所有pip和python3。</p>
<ol start="5">
<li>安装 Python 模块
<pre><code>(venv) $ pip install numpy</code></pre></li>
</ol>
<p>非常简单，并且与我们在主机上安装模块相同。
现在我们有一个Python3虚拟环境来开发和测试我们的脚本。</p>
<ol start="6">
<li>安装有要求的 Python 模块.txt
<pre><code>(venv) $ pip freeze &gt; requirements.txt</code></pre></li>
</ol>
<p>我们可以使用pip freeze来查找当前虚拟环境中安装的模块，这对于源代码管理非常有用，我们将项目发送给其他模块。</p>
<pre><code>(venv) $ pip install -r requirements.txt</code></pre>
<p>安装包含要求的模块依赖项.txt</p>
<ol start="7">
<li>完成工作后停用
<pre><code>(venv) $ deactivate</code></pre></li>
</ol>
<p><a href="https://childofcode.com/python3-virtual-environment/">https://childofcode.com/python3-virtual-environment/</a></p></div>]]></description>
            <guid isPermaLink="false">python venv模块和virtualenv工具的使用</guid>
        </item>
        <item>
            <title><![CDATA[docker-compose 快速搭建Mysql主从]]></title>
            <description><![CDATA[<link rel="stylesheet" href="https://www.cuiwei.net/static/css/github-markdown.min.css?t=20260430" type="text/css" media="screen" /><div class="markdown-body"><h1>目录结构</h1>
<pre><code>├── docker-compose.yml
├── master
│   ├── conf
│   │   └── my.cnf
│   └── log
├── phpmyadmin
│   ├── config.user.inc.php
│   └── readme.md
├── readme.md
└── slave
    ├── conf
    │   └── my.cnf
    └── log</code></pre>
<h1>启动服务</h1>
<pre><code>docker-compose up -d</code></pre>
<h1>进入master服务器</h1>
<pre><code>docker exec -it docker-mysql-master bash

#登录mysql
root@798f7e9dc2d4:/# mysql

#查看server_id是否生效
mysql&gt; show variables like '%server_id%';
+----------------+-------+
| Variable_name  | Value |
+----------------+-------+
| server_id      | 1     |
| server_id_bits | 32    |
+----------------+-------+
2 rows in set (0.01 sec)

#给slave服务器设置访问权限
mysql&gt; create user 'slave'@'%' identified by '123456';
mysql&gt; grant replication slave,replication client on *.* to 'slave'@'%';
mysql&gt; flush privileges;
#mysql8以下版本使用
mysql&gt; grant replication slave,replication client on *.* to 'slave'@'%' identified by "123456";
mysql&gt; flush privileges;
</code></pre>
<h1>进入slave服务器</h1>
<pre><code>docker exec -it docker-mysql-slave bash

#登录mysql
root@798f7e9dc2d4:/# mysql

#查看server_id是否生效
mysql&gt; show variables like '%server_id%';
+----------------+-------+
| Variable_name  | Value |
+----------------+-------+
| server_id      | 2     |
| server_id_bits | 32    |
+----------------+-------+
2 rows in set (0.00 sec)

#连接master服务器 {master_log_file}和 {master_log_pos}通过在master执行mysql&gt; show master status;获得
change master to master_host='docker-mysql-master',master_user='slave',master_password='123456',master_port=3306,master_log_file='{master_log_file}', master_log_pos={master_log_pos},master_connect_retry=30;

#启动slave
mysql&gt; start slave;
#查看slave 状态
mysql&gt; show slave status \G;
*************************** 1. row ***************************
               Slave_IO_State: Waiting for source to send event
                  Master_Host: docker-mysql-master
                  Master_User: slave
                  Master_Port: 3306
                Connect_Retry: 30
              Master_Log_File: mysql-bin.000006
          Read_Master_Log_Pos: 1063
               Relay_Log_File: 72e31f8337e8-relay-bin.000002
                Relay_Log_Pos: 509
        Relay_Master_Log_File: mysql-bin.000006
             Slave_IO_Running: Yes
            Slave_SQL_Running: Yes
              Replicate_Do_DB:
          Replicate_Ignore_DB: mysql
           Replicate_Do_Table:
       Replicate_Ignore_Table:
      Replicate_Wild_Do_Table:
  Replicate_Wild_Ignore_Table:
                   Last_Errno: 0
                   Last_Error:
                 Skip_Counter: 0
          Exec_Master_Log_Pos: 1063
              Relay_Log_Space: 725
              Until_Condition: None
               Until_Log_File:
                Until_Log_Pos: 0
           Master_SSL_Allowed: No
           Master_SSL_CA_File:
           Master_SSL_CA_Path:
              Master_SSL_Cert:
            Master_SSL_Cipher:
               Master_SSL_Key:
        Seconds_Behind_Master: 0
Master_SSL_Verify_Server_Cert: No
                Last_IO_Errno: 0
                Last_IO_Error:
               Last_SQL_Errno: 0
               Last_SQL_Error:
  Replicate_Ignore_Server_Ids:
             Master_Server_Id: 1
                  Master_UUID: 7c8b7f66-4388-11ec-8cef-0242ac180002
             Master_Info_File: mysql.slave_master_info
                    SQL_Delay: 0
          SQL_Remaining_Delay: NULL
      Slave_SQL_Running_State: Replica has read all relay log; waiting for more updates
           Master_Retry_Count: 86400
                  Master_Bind:
      Last_IO_Error_Timestamp:
     Last_SQL_Error_Timestamp:
               Master_SSL_Crl:
           Master_SSL_Crlpath:
           Retrieved_Gtid_Set:
            Executed_Gtid_Set:
                Auto_Position: 0
         Replicate_Rewrite_DB:
                 Channel_Name:
           Master_TLS_Version:
       Master_public_key_path:
        Get_master_public_key: 0
            Network_Namespace:
1 row in set, 1 warning (0.00 sec)

#如上，如果下面两个都是yes说明配置成功
Slave_IO_Running: Yes
Slave_SQL_Running: Yes
#如果不全是yes,需要先stop slave/reset slave;再执行change master...及后续步骤</code></pre>
<h1>通过phpmyadmin测试</h1>
<p><a href="http://localhost:8080/">http://localhost:8080/</a></p>
<h1>代码</h1>
<p><a href="https://github.com/chudaozhe/docker-mysql-master-slave">https://github.com/chudaozhe/docker-mysql-master-slave</a></p>
<h1>参考</h1>
<p><a href="https://www.cnblogs.com/haima/p/14341903.html">https://www.cnblogs.com/haima/p/14341903.html</a></p></div>]]></description>
            <guid isPermaLink="false">docker-compose 快速搭建Mysql主从</guid>
        </item>
        <item>
            <title><![CDATA[PHP 的一个依赖管理工具 - Composer]]></title>
            <description><![CDATA[<link rel="stylesheet" href="https://www.cuiwei.net/static/css/github-markdown.min.css?t=20260430" type="text/css" media="screen" /><div class="markdown-body"><h1>安装</h1>
<pre><code>curl -sfL https://getcomposer.org/installer | php -- --install-dir=/usr/bin --filename=composer</code></pre>
<h1>包的来源</h1>
<h3>VCS（线上版本控制系统）</h3>
<p>composer.json</p>
<pre><code>{
    "repositories": [
        {
            "type": "vcs",
            "url": "https://github.com/username/hello-world"
        }
    ],
    "require": {
        "acme/hello-world": "dev-master"
    }
}</code></pre>
<h3>Packagist</h3>
<p><a href="https://packagist.org/">Packagist</a> 是 Composer 主要的一个包信息存储库，对于已发布到Packagist的包，安装更方便</p>
<pre><code>composer require monolog/monolog</code></pre>
<p>或者</p>
<p>composer.json</p>
<pre><code>{
    "require": {
        "monolog/monolog": "1.0.*"
    }
}</code></pre>
<h1>参考</h1>
<p><a href="https://docs.phpcomposer.com/02-libraries.html">https://docs.phpcomposer.com/02-libraries.html</a></p></div>]]></description>
            <guid isPermaLink="false">PHP 的一个依赖管理工具 - Composer</guid>
        </item>
        <item>
            <title><![CDATA[docker部署chineseocr_lite实现图片文字识别]]></title>
            <description><![CDATA[<link rel="stylesheet" href="https://www.cuiwei.net/static/css/github-markdown.min.css?t=20260430" type="text/css" media="screen" /><div class="markdown-body"><h1>目录结构</h1>
<pre><code>│  .gitignore
│  docker-compose.yml
│  readme.md
├─.docker
│      docker-compose.yaml
├─chineseorc
│      init.sh</code></pre>
<p>其中<code>.docker</code>目录不是必须的，是配合docker-desktop一起用的，一个python的开发环境</p>
<p>其中<code>docker-compose.yml</code>文件中<code>networks</code>的定义，为了与其他<code>docker-compose.yml</code>网络互通，使用了外部网络。如果不需要多个docker-compose互通，可以修改一下</p>
<pre><code>version: '3'
networks:
  web-network:

services:
  docker-chineseorc:
    ...
    networks:
      - web-network</code></pre>
<h1>获取项目代码</h1>
<pre><code>cd chineseorc
#默认分支，即 onnx
git clone git@github.com:DayBreak-u/chineseocr_lite.git</code></pre>
<h1>一些修改</h1>
<pre><code>cd chineseocr_lite
mv 仿宋_GB2312.ttf fangsong_GB2312.ttf

vi backend/webInterface/tr_run.py
   myfont = ImageFont.truetype("fangsong_GB2312.ttf", size=size)

vi config.py
   max_post_time = 100000000 # 单日ip 访问最大次数

vi backend/main.py
   port = 8089 # 端口

# 关于Dockerfile
官方提供的`Dockerfile`没有问题，编译`docker build -t my/chineseocr .`一下就能用
但是，后来我发现用python的官方镜像+init.sh也是可以运行这个项目的，详见docker-compose.yml</code></pre>
<h1>使用</h1>
<pre><code># 启动
docker-compose up -d
# 浏览器访问
http://127.0.0.1:8089/</code></pre>
<p><img src="https://www.cuiwei.net/data/upload/2021-10-23/163496307552541.jpg" alt="aaa.jpg" /></p>
<h1>代码</h1>
<p><a href="https://github.com/chudaozhe/docker-chineseocr_lite">https://github.com/chudaozhe/docker-chineseocr_lite</a></p></div>]]></description>
            <guid isPermaLink="false">docker部署chineseocr_lite实现图片文字识别</guid>
        </item>
        <item>
            <title><![CDATA[使用Go + Tesseract-OCR 实现文字识别的通用服务]]></title>
            <description><![CDATA[<link rel="stylesheet" href="https://www.cuiwei.net/static/css/github-markdown.min.css?t=20260430" type="text/css" media="screen" /><div class="markdown-body"><h1>目录结构</h1>
<pre><code>│  .gitignore
│  docker-compose.yml
│  Dockerfile
│  main.go
│  readme.md
├─.docker
│      docker-compose.yaml
├─tesseract
│      .gitkeep</code></pre>
<p>其中<code>.docker</code>目录不是必须的，是配合docker-desktop一起用的，一个go的开发环境</p>
<p>其中<code>docker-compose.yml</code>文件中<code>networks</code>的定义，为了与其他<code>docker-compose.yml</code>网络互通，使用了外部网络。如果不需要多个docker-compose互通，可以修改一下</p>
<pre><code>version: '3'
networks:
  web-network:

services:
  docker-tesseract:
    ...
    networks:
      - web-network</code></pre>
<h1>步骤</h1>
<pre><code>#1. 构建镜像 Dockerfile
docker build -t my/tesseract-ocr .

#2. 修改镜像名
vi docker-compose.yml
    image: my/tesseract-ocr

#3. 运行
docker-compose up -d</code></pre>
<h1>main.go</h1>
<p>对外提供一个接口<code>/ocr?url=http://xx.com/aa.jpg</code></p>
<h1>测试</h1>
<p><img src="https://www.cuiwei.net/data/upload/2021-10-23/163497697488950.jpg" alt="O1CN01VyT2bg28nW5QYxtLr_!!22109865379770cib.jpg" /></p>
<p>浏览器访问<code>http://localhost:8000/ocr?url=http://xx.com/aa.jpg</code></p>
<pre><code>{
    "code": 200,
    "msg": "success",
    "data": {
        "tips": "Warning: Invalid resolution 0 dpi. Using 70 instead.\nEstimating resolution as 197\n",
        "content": "PHOTOGRAPH\n产品实拍\n\n由于拍摄光线以及显示器等因素影响，可能会导致图片与实物颜色\n有细微信差，最终颜色以实物为准。\n\u000c",
        "url": "http://xx.com/aa.jpg"
    }
}
</code></pre>
<p>后来发现，在没优化的前提下，chineseocr_lite的效果会更好，详见
<a href="http://www.cuiwei.net/p/1052444754">http://www.cuiwei.net/p/1052444754</a></p>
<h1>代码</h1>
<p><a href="https://github.com/chudaozhe/go-tesseract-ocr">https://github.com/chudaozhe/go-tesseract-ocr</a></p></div>]]></description>
            <guid isPermaLink="false">使用Go + Tesseract-OCR 实现文字识别的通用服务</guid>
        </item>
        <item>
            <title><![CDATA[linux 编译&部署golang 项目]]></title>
            <description><![CDATA[<link rel="stylesheet" href="https://www.cuiwei.net/static/css/github-markdown.min.css?t=20260430" type="text/css" media="screen" /><div class="markdown-body"><p>目录结构</p>
<pre><code>└─demo
   go.mod
   go.sum
   main.go</code></pre>
<h1>编译</h1>
<p>目标：编译项目得到可执行文件 app</p>
<pre><code>cd demo
#编译出支持当前系统的可执行文件
go build -o app .

# 交叉编译
# Mac下编译Linux, Windows平台的64位可执行文件：
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o app .
CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -o app .

# Linux下编译Mac, Windows平台的64位可执行文件：
CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -o app .
CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -o app .

# Windows下编译Mac, Linux平台的64位可执行文件：
CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -o app .
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o app .
</code></pre>
<p>GOOS：目标可执行程序运行操作系统，支持 darwin，freebsd，linux，windows
GOARCH：目标可执行程序操作系统构架，包括 386，amd64，arm</p>
<h1>部署</h1>
<p>通过编译我们得到了可执行文件 app, 下面只需要把它设置为后台守护进程即可</p>
<ul>
<li>nohup ./app &amp;</li>
<li>或者使用supervisor</li>
</ul>
<p>然后就可以通过程序监听的端口访问了，如：localhost:8000</p>
<p>如果不想直接暴露端口，可以使用nginx反向代理，如：客户访问app.cw.net，nginx代理到8000端口</p>
<h1>docker部署</h1>
<p>构建镜像，一个项目一个容器</p>
<p>Dockerfile</p>
<pre><code>FROM golang:1.16 AS build
WORKDIR /go/src/demo
COPY . .

RUN go build -o app .

#FROM build AS development
#RUN apt-get update \
#    &amp;&amp; apt-get install -y git
#CMD ["go", "run", "main.go"]

FROM alpine:3.12
EXPOSE 8000
COPY --from=build /go/src/demo/app /app
CMD ["/app"]
</code></pre></div>]]></description>
            <guid isPermaLink="false">linux 编译&amp;部署golang 项目</guid>
        </item>
        <item>
            <title><![CDATA[使用Docker Desktop快速搭建Go开发环境]]></title>
            <description><![CDATA[<link rel="stylesheet" href="https://www.cuiwei.net/static/css/github-markdown.min.css?t=20260430" type="text/css" media="screen" /><div class="markdown-body"><h1>前言</h1>
<p>想象一下，无论什么语言，什么项目，只要在项目根目录添加一个文件，这个项目就能运行起来，是不是很神奇？</p>
<p>是的，Docker Desktop就可以做这个事，并且使用VS Code连接到docker容器，实现容器内编程，体验完全不输本地环境。</p>
<p>Docker Desktop理论上支持所有语言环境，本人亲测 <a href="https://github.com/chudaozhe/dev-environment-go">多容器的go环境(go+nginx+mysql+redis)</a></p>
<h1>sample介绍</h1>
<p>确保您已经安装了工具!
要开始使用Dev Environments，你需要Git, Visual Studio Code和Visual Studio Code远程容器扩展</p>
<p><img src="https://www.cuiwei.net/data/upload/2021-10-15/163430484015503.jpg" alt="WX202110152125562x.png" /></p>
<p>如上图，1是单容器的go环境（只有go）</p>
<pre><code>项目地址：https://github.com/dockersamples/single-dev-env
用到的镜像：https://hub.docker.com/_/docker/dev-environments-go</code></pre>
<p>2是多容器的go环境（go+nginx+mysql）</p>
<pre><code>项目地址：https://github.com/dockersamples/compose-dev-env
基于`https://github.com/docker/awesome-compose/tree/master/nginx-golang-mysql`修改而来</code></pre>
<h1>自定义开发环境</h1>
<h2>单容器</h2>
<p>如上 <code>single-dev-env</code>，根目录下有一个<code>.docker</code>目录，里面有一个<code>config.json</code></p>
<p>cat .docker/config.json</p>
<pre><code>{
  "image": "docker/dev-environments-go:stable-1"
}</code></pre>
<p>你可以把镜像换成如下镜像</p>
<ul>
<li>docker/dev-environments-javascript</li>
<li>docker/dev-environments-python</li>
<li>docker/dev-environments-java</li>
<li>docker/dev-environments-ruby</li>
<li>docker/dev-environments-dart</li>
</ul>
<p>也可以自定义：比如php，在一个容器内安装php,mysql,nginx,redis。。。，这个不推荐</p>
<h2>多容器</h2>
<p>如上 <code>compose-dev-env</code>，根目录下有一个<code>.docker</code>目录，里面有一个<code>docker-compose.yaml</code></p>
<p>cat .docker/docker-compose.yaml</p>
<pre><code>version: "3.7"
...</code></pre>
<p>你可以修改这个<code>docker-compose.yaml</code>文件来实现多容器的开发环境，我上传了一个<a href="https://github.com/chudaozhe/dev-environment-go">多容器的go环境(go+nginx+mysql+redis)</a>，欢迎star</p>
<h1>最终效果</h1>
<p><img src="https://www.cuiwei.net/data/upload/2021-10-16/163437059939034.jpg" alt="ee.jpg" /></p>
<p><img src="https://www.cuiwei.net/data/upload/2021-10-16/163437867097351.jpg" alt="aaa1.jpg" /></p>
<p>如上图，VSCode成功连接到远程docker容器，体验完全不输本地环境。这种本地无需安装go环境，编辑器中却可以正常运行go代码，GoLand好像做不到</p>
<h1>参考</h1>
<p><a href="https://docs.docker.com/desktop/dev-environments/">https://docs.docker.com/desktop/dev-environments/</a></p></div>]]></description>
            <guid isPermaLink="false">使用Docker Desktop快速搭建Go开发环境</guid>
        </item>
        <item>
            <title><![CDATA[linux 安装go]]></title>
            <description><![CDATA[<link rel="stylesheet" href="https://www.cuiwei.net/static/css/github-markdown.min.css?t=20260430" type="text/css" media="screen" /><div class="markdown-body"><h2>普通</h2>
<pre><code>cd /usr/local
wget https://golang.org/dl/go1.17.2.linux-amd64.tar.gz
tar -xvzf go1.17.2.linux-amd64.tar.gz

#环境变量
vi /etc/profile
export GOROOT=/usr/local/go    #你的go语言包的位置
export PATH=$PATH:/usr/local/go/bin #添加go语言包的bin到path变量里
export GOPATH=/root/gopath  #go的项目存放地址，以后你的go项目需要放在哪里，这个你自己随意设置，重要的是上面两个

#使生效
source /etc/profile

#验证
go version</code></pre>
<h2>go项目初始化</h2>
<pre><code>#下载所以依赖库，类似于php的composer install
go get ./...

go get 可用于添加新模块；
go mod tidy 删除掉无用的模块；
</code></pre>
<h2>docker</h2>
<p><a href="https://github.com/chudaozhe/dev-environment-go">使用Docker Desktop快速搭建Go开发环境，多容器 </a></p>
<h2>常见问题</h2>
<p>golang.org模块无法下载？</p>
<pre><code>go env -w GO111MODULE=on
go env -w GOPROXY=https://goproxy.cn,direct

或者
export GO111MODULE=on
export GOPROXY=https://goproxy.cn</code></pre>
<p><a href="https://goproxy.cn/">https://goproxy.cn/</a></p></div>]]></description>
            <guid isPermaLink="false">linux 安装go</guid>
        </item>
        <item>
            <title><![CDATA[基于docker的php开发环境，多容器]]></title>
            <description><![CDATA[<link rel="stylesheet" href="https://www.cuiwei.net/static/css/github-markdown.min.css?t=20260430" type="text/css" media="screen" /><div class="markdown-body"><p>之前写过一篇 <a href="http://www.cuiwei.net/p/1293320704">vagrant + virtualbox搭建一个可移动的开发环境</a>，现在有了更好的选择，基于docker的php开发环境 拥有前者所有的优点</p>
<p><a href="https://github.com/chudaozhe/dev-environment-php">https://github.com/chudaozhe/dev-environment-php</a></p>
<p><img src="https://www.cuiwei.net/data/upload/2021-10-07/163360170276322.jpg" alt="aa.JPG" /></p></div>]]></description>
            <guid isPermaLink="false">基于docker的php开发环境，多容器</guid>
        </item>
        <item>
            <title><![CDATA[Supervisor进程守护监控]]></title>
            <description><![CDATA[<link rel="stylesheet" href="https://www.cuiwei.net/static/css/github-markdown.min.css?t=20260430" type="text/css" media="screen" /><div class="markdown-body"><h2>应用场景</h2>
<p>工作中可能要写一些cli脚本，需要后台运行，一般会用 <code>nohup command &amp;</code></p>
<pre><code>nohup /usr/bin/php /www/test.php &gt;&gt; /var/log/test.log 2&gt;&amp;1 &amp;</code></pre>
<p>但这样会有一些问题，不能监控进程状态，异常退出时不能自动重启，这时候 supervisor 是更好的选择</p>
<blockquote>
<p>注意：像nginx, mysql, php-fpm等，还是推荐系统级的systemctl</p>
</blockquote>
<h2>安装</h2>
<pre><code>#centos/redhat/fedora
yum install supervisor

#Debian/Ubuntu可通过apt安装
apt-get install supervisor

#pip安装
pip install supervisor

#easy_install安装
easy_install supervisor

#启动
supervisord -c /etc/supervisor/supervisord.conf
</code></pre>
<p>推荐使用 <code>easy_install</code>，安装的版本比较新</p>
<pre><code>[root@iz2zei3sr57xphkhf16csuz ~]# easy_install supervisor
Searching for supervisor
Reading http://mirrors.aliyun.com/pypi/simple/supervisor/
Downloading http://mirrors.aliyun.com/pypi/packages/ce/37/517989b05849dd6eaa76c148f24517544704895830a50289cbbf53c7efb9/supervisor-4.2.5.tar.gz#sha256=34761bae1a23c58192281a5115fb07fbf22c9b0133c08166beffc70fed3ebc12
Best match: supervisor 4.2.5
Processing supervisor-4.2.5.tar.gz
Writing /tmp/easy_install-2AQet8/supervisor-4.2.5/setup.cfg
Running supervisor-4.2.5/setup.py -q bdist_egg --dist-dir /tmp/easy_install-2AQet8/supervisor-4.2.5/egg-dist-tmp-EDB_zo
warning: no previously-included files matching '*' found under directory 'docs/.build'
creating /usr/lib/python2.7/site-packages/supervisor-4.2.5-py2.7.egg
Extracting supervisor-4.2.5-py2.7.egg to /usr/lib/python2.7/site-packages
Adding supervisor 4.2.5 to easy-install.pth file
Installing echo_supervisord_conf script to /usr/bin
Installing pidproxy script to /usr/bin
Installing supervisorctl script to /usr/bin
Installing supervisord script to /usr/bin

Installed /usr/lib/python2.7/site-packages/supervisor-4.2.5-py2.7.egg
Processing dependencies for supervisor
Finished processing dependencies for supervisor

# 创建配置文件
[root@iz2zei3sr57xphkhf16csuz ~]# echo_supervisord_conf

[root@iz2zei3sr57xphkhf16csuz ~]# mkdir /etc/supervisor
[root@iz2zei3sr57xphkhf16csuz ~]# echo_supervisord_conf &gt; /etc/supervisor/supervisord.conf

# 修改主配置
vi /etc/supervisor/supervisord.conf

[supervisord]
logfile=/tmp/supervisord.log ; main log file; default $CWD/supervisord.log
logfile_maxbytes=50MB        ; max main logfile bytes b4 rotation; default 50MB
logfile_backups=10           ; # of main logfile backups; 0 means none, default 10
loglevel=info                ; log level; default info; others: debug,warn,trace
pidfile=/tmp/supervisord.pid ; supervisord pidfile; default supervisord.pid

[include]
files = conf.d/*.ini</code></pre>
<h2>配置</h2>
<p>新建一个脚本的配置文件</p>
<pre><code>vi /etc/supervisor/conf.d/demo.conf
;demo表示程序名称
[program:demo]
;需要执行的命令
command=php demo.php
;命令执行的目录
directory=/var/www/demo/beanstalkd/demo0/
;环境变量
environment=PATH="/usr/local/bin/"
;哪个用户运行
user=root
;是否自启动
autostart=true
;是否自动重启
autorestart=true
;自动重启时间间隔，单位秒
startsecs=3
;错误日志文件
stderr_logfile=/tmp/demo.err.log
;输出日志文件
stdout_logfile=/tmp/demo.out.log

保存后reload一下，使生效
supervisorctl reload</code></pre>
<p>脚本文件 demo.php</p>
<pre><code>&lt;?php

$i = 0;
while(true) {
    $i++;
    echo $i, PHP_EOL;
    sleep(1);
}</code></pre>
<h2>web界面</h2>
<pre><code>vi /etc/supervisor/supervisord.conf

;增加
[inet_http_server]
port=0.0.0.0:9001
username=admin
password=123456

保存后reload一下，使生效
supervisorctl reload

然后访问 http://localhost:9001/，即可看到如下界面</code></pre>
<p><img src="https://www.cuiwei.net/data/upload/2021-09-26/163264587080516.jpg" alt="WX202109261643472x.png" /></p>
<h2>Centos 开机自启</h2>
<pre><code>vi /usr/lib/systemd/system/supervisor.service
#supervisor.service

[Unit] 
Description=Supervisor daemon

[Service] 
Type=forking 
ExecStart=/usr/local/bin/supervisord -c /etc/supervisor/supervisord.conf
ExecStop=/usr/local/bin/supervisorctl shutdown 
ExecReload=/usr/local/bin/supervisorctl reload 
KillMode=process 
Restart=on-failure 
RestartSec=42s

[Install] 
WantedBy=multi-user.target

# 激活
systemctl enable supervisor</code></pre>
<p>使用</p>
<pre><code>service supervisor start
service supervisor stop</code></pre>
<p>laravel队列</p>
<pre><code>cat /etc/supervisor/conf.d/laravel-worker.conf
[program:laravel-worker]
process_name=%(program_name)s_%(process_num)02d
command=docker exec -u www-data environment-php-docker-php-fpm-1 /var/www/laravel-demo/artisan queue:work --queue=high,default,robot,vip_expire --sleep=3 --tries=3 --max-time=3600
autostart=true
autorestart=true
stopasgroup=true
killasgroup=true
user=root
numprocs=8
redirect_stderr=true
stdout_logfile=/var/log/laravel-worker.log
stopwaitsecs=3600</code></pre>
<p>任务调度（定时任务）</p>
<pre><code>* * * * * docker exec -u www-data environment-php-docker-php-fpm-1 /var/www/laravel-demo/artisan schedule:run &gt;&gt; /dev/null 2&gt;&amp;1</code></pre>
<h2>常用命令</h2>
<pre><code>#启动
supervisord -c /etc/supervisor/supervisord.conf

#停止supervisord
supervisorctl shutdown

#reload一下，使生效
supervisorctl reload

#查看supervisord当前管理的所有进程的状态
supervisorctl status

#停止supervisord管理的所有进程
supervisorctl stop all
#supervisord管理的某一个特定进程
supervisorctl start program-name // program-name为[program:xx]中的xx
#停止supervisord管理的某一个特定进程
supervisorctl stop program-name  // program-name为[program:xx]中的xx

#重启所有进程或某一进程
supervisorctl restart all //重启所有
supervisorctl reatart program-name //重启某一进程，program-name为[program:xx]中的xx

#启动进程
supervisorctl start xxx
#重启进程
supervisorctl restart xxx
#重启所有属于名为group的分组进程
supervisorctl stop group
#停止全部进程
supervisorctl stop all
#载入最新配置的文件
supervisorctl reload
#根据最新的配置文件，启动新配置或有改动的进程
supervisorctl update
#查看日志文件
/var/log/supervisor</code></pre></div>]]></description>
            <guid isPermaLink="false">Supervisor进程守护监控</guid>
        </item>
        <item>
            <title><![CDATA[RabbitMQ通过websocket与前端通信]]></title>
            <description><![CDATA[<link rel="stylesheet" href="https://www.cuiwei.net/static/css/github-markdown.min.css?t=20260430" type="text/css" media="screen" /><div class="markdown-body"><p>本文主要介绍的是<code>RabbitMQ</code>的一个插件 —— <code>STOMP</code>，还有一个<code>MQTT</code>插件，也是不错的选择，详见：<a href="http://www.cuiwei.net/p/1135009574">RabbitMQ插件之MQTT</a></p>
<p>如何安装rabbitmq，请移步：<a href="http://www.cuiwei.net/p/1371869141">http://www.cuiwei.net/p/1371869141</a></p>
<p>启用stomp插件</p>
<pre><code>vi enabled_plugins
[...,rabbitmq_stomp,rabbitmq_web_stomp].</code></pre>
<p>重启rabbitmq后，访问 <a href="http://localhost:15672/">RabbitMQ Management</a></p>
<p><img src="https://www.cuiwei.net/data/upload/2022-06-27/165630271927493.jpg" alt="WX202206271204282x.png" /></p>
<p>可以看到</p>
<ul>
<li>http/web-stomp服务(ws)已经启动了，在15674端口上了</li>
<li>https/web-stomp服务(wss)已经启动了，在15673端口上了</li>
<li>stomp服务(tcp)已经启动了，在61613端口上</li>
<li>stomp/ssl服务(ssl)已经启动了，在61614端口上</li>
</ul>
<h1>tcp/ssl</h1>
<p>tcp://localhost:61613</p>
<p>ssl://localhost:61614</p>
<pre><code>cat /etc/rabbitmq/conf.d/23-stomp-ssl.conf
ssl_options.cacertfile = /etc/rabbitmq/cert/ca.cer
ssl_options.certfile   = /etc/rabbitmq/cert/www.cuiwei.net.pem
ssl_options.keyfile    = /etc/rabbitmq/cert/www.cuiwei.net.key
ssl_options.verify     = verify_peer
ssl_options.fail_if_no_peer_cert  = true

stomp.listeners.tcp.1 = 61613
# default TLS-enabled port for STOMP connections
stomp.listeners.ssl.1 = 61614</code></pre>
<h1>TLS (WSS)</h1>
<p>具体项目中，是使用<code>ws</code>，还是<code>wss</code>，取决于当前域名，如果当前域名是<code>https</code>，就只能使用<code>wss</code>，如果当前域名是<code>http</code>，就只能使用<code>ws</code></p>
<p>这个插件默认支持<code>ws</code>，直接用<code>ws://127.0.0.1:15674/ws</code>就行</p>
<p>wss需要一些配置才能使用<code>wss://127.0.0.1:15673/ws</code>1️⃣</p>
<pre><code>cat /etc/rabbitmq/conf.d/25-web-stomp-ssl.conf
web_stomp.tcp.port = 15674
web_stomp.ssl.port       = 15673
web_stomp.ssl.backlog    = 1024
web_stomp.ssl.cacertfile = /etc/rabbitmq/cert/ca.cer
web_stomp.ssl.certfile   = /etc/rabbitmq/cert/www.cuiwei.net.pem
web_stomp.ssl.keyfile    = /etc/rabbitmq/cert/www.cuiwei.net.key
# web_stomp.ssl.password   = changeme</code></pre>
<p>如上，用到3个文件，这些文件和配置https用的是一样的。</p>
<p>详见：<a href="http://www.cuiwei.net/p/1135009574/">http://www.cuiwei.net/p/1135009574/</a></p>
<h1>应用场景</h1>
<p>做过微信h5支付的应该都知道，用户支付完会出现等待页面，在这个页面 前端会通过不断请求服务端接口的方式 获取支付结果。这种轮询的方式会对服务器造成一定的压力，下面我们就用 RabbitMQ 实现一下</p>
<p>先看下结果，如下图。客户端订阅order-99的队列，服务向order-99推数据，客户端可以实时收到</p>
<p><img src="https://www.cuiwei.net/data/upload/2021-09-25/163256272131422.jpg" alt="20210925173525.jpg" /></p>
<h1>测试工具</h1>
<p><a href="http://jiangxy.github.io/websocket-debug-tool">http://jiangxy.github.io/websocket-debug-tool</a></p>
<p><img src="https://www.cuiwei.net/data/upload/2022-06-26/165625052952203.jpg" alt="WX202206262134142x.png" /></p>
<h1>代码</h1>
<p>composer.json</p>
<pre><code>{
  "require": {
    "stomp-php/stomp-php": "^5.0"
  }
}</code></pre>
<p>test.php</p>
<pre><code>&lt;?php
require_once __DIR__ . '/../../vendor/autoload.php';

use Stomp\Client;
use Stomp\SimpleStomp;
use Stomp\Transport\Bytes;

// make a connection
$client = new Client('tcp://docker-rabbitmq:61613');
$client-&gt;setLogin('guest', 'guest');
$stomp = new SimpleStomp($client);

// send a message to the queue
$body = ['id'=&gt;99, 'title'=&gt;'order-99', 'status'=&gt;0];
$bytesMessage = new Bytes(json_encode($body, JSON_UNESCAPED_UNICODE));
$stomp-&gt;send('order-99', $bytesMessage);
echo 'Sending message: ';
print_r(json_encode($body, JSON_UNESCAPED_UNICODE) . "\n");

//$stomp-&gt;subscribe('order-99', 'binary-sub-test', 'client-individual');
//$msg = $stomp-&gt;read();
//
//// extract
//if ($msg != null) {
//    echo 'Received message: ';
//    print_r($msg-&gt;body . "\n");
//    // mark the message as received in the queue
//    $stomp-&gt;ack($msg);
//} else {
//    echo "Failed to receive a message\n";
//}
//
//$stomp-&gt;unsubscribe('order-99', 'binary-sub-test');</code></pre>
<p>index.html</p>
<pre><code>&lt;!DOCTYPE html&gt;
&lt;html lang="en"&gt;
&lt;head&gt;
    &lt;meta charset="UTF-8"&gt;
    &lt;title&gt;My WebSocket&lt;/title&gt;
    &lt;script src="https://cdn.bootcdn.net/ajax/libs/stomp.js/2.3.3/stomp.min.js"&gt;&lt;/script&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;script&gt;
    if (typeof WebSocket == 'undefined') {
        console.log('不支持websocket')
    }

    // 初始化 ws 对象
    var ws = new WebSocket('ws://localhost:15674/ws');
    var client = Stomp.over(ws);

    var on_connect = function() {
        client.subscribe("order-99", function(message) {
            let result = message.body;
            console.log("收到数据:"+result)
            let r=JSON.parse(result);
            if (r.status===1){
                console.log('已支付');
                message.ack();//确认消息
            }
            // message.nack();//消息驳回，要求ack模式为{ack: 'client-individual'}
            //https://www.cnblogs.com/piaolingzxh/p/5463918.html

        }, {ack: 'client'});
        console.log('connected');
    };
    var on_error =  function() {
        console.log('error');
    };
    // 连接RabbitMQ
    //参数依次为：用户名，密码，连接后，出错，虚拟主机名
    client.connect('guest', 'guest', on_connect, on_error, 'docker-rabbitmq');
    // console.log("&gt;&gt;&gt;连接上http://localhost:15674");
&lt;/script&gt;
&lt;/body&gt;
&lt;/html&gt;</code></pre>
<h1>关于STOMP</h1>
<p><a href="https://www.cnblogs.com/piaolingzxh/p/5463918.html">https://www.cnblogs.com/piaolingzxh/p/5463918.html</a></p>
<p><a href="https://my.oschina.net/feinik/blog/853875">https://my.oschina.net/feinik/blog/853875</a></p>
<p><a href="https://www.cnblogs.com/goloving/p/10746378.html">https://www.cnblogs.com/goloving/p/10746378.html</a></p></div>]]></description>
            <guid isPermaLink="false">RabbitMQ通过websocket与前端通信</guid>
        </item>
        <item>
            <title><![CDATA[使用 Beanstalk 实现微信支付的异步通知]]></title>
            <description><![CDATA[<link rel="stylesheet" href="https://www.cuiwei.net/static/css/github-markdown.min.css?t=20260430" type="text/css" media="screen" /><div class="markdown-body"><h1>Beanstalk介绍</h1>
<p>Beanstalk是一个基于内存的（binlog持久化到硬盘），事件驱动（libevent），简单、快速的任务队列，支持大部分编程语言，将前台的任务转为后台异步处理，为web开发提供更高弹性。它可以支持多个server（客户端支持），一个任务只会被投递到一台server，一个任务只会被一个消费者获取（Reverse）。</p>
<p>使用Beanstalk任务队列提升PHP异步处理能力，降低程序耦合度，使前台更专注，后台处理耗时、扩展性任务（也可以使用其他语言开发），使得web架构更具扩展性。</p>
<p>相比RabbitMQ，Beanstalk作为一个任务队列，设计比较简单，支持以下特性：</p>
<ul>
<li>优先级（priority），可以对任务进行优先处理（或降级），越小的值优先级越高（0～4,294,967,295），默认按先进先出（FIFO）</li>
<li>延迟执行（delay），一个任务创建完成并稍后再执行（比如等待主从同步）</li>
<li>超时重试（TTR），一个任务没有在指定时间内完成，将会被重新投递，由其他客户端处理。客户端也可以主动进行延时（touch)或重新入队（release）</li>
<li>隐藏（bury），一个任务执行失败了，可以先隐藏，隐藏的任务可以被重新激活（kick）.</li>
</ul>
<h1>应用场景</h1>
<p>对接过微信支付的应该会知道，用户支付成功后，微信会给我们发一个异步通知，如果我们没有正确处理，这个通知会发多次，直到我们返回正确的标识。</p>
<p>今天我们就用 Beanstalk 实现一下这个通知（通知频率为15s/15s/30s/3m/10m/20m/30m/30m/30m/60m/3h/3h/3h/6h/6h - 总计 24h4m）</p>
<p>先看下结果，如下图，15s/15s/30s/3m都是正常的，第10m出现了1s误差，这个也算正常。后面的就不展示了，时间太长</p>
<p><img src="https://www.cuiwei.net/data/upload/2021-09-25/163254742867588.jpg" alt="微信图片_20210925131906.png" /></p>
<h1>目录结构</h1>
<h1>测试</h1>
<pre><code>composer up -d

访问 producer.php，向队列中推一条任务

执行 php consumer.php，结果如上图</code></pre>
<h1>代码</h1>
<p>docker-compose.yml</p>
<pre><code>version: '3'

networks:
  web-network:

services:
  docker-beanstalkd:
    # registry.cn-hangzhou.aliyuncs.com/cuiw/beanstalkd:20210923
    image: "beanstalkd:20210923"1️⃣
    restart: always
    tty: true
    volumes:
      - ./beanstalkd/data:/var/lib/beanstalkd
    ports:
      - "11300:11300"
    networks:
      - web-network</code></pre>
<p>composer.json</p>
<pre><code>{
  "require": {
    "pda/pheanstalk": "^4.0"
  }
}</code></pre>
<p>beanstalkd.php</p>
<pre><code>&lt;?php
require __DIR__ . '/../../vendor/autoload.php';

use Pheanstalk\Pheanstalk;

class beanstalkd{
    public $conf=[
        'host'=&gt;'docker-beanstalkd',
        'port'=&gt;11300,
        'timeout'=&gt;10,
    ];

    public static function factory(){
        return new self();
    }

    public function handle(): Pheanstalk {
        return Pheanstalk::create($this-&gt;conf['host'], $this-&gt;conf['port'], $this-&gt;conf['timeout']);
    }
}</code></pre>
<p>producer.php</p>
<pre><code>&lt;?php
require './beanstalkd.php';

$pheanstalk = beanstalkd::factory()-&gt;handle();

$re=$pheanstalk
    -&gt;useTube('testtube')
    -&gt;put(
        json_encode(['test' =&gt; 'data'], JSON_UNESCAPED_UNICODE),
        1024,
        30,
        60
    );
echo json_encode(['id'=&gt;$re-&gt;getId(), 'data'=&gt;json_decode($re-&gt;getData(), true)], JSON_UNESCAPED_UNICODE);</code></pre>
<p>consumer.php</p>
<pre><code>&lt;?php
require './beanstalkd.php';

$pheanstalk = beanstalkd::factory()-&gt;handle();
$pheanstalk-&gt;watch('testtube');
while (true) {
    $job = $pheanstalk-&gt;reserve();

    //echo $job-&gt;getData().PHP_EOL;
    //处理任务
    exec('php result.php', $re, $status);
    $data=json_decode($re[0], true);
    if ($data['err']==0) {
        //删除任务
        $pheanstalk-&gt;delete($job);
    } else {
        $stats = $pheanstalk-&gt;statsJob($job);
        echo date("Y-m-d H:i:s").':'.json_encode($stats)."\n".PHP_EOL;
        if ($stats['releases'] &gt;=0 &amp;&amp; $stats['releases'] &lt;15) {
            //15次以下延时返回队列，通知频率为15s/15s/30s/3m/10m/20m/30m/30m/30m/60m/3h/3h/3h/6h/6h - 总计 24h4m
            $timer = [15,15,30,180,600,1200,1800,1800,1800,3600,10800,10800,10800,21600,21600];
            $pheanstalk-&gt;release($job, 0, $timer[$stats['releases']]);

        }else{
            //错误次数过多时 bury
            $pheanstalk-&gt;bury($job);
        }

    }

}</code></pre>
<p>result.php</p>
<pre><code>&lt;?php
//处理过程，err==0为成功，
echo json_encode(['err'=&gt;1, 'data'=&gt;[]]);</code></pre>
<h1>其他</h1>
<p>1️⃣ 构建 beanstalkd 容器
我已经build一个并上传到阿里云，可以直接使用：registry.cn-hangzhou.aliyuncs.com/cuiw/beanstalkd:20210923</p>
<pre><code>git clone git@github.com:beanstalkd/beanstalkd.git

cd beanstalkd

docker build -t beanstalkd:20210923 .</code></pre></div>]]></description>
            <guid isPermaLink="false">使用 Beanstalk 实现微信支付的异步通知</guid>
        </item>
        <item>
            <title><![CDATA[Redis 中的订阅消息转发到 WebSocket 客户端]]></title>
            <description><![CDATA[<link rel="stylesheet" href="https://www.cuiwei.net/static/css/github-markdown.min.css?t=20260430" type="text/css" media="screen" /><div class="markdown-body"><p>WebSocketTest.php</p>
<pre><code>&lt;?php
class WebSocketTest {
    public \Swoole\WebSocket\Server $server;

    public function __construct() {
        $this-&gt;server = new Swoole\WebSocket\Server("0.0.0.0", 9502);
        $this-&gt;server-&gt;on('open', function (Swoole\WebSocket\Server $server, $request) {
            echo "server: handshake success with fd{$request-&gt;fd}\n";
        });
        $this-&gt;server-&gt;on('message', function (Swoole\WebSocket\Server $server, $frame) {
            echo "receive from {$frame-&gt;fd}:{$frame-&gt;data},opcode:{$frame-&gt;opcode},fin:{$frame-&gt;finish}\n";
            $server-&gt;push($frame-&gt;fd, "this is server");
        });
        $this-&gt;server-&gt;on('close', function ($ser, $fd) {
            echo "client {$fd} closed\n";
        });
        $this-&gt;server-&gt;on('workerStart', function ($server, $worker_id) {
            $redis = new Swoole\Coroutine\Redis();
            $redis-&gt;connect('docker-redis', 6379);
            if ($redis-&gt;subscribe(['cctv1'])){
                while ($msg = $redis-&gt;recv()) {
                    echo json_encode($msg, JSON_UNESCAPED_UNICODE).PHP_EOL;
                    if ($msg[0] == 'message') {
                        echo 'connections:'.json_encode(iterator_to_array($server-&gt;connections, true), JSON_UNESCAPED_UNICODE).PHP_EOL;
                        foreach ($server-&gt;connections as $fd) {
                            $server-&gt;push($fd, $msg[2]);
                        }
                    }
                }
            }
        });

        $this-&gt;server-&gt;start();
    }
}

new WebSocketTest();</code></pre>
<p>index.html</p>
<pre><code>&lt;!DOCTYPE html&gt;
&lt;html lang="en"&gt;
&lt;head&gt;
    &lt;meta charset="UTF-8"&gt;
    &lt;title&gt;Title&lt;/title&gt;
  &lt;script&gt;
      var wsServer = 'ws://localhost:9502';
      var websocket = new WebSocket(wsServer);
      websocket.onopen = function (evt) {
          console.log("Connected to WebSocket server.");
          // websocket.send('fronted..');//向服务端发消息
      };

      websocket.onclose = function (evt) {
          console.log("Disconnected");
      };

      websocket.onmessage = function (evt) {
          console.log('Retrieved data from server: ' + evt.data);
      };

      websocket.onerror = function (evt, e) {
          console.log('Error occured: ' + evt.data);
      };
  &lt;/script&gt;
&lt;/head&gt;
&lt;body&gt;

&lt;/body&gt;
&lt;/html&gt;</code></pre>
<h1>测试</h1>
<pre><code>开启服务
php WebSocketTest.php

浏览器访问index.html,并打开控制台

执行redis操作
redis-cli
127.0.0.1:6379&gt; publish cctv1 haha

不出意外，浏览器控制台就会看到 haha
</code></pre></div>]]></description>
            <guid isPermaLink="false">Redis 中的订阅消息转发到 WebSocket 客户端</guid>
        </item>
        <item>
            <title><![CDATA[使用puppeteer爬取spa单页（vue/react）]]></title>
            <description><![CDATA[<link rel="stylesheet" href="https://www.cuiwei.net/static/css/github-markdown.min.css?t=20260430" type="text/css" media="screen" /><div class="markdown-body"><h1>docker 部署 puppeteer</h1>
<p><img src="https://www.cuiwei.net/data/upload/2021-09-15/163170174248687.jpg" alt="WX202109151828422x.png" /></p>
<p>官方提供的Dockerfile1️⃣</p>
<pre><code>FROM node:12-slim

# Install latest chrome dev package and fonts to support major charsets (Chinese, Japanese, Arabic, Hebrew, Thai and a few others)
# Note: this installs the necessary libs to make the bundled version of Chromium that Puppeteer
# installs, work.
RUN apt-get update \
    &amp;&amp; apt-get install -y wget gnupg \
    &amp;&amp; wget -q -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | apt-key add - \
    &amp;&amp; sh -c 'echo "deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main" &gt;&gt; /etc/apt/sources.list.d/google.list' \
    &amp;&amp; apt-get update \
    &amp;&amp; apt-get install -y google-chrome-stable fonts-ipafont-gothic fonts-wqy-zenhei fonts-thai-tlwg fonts-kacst fonts-freefont-ttf libxss1 \
      --no-install-recommends \
    &amp;&amp; rm -rf /var/lib/apt/lists/*

# If running Docker &gt;= 1.13.0 use docker run's --init arg to reap zombie processes, otherwise
# uncomment the following lines to have `dumb-init` as PID 1
# ADD https://github.com/Yelp/dumb-init/releases/download/v1.2.2/dumb-init_1.2.2_x86_64 /usr/local/bin/dumb-init
# RUN chmod +x /usr/local/bin/dumb-init
# ENTRYPOINT ["dumb-init", "--"]

# Uncomment to skip the chromium download when installing puppeteer. If you do,
# you'll need to launch puppeteer with:
#     browser.launch({executablePath: 'google-chrome-stable'})
# ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD true

# Install puppeteer so it's available in the container.
RUN npm init -y &amp;&amp;  \
    npm i puppeteer \
    # Add user so we don't need --no-sandbox.
    # same layer as npm install to keep re-chowned files from using up several hundred MBs more space
    &amp;&amp; groupadd -r pptruser &amp;&amp; useradd -r -g pptruser -G audio,video pptruser \
    &amp;&amp; mkdir -p /home/pptruser/Downloads \
    &amp;&amp; chown -R pptruser:pptruser /home/pptruser \
    &amp;&amp; chown -R pptruser:pptruser /node_modules \
    &amp;&amp; chown -R pptruser:pptruser /package.json \
    &amp;&amp; chown -R pptruser:pptruser /package-lock.json

# Run everything after as non-privileged user.
USER pptruser

CMD ["google-chrome-stable"]</code></pre>
<p>构建</p>
<pre><code># 需要挂代理，我build了一个，并上传到了阿里云，可以直接使用：registry.cn-hangzhou.aliyuncs.com/cuiw/puppeteer-chrome-linux:20210916
cd puppeteer
docker build -t puppeteer-chrome-linux .</code></pre>
<h1>puppeteer/server.js</h1>
<p>获取ks的视频链接，接收一个url参数（在ks中分享出来的链接）</p>
<pre><code>const http = require("http");
const puppeteer = require('puppeteer');
const server = http.createServer(function (req, res) {
  const urlObj = new URL('http://localhost:3000' + req.url)
  // console.log(urlObj)
    let url = urlObj.searchParams.get('url');
    if (null !== url &amp;&amp; url.length &gt; 0) {
      if (urlObj.pathname === '/ks/video') {
        (async () =&gt; {
        const browser = await puppeteer.launch({
          args:['--no-sandbox', '--disable-setuid-sandbox'],
        });
        const page = await browser.newPage();
        // 设置浏览器信息
        await page.emulate(puppeteer.devices['iPhone X']);
        await page.goto(url);
        let video = await page.$eval('#video-player', el =&gt; el.src);
        console.log(video)

        res.writeHead(200, {"Content-Type": "application/json"});
        let json = JSON.stringify({'url': video });
        res.end(json);

        await browser.close();
      })();
    }
  }else res.end();

});

server.listen(3000);</code></pre>
<h1>docker-compose.yml</h1>
<pre><code>version: '3'

networks:
  puppeteer:

services:
  puppeteer:
    # registry.cn-hangzhou.aliyuncs.com/cuiw/puppeteer-chrome-linux:20210916
    image: puppeteer-chrome-linux
    container_name: puppeteer
    command: bash -c "/usr/local/bin/node /app/server.js"
    ports:
      - 3000:3000
    volumes:
      - ./puppeteer:/app
    networks:
      - puppeteer
#    entrypoint: ["sh", "-c", "sleep infinity"]</code></pre>
<h1>测试</h1>
<pre><code>http://localhost:3000/ks/video?url=https://v.kuaishou.com/dlFLDr

# 如果没意外，将会输出
{"url":"https://..."}</code></pre>
<h1>其他相关</h1>
<p><a href="https://hub.docker.com/r/zenato/puppeteer-renderer">https://hub.docker.com/r/zenato/puppeteer-renderer</a></p>
<h1>备注</h1>
<p>1️⃣<a href="https://github.com/puppeteer/puppeteer/blob/main/docs/troubleshooting.md#running-puppeteer-in-docker">https://github.com/puppeteer/puppeteer/blob/main/docs/troubleshooting.md#running-puppeteer-in-docker</a></p></div>]]></description>
            <guid isPermaLink="false">使用puppeteer爬取spa单页（vue/react）</guid>
        </item>
        <item>
            <title><![CDATA[在k8s上部署一个前后端分离的项目]]></title>
            <description><![CDATA[<link rel="stylesheet" href="https://www.cuiwei.net/static/css/github-markdown.min.css?t=20260430" type="text/css" media="screen" /><div class="markdown-body"><h1>4台虚拟机</h1>
<table>
<thead>
<tr>
<th>节点</th>
<th>系统</th>
<th>IP</th>
<th>备注</th>
</tr>
</thead>
<tbody>
<tr>
<td>master</td>
<td>CentOS-8</td>
<td>192.168.10.90</td>
<td>主节点</td>
</tr>
<tr>
<td>node1</td>
<td>CentOS-8</td>
<td>192.168.10.91</td>
<td>节点1</td>
</tr>
<tr>
<td>node2</td>
<td>CentOS-8</td>
<td>192.168.10.92</td>
<td>节点2</td>
</tr>
<tr>
<td>NFS</td>
<td>CentOS-8</td>
<td>192.168.10.99</td>
<td>文件系统，用于存储项目文件及日志</td>
</tr>
</tbody>
</table>
<h1>准备</h1>
<ul>
<li><a href="http://www.cuiwei.net/p/1088442939">基于vagrant搭建k8s集群</a></li>
<li><a href="http://www.cuiwei.net/p/1590637562">linux 搭建 nfs 服务</a></li>
</ul>
<h3>一个前后端分离的项目</h3>
<blockquote>
<p>用户端(shop-h5)：vue + vant</p>
<p>管理员端(shop-admin)：vue + element ui</p>
<p>服务端(shop)：php + mysql + nginx + redis</p>
</blockquote>
<pre><code>[root@nfsFileSystem vagrant]# ls /nfs/data/www/
shop  shop-admin  shop-h5</code></pre>
<h3>域名及证书文件</h3>
<pre><code>shop.cw.ltd.key
shop.cw.ltd.pem</code></pre>
<h3>配置文件</h3>
<pre><code>#pv/pvc
log-pv.yaml
log-pvc.yaml
www-pv.yaml
www-pvc.yaml

#nginx配置
nginx-configmap.yaml

#ingress
shop-ingress.yaml

#nginx
shop-nginx-deployment.yaml
shop-nginx-service.yaml

#php
shop-php-deployment.yaml
shop-php-service.yaml

#redis
shop-redis-deployment.yaml
shop-redis-service.yaml

#network
web-network.yaml
</code></pre>
<h1>配置文件下载</h1>
<p><a href="https://github.com/chudaozhe/shop-k8s">https://github.com/chudaozhe/shop-k8s</a></p></div>]]></description>
            <guid isPermaLink="false">在k8s上部署一个前后端分离的项目</guid>
        </item>
        <item>
            <title><![CDATA[基于docker的 EFK 日志分析系统]]></title>
            <description><![CDATA[<link rel="stylesheet" href="https://www.cuiwei.net/static/css/github-markdown.min.css?t=20260430" type="text/css" media="screen" /><div class="markdown-body"><blockquote>
<p>EFK 不是一个软件，而是一套解决方案。EFK 是三个开源软件的缩写，Elasticsearch，Fluentd，Kibana。其中 ELasticsearch 负责日志分析和存储，Fluentd 负责日志收集，Kibana 负责界面展示。它们之间互相配合使用，完美衔接，高效的满足了很多场合的应用，是目前主流的一种日志分析系统解决方案。
本文主要基于Fluentd实时读取日志文件的方式获取日志内容，将日志发送至Elasticsearch，并通过Kibana展示。</p>
</blockquote>
<p><img src="https://www.cuiwei.net/data/upload/2021-09-09/163117893545343.jpg" alt="ee.JPG" /></p>
<p><img src="https://www.cuiwei.net/data/upload/2021-09-09/163120184623746.jpg" alt="WechatIMG2066.png" /></p>
<h1>启动服务</h1>
<pre><code>docker-compose -f docker-compose.yml up -d</code></pre>
<p>验证服务是否已正常启动</p>
<blockquote>
<p>注意，es和kibana容器启动后，里面的服务还需要等待片刻</p>
</blockquote>
<pre><code># es
http://localhost:9200
# kibana后台
http://localhost:5601</code></pre>
<h1>获取nginx 日志，将日志发送至Elasticsearch，并通过Kibana展示</h1>
<pre><code>#配置文件
fluentd/fluent.conf
#登录fluent容器，并向access.log添加数据
cat /var/log/nginx/access2.log &gt; /var/log/nginx/access.log</code></pre>
<p>如果没意外，就会在kibana后台（Management-&gt;Stack Management-&gt;数据-&gt;索引管理）看到<code>fluentd.td.nginx.access</code>开头的索引
<img src="https://www.cuiwei.net/data/upload/2021-09-09/163118291695628.jpg" alt="aaa.JPG" />
下一步，创建索引模式(Management-&gt;Stack Management-&gt;Kibana-&gt;索引模式)
<img src="https://www.cuiwei.net/data/upload/2021-09-09/163118324799457.jpg" alt="微信图片_20210909182709.png" />
最后，点击Discover（Analytics-&gt;Discover）就可以看到日志数据了
<img src="https://www.cuiwei.net/data/upload/2021-09-09/163118360138692.jpg" alt="www.JPG" /></p>
<h1>向es，Fluentd推数据</h1>
<h2>php向es推数据</h2>
<p><a href="http://localhost/es.php">http://localhost/es.php</a></p>
<p>详见：/php/www/es.php</p>
<h2>php向Fluentd推数据</h2>
<p>Fluentd再将数据转发到es,最终在Kibana中展示</p>
<p><a href="http://localhost/fluentd.php">http://localhost/fluentd.php</a></p>
<p>详见：/php/www/fluentd.php</p>
<h2>通过url向Fluentd推数据</h2>
<p>Fluentd再将数据转发到es,最终在Kibana中展示</p>
<p>vi fluentd/fluent.conf</p>
<pre><code>&lt;source&gt;
  @type forward
  port 24224
  bind 0.0.0.0
&lt;/source&gt;
# http://&lt;ip&gt;:9880/debug.test?json={"hehe":"uu"}
&lt;source&gt;
  @type http
  port 9880
&lt;/source&gt;

&lt;match debug.test&gt;
  @type elasticsearch
  host elasticsearch
  port 9200
  index_name fluentd.${tag}.%Y%m%d
  &lt;buffer tag,time&gt;
    timekey 1m
  &lt;/buffer&gt;
&lt;/match&gt;</code></pre>
<p><a href="http://localhost:9880/debug.test?json={&quot;haha&quot;:&quot;heihei&quot;}">http://localhost:9880/debug.test?json={&quot;haha&quot;:&quot;heihei&quot;}</a></p>
<p><img src="https://www.cuiwei.net/data/upload/2021-09-09/163120062822900.jpg" alt="WechatIMG2065.png" /></p>
<h1>代码</h1>
<p><a href="https://github.com/chudaozhe/efk">https://github.com/chudaozhe/efk</a></p>
<h1>参考</h1>
<p><a href="https://www.cnblogs.com/zongxiang/p/14745073.html">https://www.cnblogs.com/zongxiang/p/14745073.html</a></p>
<p><a href="https://www.cnblogs.com/sanduzxcvbnm/p/13932087.html">https://www.cnblogs.com/sanduzxcvbnm/p/13932087.html</a></p>
<p><a href="https://docs.fluentd.org/configuration/config-file">https://docs.fluentd.org/configuration/config-file</a></p>
<p><a href="https://docs.fluentd.org/input">https://docs.fluentd.org/input</a></p>
<p><a href="https://docs.fluentd.org/output">https://docs.fluentd.org/output</a></p></div>]]></description>
            <guid isPermaLink="false">基于docker的 EFK 日志分析系统</guid>
        </item>
        <item>
            <title><![CDATA[k8s 持久化存储]]></title>
            <description><![CDATA[<link rel="stylesheet" href="https://www.cuiwei.net/static/css/github-markdown.min.css?t=20260430" type="text/css" media="screen" /><div class="markdown-body"><p>为什么需要持久化存储：</p>
<blockquote>
<p>1、使得使用资源的pod的生命周期与存储卷的生命周期分开
2、使得使用资源的pod在被重启后仍然能够使用之前的存储卷
3、使得使用资源的pod在被调度到其它节点后仍然能够使用之前的存储卷</p>
</blockquote>
<h1>Host类型volume</h1>
<p>测试用 - 仅适用于单节点k8s</p>
<pre><code>apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-deployment
spec:
  selector:
    matchLabels:
      app: nginx
  replicas: 1
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
        - name: nginx
          image: nginx:latest
          ports:
            - containerPort: 80
          volumeMounts:
            - name: www-storage
              mountPath: /usr/share/nginx/html
      volumes:
        - name: www-storage
          hostPath:
            path: /data/www</code></pre>
<p>如上：pod内是目录（/usr/share/nginx/html）挂载到宿主机目录（/data/www）</p>
<h1>PersistentVolumes</h1>
<h2>PV定义</h2>
<pre><code>apiVersion: v1
kind: PersistentVolume
metadata:
  name: www-pv
spec:
  accessModes:
    - ReadWriteMany
  capacity:
    storage: 2Gi    
  nfs:
    path: /nfs/data/www
    server: 192.168.10.99</code></pre>
<h2>PVC定义</h2>
<pre><code># 用于消费PV
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: www-pvc
spec:
  accessModes:
    - ReadWriteMany
  resources:
    requests:
      storage: 2Gi</code></pre>
<h2>在pod中使用</h2>
<pre><code>apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-deployment
spec:
  selector:
    matchLabels:
      app: nginx
  replicas: 1
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
        - name: nginx
          image: nginx:latest
          ports:
            - containerPort: 80
          volumeMounts:
            - name: www-storage
              mountPath: /usr/share/nginx/html
      volumes:
        - name: www-storage
          persistentVolumeClaim:
            claimName: www-pvc</code></pre>
<h1>StorageClass</h1>
<p>详见
<a href="http://www.cuiwei.net/p/1755648759">k8s 使用 StorageClass 动态生成 NFS 类型的 PV</a></p>
<h1>参考</h1>
<p><a href="https://blog.csdn.net/cuixhao110/article/details/105858553">https://blog.csdn.net/cuixhao110/article/details/105858553</a></p></div>]]></description>
            <guid isPermaLink="false">k8s 持久化存储</guid>
        </item>
        <item>
            <title><![CDATA[linux 搭建 nfs 服务]]></title>
            <description><![CDATA[<link rel="stylesheet" href="https://www.cuiwei.net/static/css/github-markdown.min.css?t=20260430" type="text/css" media="screen" /><div class="markdown-body"><p>NFS 是什么？ nfs(network file system) 网络文件系统，是FreeBSD支持的文件系统中的一种，允许网络中的计算机之间通过TCP/IP网络共享资源</p>
<h1>服务端</h1>
<pre><code>yum install -y nfs-utils
# 创建nfs目录
mkdir -p /nfs/data/
# 授予权限
chmod -R 777 /nfs/data
# 编辑export文件
vi /etc/exports
/nfs/data *(rw,no_root_squash,sync)
# 使得配置生效
exportfs -r
# 查看生效
exportfs
# 启动rpcbind、nfs服务
systemctl restart rpcbind &amp;&amp; systemctl enable rpcbind
systemctl restart nfs-server &amp;&amp; systemctl enable nfs-server
# 查看rpc服务的注册情况
rpcinfo -p localhost
</code></pre>
<h1>客户端</h1>
<p>比如k8s，每个节点都应执行</p>
<pre><code>yum -y install nfs-utils
systemctl restart nfs-server &amp;&amp; systemctl enable nfs-server

showmount测试
[root@master ~]# showmount -e 192.168.10.99
Export list for 192.168.10.99:
/nfs/data *

挂载本地目录
mount -t nfs 192.168.10.99:/nfs/data(共享目录) /test(本地目录)</code></pre></div>]]></description>
            <guid isPermaLink="false">linux 搭建 nfs 服务</guid>
        </item>
        <item>
            <title><![CDATA[k8s ingress 两种部署方式nodePort和hostNetwork]]></title>
            <description><![CDATA[<link rel="stylesheet" href="https://www.cuiwei.net/static/css/github-markdown.min.css?t=20260430" type="text/css" media="screen" /><div class="markdown-body"><h1>准备</h1>
<h2>下载deploy.yaml</h2>
<p><a href="https://github.com/kubernetes/ingress-nginx/blob/main/deploy/static/provider/baremetal/deploy.yaml">https://github.com/kubernetes/ingress-nginx/blob/main/deploy/static/provider/baremetal/deploy.yaml</a></p>
<h1>替换镜像url并 创建资源对象</h1>
<pre><code># 替换镜像url
# 192.168.10.104:5000为本地镜像
将
k8s.gcr.io/ingress-nginx/controller:v1.0.0@sha256:0851b34f69f69352bf168e6ccf30e1e20714a264ab1ecd1933e4d8c0fc3215c6
替换为
192.168.10.104:5000/k8s.gcr.io/ingress-nginx/controller:v1.0.0

将
k8s.gcr.io/ingress-nginx/kube-webhook-certgen:v1.0@sha256:f3b6b39a6062328c095337b4cadcefd1612348fdd5190b1dcbcb9b9e90bd8068
替换为
192.168.10.104:5000/k8s.gcr.io/ingress-nginx/kube-webhook-certgen:v1.0

# 创建资源对象
kubectl apply -f deploy.yaml</code></pre>
<h1>安装</h1>
<h2>创建应用</h2>
<pre><code>kubectl apply -f - &lt;&lt;EOF
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-deployment
spec:
  selector:
    matchLabels:
      app: nginx
  replicas: 2 # tells deployment to run 2 pods matching the template
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
        - name: nginx
          image: nginx:latest
          ports:
            - containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
  name: nginx-service
spec:
  type: ClusterIP
  ports:
    - port: 80
  selector:
    app: nginx
EOF</code></pre>
<h2>创建ingress</h2>
<pre><code>kubectl apply -f - &lt;&lt;EOF
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: test-ingress
  annotations:
    kubernetes.io/ingress.class: "nginx"
spec:
  rules:
    - host: "nginx.cw.net"
      http:
        paths:
          - pathType: Prefix
            path: "/"
            backend:
              service:
                name: nginx-service
                port:
                  number: 80
EOF</code></pre>
<h1>部署方式nodePort</h1>
<pre><code>kubectl get pods --all-namespaces -l app.kubernetes.io/name=ingress-nginx

# 查看 ingress 对应节点的端口
[root@master ingress-nginx]# kubectl get services ingress-nginx-controller --namespace=ingress-nginx
NAME                       TYPE       CLUSTER-IP    EXTERNAL-IP   PORT(S)                      AGE
ingress-nginx-controller   NodePort   10.1.61.167   &lt;none&gt;        80:30447/TCP,443:30976/TCP   4h41m
# 修改hosts
# 192.168.10.90为主节点ip
echo '192.168.10.90 nginx.cw.net'&gt;&gt;/etc/hosts

#我们这里设置了replicas=2，会产生两个容器，分别进入两个容器，增加一个文件
echo 'aaa' &gt;/usr/share/nginx/html/a.html
# 通过浏览器访问 http://nginx.cw.net:30447/a.html</code></pre>
<h1>部署方式hostNetwork</h1>
<pre><code>1. kind: Deployment =&gt; kind: DaemonSet
2. 
添加 hostNetwork: true
vi deploy.yaml
...
      nodeSelector:
        kubernetes.io/os: linux
      serviceAccountName: ingress-nginx
      hostNetwork: true
      terminationGracePeriodSeconds: 300
...

kubectl apply -f deploy.yaml

# 查看节点的对外ip
[root@master ingress-nginx]# kubectl get po -n ingress-nginx -owide
NAME                                      READY   STATUS      RESTARTS   AGE     IP              NODE    NOMINATED NODE   READINESS GATES
ingress-nginx-admission-create--1-h7bvr   0/1     Completed   0          5h59m   10.244.1.4      node1   &lt;none&gt;           &lt;none&gt;
ingress-nginx-admission-patch--1-dg4m6    0/1     Completed   3          5h59m   10.244.2.4      node2   &lt;none&gt;           &lt;none&gt;
ingress-nginx-controller-cpdqw            1/1     Running     0          6m35s   192.168.10.92   node2   &lt;none&gt;           &lt;none&gt;
ingress-nginx-controller-tsdvz            1/1     Running     0          6m35s   192.168.10.91   node1   &lt;none&gt;           &lt;none&gt;

# 修改hosts
# 192.168.10.91，192.168.10.92为子节点ip
echo -e '192.168.10.91 nginx.cw.net\n192.168.10.92 nginx.cw.net'&gt;&gt;/etc/hosts

#我们这里设置了replicas=2，会产生两个容器，分别进入两个容器，增加一个文件
echo 'aaa' &gt;/usr/share/nginx/html/a.html

# 通过浏览器访问
http://nginx.cw.net/a.html</code></pre>
<h1>HTTPS</h1>
<p>证书文件通过阿里云免费申请</p>
<pre><code>#创建secret
kubectl create secret tls test-ingress-secret --cert=nginx.cw.net.pem --key=nginx.cw.net.key

kubectl get secret
kubectl describe secret test-ingress-secret

vi test-ingress.yml
...
spec:
  tls:
    - hosts:
        - nginx.cw.net
      secretName: test-ingress-secret
  rules:
    - host: "nginx.cw.net"
...

kubectl apply -f test-ingress.yml</code></pre>
<h1>清理</h1>
<pre><code>kubectl delete -f deploy.yaml
kubectl delete -n default deployment nginx-deployment
kubectl delete -n default service nginx-service
kubectl delete -n default ingress test-ingress</code></pre>
<h1>参考</h1>
<p><a href="https://huangzhongde.cn/istio/Chapter3/Chapter3-1.html">https://huangzhongde.cn/istio/Chapter3/Chapter3-1.html</a></p></div>]]></description>
            <guid isPermaLink="false">k8s ingress 两种部署方式nodePort和hostNetwork</guid>
        </item>
        <item>
            <title><![CDATA[k8s 安装 dashboard]]></title>
            <description><![CDATA[<link rel="stylesheet" href="https://www.cuiwei.net/static/css/github-markdown.min.css?t=20260430" type="text/css" media="screen" /><div class="markdown-body"><p>在 master 节点执行</p>
<blockquote>
<p>本例 k8s 是 v1.22.0，对应的 dashboard 是 -- 这个版本，具体去这里查看对应的版本 <a href="https://github.com/kubernetes/dashboard/releases">https://github.com/kubernetes/dashboard/releases</a></p>
</blockquote>
<pre><code>wget https://raw.githubusercontent.com/kubernetes/dashboard/v2.3.1/aio/deploy/recommended.yaml
# 创建 pod
kubectl apply -f recommended.yaml

[root@master vagrant]# kubectl get pods -n kubernetes-dashboard
NAME                                         READY   STATUS    RESTARTS   AGE
dashboard-metrics-scraper-856586f554-4xbht   1/1     Running   0          24m
kubernetes-dashboard-67484c44f6-5m6h6        1/1     Running   0          24m</code></pre>
<p>dashboard 默认的服务的类型是ClusterIP，不便于我们通过浏览器访问，因此需要改成NodePort型的</p>
<pre><code>[root@master vagrant]# kubectl get svc --all-namespaces
NAMESPACE              NAME                        TYPE        CLUSTER-IP     EXTERNAL-IP   PORT(S)                  AGE
default                kubernetes                  ClusterIP   10.1.0.1       &lt;none&gt;        443/TCP                  114m
kube-system            kube-dns                    ClusterIP   10.1.0.10      &lt;none&gt;        53/UDP,53/TCP,9153/TCP   113m
kubernetes-dashboard   dashboard-metrics-scraper   ClusterIP   10.1.112.227   &lt;none&gt;        8000/TCP                 3m39s
kubernetes-dashboard   kubernetes-dashboard        ClusterIP   10.1.2.145     &lt;none&gt;        443/TCP                  3m40s

# 删除
kubectl delete service kubernetes-dashboard --namespace=kubernetes-dashboard</code></pre>
<p>创建配置文件</p>
<pre><code>vim dashboard-svc.yaml

# 内容
kind: Service
apiVersion: v1
metadata:
  labels:
    k8s-app: kubernetes-dashboard
  name: kubernetes-dashboard
  namespace: kubernetes-dashboard
spec:
  type: NodePort
  ports:
    - port: 443
      targetPort: 8443
  selector:
    k8s-app: kubernetes-dashboard

# 执行
kubectl apply -f dashboard-svc.yaml</code></pre>
<p>想要访问dashboard服务，就要有访问权限，创建kubernetes-dashboard管理员角色</p>
<pre><code>vi dashboard-svc-account.yaml

apiVersion: v1
kind: ServiceAccount
metadata:
  name: dashboard-admin
  namespace: kube-system
---
kind: ClusterRoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
  name: dashboard-admin
subjects:
  - kind: ServiceAccount
    name: dashboard-admin
    namespace: kube-system
roleRef:
  kind: ClusterRole
  name: cluster-admin
  apiGroup: rbac.authorization.k8s.io

# 执行
kubectl apply -f dashboard-svc-account.yaml</code></pre>
<p>获取 token</p>
<pre><code>[root@master1 ~]# kubectl get secret -n kube-system |grep admin|awk '{print $1}'
dashboard-admin-token-bwgjv

# 复制下面的 token,后面登陆的时候要用到
[root@master1 ~]# kubectl describe secret dashboard-admin-token-bwgjv -n kube-system|grep '^token'|awk '{print $2}'</code></pre>
<p>访问</p>
<pre><code>https://192.168.10.90:31315/</code></pre>
<h1>参考</h1>
<p><a href="https://blog.csdn.net/mshxuyi/article/details/108425487">https://blog.csdn.net/mshxuyi/article/details/108425487</a></p></div>]]></description>
            <guid isPermaLink="false">k8s 安装 dashboard</guid>
        </item>
        <item>
            <title><![CDATA[基于vagrant搭建k8s集群]]></title>
            <description><![CDATA[<link rel="stylesheet" href="https://www.cuiwei.net/static/css/github-markdown.min.css?t=20260430" type="text/css" media="screen" /><div class="markdown-body"><h1>3台虚拟机</h1>
<table>
<thead>
<tr>
<th>节点</th>
<th>系统</th>
<th>IP</th>
</tr>
</thead>
<tbody>
<tr>
<td>master</td>
<td>CentOS-8</td>
<td>192.168.10.90</td>
</tr>
<tr>
<td>node1</td>
<td>CentOS-8</td>
<td>192.168.10.91</td>
</tr>
<tr>
<td>node2</td>
<td>CentOS-8</td>
<td>192.168.10.92</td>
</tr>
</tbody>
</table>
<h1>构建基础镜像</h1>
<pre><code>cd k8sbase
vagrant box add centos8 ../vagrant_package/CentOS-8-generic.box
vagrant init centos8
#启动
vagrant up
#登陆系统，进行基础配置
vagrant ssh
#切换到root
sudo su
</code></pre>
<h2>基础配置</h2>
<pre><code>#关闭防火墙
systemctl stop firewalld &amp;&amp; systemctl disable firewalld

#关闭 seLinux
sed -i 's/^SELINUX=enforcing$/SELINUX=disabled/' /etc/selinux/config

#关闭 swap 分区
sed -ri 's/.*swap.*/#&amp;/' /etc/fstab

#将桥接的 IPV4 流量传递到 iptables 的链
cat &lt;&lt;EOF &gt;  /etc/sysctl.d/k8s.conf
net.bridge.bridge-nf-call-ip6tables = 1
net.bridge.bridge-nf-call-iptables = 1
EOF
sysctl --system

#同步时间
1 date #查看时间是否正确，不正确则执行以下步骤
2 rm -rf /etc/localtime
3 ln -s /usr/share/zoneinfo/Asia/Shanghai /etc/localtime
4 设置时区
    tzselect
5 同步时间
    yum install -y ntpdate
    ntpdate cn.pool.ntp.org
6 date

# yum源
vi /etc/yum.repos.d/docker-ce.repo
[docker-ce-stable]
name=Docker CE Stable - $basearch
baseurl=https://mirrors.aliyun.com/docker-ce/linux/centos/$releasever/$basearch/stable
enabled=1
gpgcheck=1
gpgkey=https://mirrors.aliyun.com/docker-ce/linux/centos/gpg

vi /etc/yum.repos.d/kubernetes.repo
[kubernetes]
name=Kubernetes
baseurl=https://mirrors.aliyun.com/kubernetes/yum/repos/kubernetes-el7-x86_64/
enabled=1
gpgcheck=1
repo_gpgcheck=1
gpgkey=https://mirrors.aliyun.com/kubernetes/yum/doc/yum-key.gpg https://mirrors.aliyun.com/kubernetes/yum/doc/rpm-package-key.gpg

#安装
yum install docker-ce kubelet kubeadm kubectl
systemctl enable docker
systemctl enable kubelet

#注意，这里我遇到一个问题，docker的驱动类型和kubelet的驱动类型不同，需要统一一下。修改docker的驱动类型为systemd
cat &gt; /etc/docker/daemon.json &lt;&lt;EOF
{
  "exec-opts": ["native.cgroupdriver=systemd"]
}
EOF</code></pre>
<h2>打包镜像</h2>
<pre><code>vagrant halt
vagrant package --output k8s.box
vagrant box add k8s k8s.box

#此镜像已上传到vagrant cloud，可以直接使用
https://app.vagrantup.com/cuiw/boxes/k8s-centos8-base</code></pre>
<h1>基于基础镜像批量生成虚拟机</h1>
<pre><code># Vagrantfile
# vim: set ft=ruby ts=2 :

Vagrant.configure("2") do |config|
  config.vm.box = "k8s"
  config.vm.provider "virtualbox" do |v|
    v.memory = 2048
    v.cpus = 2
  end

  config.vm.define :master do |cfg|
    cfg.vm.hostname = "master"
    cfg.vm.network :public_network, ip: "192.168.10.90"
  end

  config.vm.define :node1 do |cfg|
    cfg.vm.hostname = "node1"
    cfg.vm.network :public_network, ip: "192.168.10.91"
  end

  config.vm.define :node2 do |cfg|
    cfg.vm.hostname = "node2"
    cfg.vm.network :public_network, ip: "192.168.10.92"
  end

  config.vm.synced_folder "./", "/vagrant"
end</code></pre>
<pre><code>cd k8s-demo
# 启动3台虚拟机
vagrant up
# 开一个新窗口
vagrant ssh master
# 开一个新窗口
vagrant ssh node1
# 开一个新窗口
vagrant ssh node2</code></pre>
<h1>master 节点初始化</h1>
<pre><code># vi /etc/sysconfig/kubelet
KUBELET_EXTRA_ARGS="--node-ip=192.168.10.90 --pod-infra-container-image=registry.cn-hangzhou.aliyuncs.com/google_containers/pause:3.5"

service kubelet restart

#kubeadm 预下载基础镜像
kubeadm config images pull --image-repository registry.cn-hangzhou.aliyuncs.com/google_containers

#初始化
kubeadm init \
--kubernetes-version v1.22.0 \
--apiserver-advertise-address=192.168.10.90 \
--ignore-preflight-errors=all \
--image-repository registry.cn-hangzhou.aliyuncs.com/google_containers \
--service-cidr=10.1.0.0/16 \
--pod-network-cidr=10.244.0.0/16</code></pre>
<p>POD的网段为: 10.244.0.0/16， api server地址就是master本机IP。</p>
<p>这一步很关键，由于kubeadm 默认从官网k8s.grc.io下载所需镜像，国内无法访问，因此需要通过–image-repository指定阿里云镜像仓库地址。1️⃣</p>
<p>这一步也可能会因为kubelet启动失败而失败2️⃣</p>
<p>参数解释：</p>
<pre><code>–kubernetes-version: 用于指定k8s版本；
–apiserver-advertise-address：用于指定kube-apiserver监听的ip地址,就是 master本机IP地址。
–pod-network-cidr：用于指定Pod的网络范围； 10.244.0.0/16
–service-cidr：用于指定SVC的网络范围；
–image-repository: 指定阿里云镜像仓库地址</code></pre>
<p>如果执行成功会看到</p>
<pre><code>Your Kubernetes control-plane has initialized successfully!

To start using your cluster, you need to run the following as a regular user:

  mkdir -p $HOME/.kube
  sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
  sudo chown $(id -u):$(id -g) $HOME/.kube/config

Alternatively, if you are the root user, you can run:

  export KUBECONFIG=/etc/kubernetes/admin.conf

You should now deploy a pod network to the cluster.
Run "kubectl apply -f [podnetwork].yaml" with one of the options listed at:
  https://kubernetes.io/docs/concepts/cluster-administration/addons/

Then you can join any number of worker nodes by running the following on each as root:

kubeadm join 192.168.10.90:6443 --token 9vh4kc.5lcth0nqbhx7mugj \
    --discovery-token-ca-cert-hash sha256:eb606e1c5f634d7a861fe09644dfdb12f9bfeb02534012445cc9712e4e8caede 

#依次执行上面的3条命令
mkdir -p $HOME/.kube
sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
sudo chown $(id -u):$(id -g) $HOME/.kube/config

[root@master vagrant]# kubectl get nodes
NAME     STATUS     ROLES                  AGE     VERSION
master   NotReady   control-plane,master   6m54s   v1.22.0</code></pre>
<h1>node1节点</h1>
<pre><code># vi /etc/sysconfig/kubelet
KUBELET_EXTRA_ARGS="--node-ip=192.168.10.91 --pod-infra-container-image=registry.cn-hangzhou.aliyuncs.com/google_containers/pause:3.5"

service kubelet restart

#将当前的节点加入到kubelet集群当中去，如果忘记可以通过命令 kubeadm token create --print-join-command 获取
[root@node vagrant]# kubeadm join 192.168.10.90:6443 --token 9vh4kc.5lcth0nqbhx7mugj \
&gt;     --discovery-token-ca-cert-hash sha256:eb606e1c5f634d7a861fe09644dfdb12f9bfeb02534012445cc9712e4e8caede
[preflight] Running pre-flight checks
    [WARNING FileExisting-tc]: tc not found in system path
[preflight] Reading configuration from the cluster...
[preflight] FYI: You can look at this config file with 'kubectl -n kube-system get cm kubeadm-config -o yaml'
[kubelet-start] Writing kubelet configuration to file "/var/lib/kubelet/config.yaml"
[kubelet-start] Writing kubelet environment file with flags to file "/var/lib/kubelet/kubeadm-flags.env"
[kubelet-start] Starting the kubelet
[kubelet-start] Waiting for the kubelet to perform the TLS Bootstrap...

This node has joined the cluster:
* Certificate signing request was sent to apiserver and a response was received.
* The Kubelet was informed of the new secure connection details.

Run 'kubectl get nodes' on the control-plane to see this node join the cluster.

#回到master上看一下, 可以看见INTERNAL-IP都是我们希望的IP了（状态还是NoReady，因为网络组件还没安装)
[root@master vagrant]# kubectl get nodes -o wide
NAME     STATUS     ROLES                  AGE   VERSION   INTERNAL-IP     EXTERNAL-IP   OS-IMAGE         KERNEL-VERSION                 CONTAINER-RUNTIME
master   NotReady   control-plane,master   13m   v1.22.0   192.168.10.90   &lt;none&gt;        CentOS Linux 8   4.18.0-240.15.1.el8_3.x86_64   docker://20.10.8
node1    NotReady   &lt;none&gt;                 8s    v1.22.0   192.168.10.91   &lt;none&gt;        CentOS Linux 8   4.18.0-240.15.1.el8_3.x86_64   docker://20.10.8
node2    NotReady   &lt;none&gt;                 16s   v1.22.0   192.168.10.92   &lt;none&gt;        CentOS Linux 8   4.18.0-240.15.1.el8_3.x86_64   docker://20.10.8</code></pre>
<h1>node2节点</h1>
<p>除了ip,和node1节点的操作一致</p>
<h1>flannel网络组件</h1>
<p>在master上执行</p>
<pre><code>kubectl apply -f https://raw.githubusercontent.com/coreos/flannel/master/Documentation/kube-flannel.yml
#可能上面的命令因网络问题而执行失败，那就把整个库下载下来再执行
https://github.com/flannel-io/flannel
#修改kube-flannel.yml，指定eth1
...
      containers:
      - name: kube-flannel
        image: registry.cn-hangzhou.aliyuncs.com/google-containers/flannel:v0.9.0
        command:
        - /opt/bin/flanneld
        args:
        - --ip-masq
        - --kube-subnet-mgr
        - --iface=eth1
...
#执行
kubectl apply -f ./Documentation/kube-flannel.yml

#查询所以pod，不出所料，STATUS都是Running
[root@master vagrant]# kubectl get pods -n kube-system -o wide
NAME                             READY   STATUS    RESTARTS        AGE     IP              NODE     NOMINATED NODE   READINESS GATES
coredns-7d89d9b6b8-fcs2v         1/1     Running   0               42m     10.244.0.3      master   &lt;none&gt;           &lt;none&gt;
coredns-7d89d9b6b8-kb8f8         1/1     Running   0               42m     10.244.0.2      master   &lt;none&gt;           &lt;none&gt;
etcd-master                      1/1     Running   1 (2m51s ago)   51m     192.168.10.90   master   &lt;none&gt;           &lt;none&gt;
kube-apiserver-master            1/1     Running   2 (2m30s ago)   51m     192.168.10.90   master   &lt;none&gt;           &lt;none&gt;
kube-controller-manager-master   1/1     Running   9 (45s ago)     51m     192.168.10.90   master   &lt;none&gt;           &lt;none&gt;
kube-flannel-ds-4g9k2            1/1     Running   0               4m28s   192.168.10.92   node2    &lt;none&gt;           &lt;none&gt;
kube-flannel-ds-bzw9q            1/1     Running   0               5m39s   192.168.10.90   master   &lt;none&gt;           &lt;none&gt;
kube-flannel-ds-c5x4l            1/1     Running   0               5m42s   192.168.10.91   node1    &lt;none&gt;           &lt;none&gt;
kube-proxy-bfl8p                 1/1     Running   1 (2m8s ago)    38m     192.168.10.92   node2    &lt;none&gt;           &lt;none&gt;
kube-proxy-pf8r9                 1/1     Running   1 (2m10s ago)   38m     192.168.10.91   node1    &lt;none&gt;           &lt;none&gt;
kube-proxy-vj85t                 1/1     Running   1 (2m51s ago)   42m     192.168.10.90   master   &lt;none&gt;           &lt;none&gt;
kube-scheduler-master            0/1     Running   4 (46s ago)     49m     192.168.10.90   master   &lt;none&gt;           &lt;none&gt;

#查询所有node，不出所料，STATUS都是Ready
[root@master vagrant]# kubectl get nodes -o wide
NAME     STATUS   ROLES                  AGE   VERSION   INTERNAL-IP     EXTERNAL-IP   OS-IMAGE         KERNEL-VERSION                 CONTAINER-RUNTIME
master   Ready    control-plane,master   51m   v1.22.0   192.168.10.90   &lt;none&gt;        CentOS Linux 8   4.18.0-240.15.1.el8_3.x86_64   docker://20.10.8
node1    Ready    &lt;none&gt;                 38m   v1.22.0   192.168.10.91   &lt;none&gt;        CentOS Linux 8   4.18.0-240.15.1.el8_3.x86_64   docker://20.10.8
node2    Ready    &lt;none&gt;                 38m   v1.22.0   192.168.10.92   &lt;none&gt;        CentOS Linux 8   4.18.0-240.15.1.el8_3.x86_64   docker://20.10.8</code></pre>
<h1>dashboard</h1>
<p><a href="https://www.cuiwei.net/p/1987004779">k8s 安装 dashboard</a></p>
<h1>ingress-nginx</h1>
<p><a href="http://www.cuiwei.net/p/1857935436">k8s ingress 两种部署方式nodePort和hostNetwork</a></p>
<h1>文件下载</h1>
<p><a href="https://github.com/chudaozhe/k8s-demo">https://github.com/chudaozhe/k8s-demo</a></p>
<h1>参考</h1>
<p><a href="https://developer.aliyun.com/mirror/kubernetes">https://developer.aliyun.com/mirror/kubernetes</a></p>
<p>1️⃣ 关于registry.aliyuncs.com/google_containers，详见 <a href="https://cr.console.aliyun.com/images/cn-hangzhou/google_containers/kube-apiserver/detail">阿里云镜像仓库市场</a></p>
<h1>问题</h1>
<pre><code># 问题1
[WARNING ImagePull]: failed to pull image registry.cn-hangzhou.aliyuncs.com/google_containers/coredns:v1.8.4: output: Error response from daemon: manifest for registry.cn-hangzhou.aliyuncs.com/google_containers/coredns:v1.8.4 not found: manifest unknown: manifest unknown

# 解决办法(所以节点都需要执行)
#拉取
docker pull registry.cn-hangzhou.aliyuncs.com/google_containers/coredns:1.8.4
# 重命名
docker tag registry.cn-hangzhou.aliyuncs.com/google_containers/coredns:1.8.4 registry.cn-hangzhou.aliyuncs.com/google_containers/coredns:v1.8.4
# 删除原有镜像
docker rmi registry.cn-hangzhou.aliyuncs.com/google_containers/coredns:1.8.4</code></pre>
<p>2️⃣问题2</p>
<pre><code>如果kubeadm init执行失败，先确认kubelet是否正常允许
service kubelet status
如果kubelet未能正常运行，执行下面这条命令排查
journalctl -xefu kubelet
假如看到了下面这样的错误

 "Failed to run kubelet" err="failed to run Kubelet: misconfiguration: kubelet cgroup driver: \"systemd\" is different from docker cgroup driver: \"cgroupfs\""

大致意思是docker和kubelet的cgroup driver不一致

#查看docker的cgroup driver
docker info
#查看kubelet的cgroup driver
cat /var/lib/kubelet/config.yaml

#设置docker的cgroup driver为systemd
cat &gt; /etc/docker/daemon.json &lt;&lt;EOF
{
  "exec-opts": ["native.cgroupdriver=systemd"]
}
EOF
#重启docker
systemctl restart docker

#多次执行kubeadm init，可能会提示端口占用，需重置一下
kubeadm reset
</code></pre></div>]]></description>
            <guid isPermaLink="false">基于vagrant搭建k8s集群</guid>
        </item>
        <item>
            <title><![CDATA[docker私有仓库Harbor搭建]]></title>
            <description><![CDATA[<link rel="stylesheet" href="https://www.cuiwei.net/static/css/github-markdown.min.css?t=20260430" type="text/css" media="screen" /><div class="markdown-body"><blockquote>
<p>虽然Docker官方提供了公共的镜像仓库，但是从安全和效率等方面考虑，部署我们私有环境内的Registry也是非常必要的。
Harbor是由VMware公司开源的企业级的Docker Registry管理项目，相比docker官方拥有更丰富的权限权利和完善的架构设计，适用大规模docker集群部署提供仓库服务。
它主要提供 Dcoker Registry 管理界面UI，可基于角色访问控制,镜像复制， AD/LDAP 集成，日志审核等功能，完全的支持中文。</p>
</blockquote>
<h1>准备</h1>
<ul>
<li>安装docker与docker-compose</li>
<li>下载离线安装包<a href="https://github.com/goharbor/harbor/releases">harbor-offline-installer-v2.3.1.tgz</a></li>
<li>域名（harbor.cw.net）及证书（harbor.cw.net.pem，harbor.cw.net.key），阿里云，腾讯云都可免费申请</li>
</ul>
<h1>安装</h1>
<h2>情况1</h2>
<p>一台服务器只安装harbor（无需单独配置nginx）</p>
<p>修改harbor.yml</p>
<pre><code>hostname: harbor.cw.net

http:
  port: 80 

https:
  port: 443
  certificate: /data/server/nginx/conf/ssl/harbor.cw.net.pem
  private_key: /data/server/nginx/conf/ssl/harbor.cw.net.key

harbor_admin_password: 123456 #管理员密码

database:
  password: 123456

data_volume: /data/harbor #挂载本地目录，会生成6个目录（ca_download  database  job_logs  redis  registry  secret）</code></pre>
<h2>情况2</h2>
<p>服务器中已经存在web服务，80/443端口已被占用</p>
<p>修改harbor.yml</p>
<pre><code>hostname: harbor.cw.net

http:
  port: 9480 #没用，但没有会报错

https:
  port: 9443
  certificate: /data/server/nginx/conf/ssl/harbor.cw.net.pem #没用，但没有会报错
  private_key: /data/server/nginx/conf/ssl/harbor.cw.net.key #没用，但没有会报错

harbor_admin_password: 123456 #管理员密码

database:
  password: 123456

data_volume: /data/harbor #挂载本地目录，会生成6个目录（ca_download  database  job_logs  redis  registry  secret）</code></pre>
<p>nginx配置</p>
<pre><code>server {
    listen       443 ssl;
    listen       80;
    server_name  harbor.cw.net;

    ssl_certificate   ssl/harbor.cw.net.pem;
    ssl_certificate_key  ssl/harbor.cw.net.key;
    include conf.d/ssl.conf;

    location / {
        proxy_pass   https://harbor.cw.net:9443;
        include conf.d/proxy.md;
    }

}
</code></pre>
<h2>执行</h2>
<pre><code>./install.sh #可以重复执行，执行成功会在根目录生成docker-compose.yml文件，同时可以使用域名https://harbor.cw.net访问</code></pre>
<h1>日常操作</h1>
<pre><code>docker-compose down #停止
docker-compose up -d #开启

#如果修改了harbor.yml
1、docker-compose down #停止
2、./prepare #重新生成docker-compose.yml文件
3、docker-compose up -d #开启
</code></pre>
<h1>客户端</h1>
<h2>登陆</h2>
<pre><code>docker login harbor.cw.net</code></pre>
<h2>上传</h2>
<pre><code>#标记（69593048aa3a为本地镜像id）
docker tag 69593048aa3a harbor.cw.net/library/busybox
#push
docker push harbor.cw.net/library/busybox</code></pre>
<h2>拉取</h2>
<pre><code>docker pull harbor.cw.net/library/busybox</code></pre></div>]]></description>
            <guid isPermaLink="false">docker私有仓库Harbor搭建</guid>
        </item>
        <item>
            <title><![CDATA[docker私有仓库搭建]]></title>
            <description><![CDATA[<link rel="stylesheet" href="https://www.cuiwei.net/static/css/github-markdown.min.css?t=20260430" type="text/css" media="screen" /><div class="markdown-body"><p>这里宿主机ip为：192.168.10.100</p>
<p>修改宿主机hosts</p>
<pre><code>echo '192.168.10.100 registry.cw.net' &gt;&gt; /etc/hosts</code></pre>
<h1>配置</h1>
<h2>docker-compose.yml</h2>
<pre><code># tell docker what version of the docker-compose.yml we're using
version: '3.1'
services:
  registry:
    image: registry
    restart: always
    container_name: registry
    ports:
      - 5000:5000
    volumes:
      - ./registry:/var/lib/registry</code></pre>
<h2>修改docker配置文件</h2>
<pre><code>vi /etc/docker/daemon.json
{
  "insecure-registries": [
    "registry.cw.net:5000"
  ]
}</code></pre>
<h2>启动该容器</h2>
<pre><code>docker-compose up -d</code></pre>
<h1>测试</h1>
<pre><code>准备一个本地镜像
docker pull busybox:latest ⬅️拉取镜像
打标记
docker tag busybox:latest registry.cw.net:5000/busybox
将本地镜像push到本地仓库
docker push registry.cw.net:5000/busybox
从仓库拉取busybox
docker pull registry.cw.net:5000/busybox
删除
docker rmi registry.cw.net:5000/busybox</code></pre>
<h2>通过浏览器查看本地镜像列表</h2>
<pre><code>http://localhost:5000/v2/ ⬅️空
http://localhost:5000/v2/_catalog ⬅️会看到本地镜像列表</code></pre>
<h1>给虚拟机里的docker使用宿主机的仓库</h1>
<p>1、修改宿主机hosts</p>
<pre><code>echo '192.168.10.100 registry.cw.net' &gt;&gt; /etc/hosts</code></pre>
<p>2、修改docker配置文件</p>
<pre><code>vi /etc/docker/daemon.json
{
  "insecure-registries": [
    "registry.cw.net:5000"
  ]
}</code></pre>
<p>3、重启docker</p>
<pre><code>systemctl restart docker</code></pre>
<h1>其他</h1>
<p>正式环境建议用<code>harbor</code></p></div>]]></description>
            <guid isPermaLink="false">docker私有仓库搭建</guid>
        </item>
        <item>
            <title><![CDATA[k8s包管理器 - Helm]]></title>
            <description><![CDATA[<link rel="stylesheet" href="https://www.cuiwei.net/static/css/github-markdown.min.css?t=20260430" type="text/css" media="screen" /><div class="markdown-body"><blockquote>
<p>Helm是Kubernetes的包管理器，类似于Python的pip centos的yum,主要用来管理 Charts
Helm Chart是用来封装Kubernetes原生应用程序的一系列YAML文件。可以在你部署应用的时候自定义应用程序的一些Metadata，
以便于应用程序的分发。对于应用发布者而言，可以通过Helm打包应用、管理应用依赖关系、管理应用版本并发布应用到软件仓库。
对于使用者而言，使用Helm后不用需要编写复杂的应用部署文件，可以以简单的方式在Kubernetes上查找、安装、升级、回滚、卸载应用程序</p>
</blockquote>
<p>下面以 kubernetes-dashboard 为例</p>
<h1>安装</h1>
<pre><code>helm repo add kubernetes-dashboard https://kubernetes.github.io/dashboard/
helm install kubernetes-dashboard kubernetes-dashboard/kubernetes-dashboard
#执行成功会看到以下提示（按照提示执行相关命令）
NOTES:
*********************************************************************************
*** PLEASE BE PATIENT: kubernetes-dashboard may take a few minutes to install ***
*********************************************************************************

Get the Kubernetes Dashboard URL by running:
  export POD_NAME=$(kubectl get pods -n default -l "app.kubernetes.io/name=kubernetes-dashboard,app.kubernetes.io/instance=my-kubernetes-dashboard" -o jsonpath="{.items[0].metadata.name}")
  echo https://127.0.0.1:8443/
  kubectl -n default port-forward $POD_NAME 8443:8443</code></pre>
<h1>删除</h1>
<pre><code>helm delete kubernetes-dashboard</code></pre></div>]]></description>
            <guid isPermaLink="false">k8s包管理器 - Helm</guid>
        </item>
        <item>
            <title><![CDATA[docker搭建Redis集群-主从复制]]></title>
            <description><![CDATA[<link rel="stylesheet" href="https://www.cuiwei.net/static/css/github-markdown.min.css?t=20260430" type="text/css" media="screen" /><div class="markdown-body"><p>一主二从</p>
<p><img src="https://www.cuiwei.net/data/upload/2021-08-04/162808732350160.jpg" alt="WechatIMG1681.jpeg" /></p>
<h1>代码目录</h1>
<pre><code>│ docker-compose.yml
├─apache
│   Dockerfile
│   index.php
├─follower
│   Dockerfile
│   run.sh
└─leader
    Dockerfile</code></pre>
<p>docker-compose.yml</p>
<pre><code># docker-compose.yml
# tell docker what version of the docker-compose.yml we're using
version: '3'

# define the network
networks:
  web-network:

# start the services section
services:
  # define the name of our service
  # corresponds to the "--name" parameter
  apache:
    build:
      context: ./apache
    # defines the port mapping
    # corresponds to the "-p" flag
    ports:
      - 80:80
    tty: true
    volumes:
      - ./apache:/var/www/html
    networks:
      - web-network

  redis-leader:
    build:
      context: ./leader
    tty: true
    networks:
      - web-network

  redis-follower:
    build:
      context: ./follower
    tty: true
    networks:
      - web-network
    deploy:
      replicas: 2
</code></pre>
<p>apache/index.php</p>
<pre><code># apache/index.php
&lt;?php
# 访问链接http://localhost/，测试结果
$redis=new Redis;
$redis-&gt;connect('redis-leader', '6379');
$redis-&gt;set('aa', 'aa123');

$redis-&gt;connect('redis-follower', '6379');
echo $redis-&gt;get('aa');</code></pre>
<p>apache/Dockerfile</p>
<pre><code>FROM php:7.4-apache
RUN pecl install redis-5.3.4 \
    &amp;&amp; docker-php-ext-enable redis

# 将apache目录下的文件复制到容器内/var/www/html
# COPY . .
</code></pre>
<p>follower/Dockerfile</p>
<pre><code># follower/Dockerfile
FROM redis:6.2.5

ADD run.sh /run.sh
RUN chmod a+x /run.sh
CMD /run.sh</code></pre>
<p>follower/run.sh</p>
<pre><code># follower/run.sh
#!/bin/bash
redis-server --replicaof redis-leader 6379</code></pre>
<p>leader/Dockerfile</p>
<pre><code># leader/Dockerfile
FROM redis:6.2.5
</code></pre></div>]]></description>
            <guid isPermaLink="false">docker搭建Redis集群-主从复制</guid>
        </item>
        <item>
            <title><![CDATA[k8s——一个简单示例]]></title>
            <description><![CDATA[<link rel="stylesheet" href="https://www.cuiwei.net/static/css/github-markdown.min.css?t=20260430" type="text/css" media="screen" /><div class="markdown-body"><pre><code>apiVersion #API对象版本，可通过`kubectl api-versions`命令查看

kind #资源类型，区分大小写，可通过`kubectl api-resources`命令查看，这里使用Deployment对象

metadata #是该资源的元数据,name是必需的元数据项

spec# 部分是该Deployment的规格说明

    replicas#指明副本数量,默认为1

    template#定义Pod的模板,这是配置文件的重要部分

        metadata#定义Pod的元数据,至少要定义一个label。label的key和value可以任意指定

        spec # 描述Pod的规格,此部分定义Pod中每一个容器的属性,name和image是必需的

status</code></pre>
<h1>准备镜像</h1>
<p>这里直接使用nginx官方镜像</p>
<h1>创建Deployment</h1>
<p>方式一</p>
<pre><code>kubectl create deployment nginx-deployment --image=nginx --replicas=2 --port=80</code></pre>
<p>方式二</p>
<p><code>kubectl apply -f nginx-deployment.yaml</code></p>
<pre><code># nginx-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-deployment
spec:
  selector:
    matchLabels:
      app: nginx
  replicas: 2 # tells deployment to run 2 pods matching the template
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
        - name: nginx
          image: nginx:latest
          ports:
            - containerPort: 80</code></pre>
<h1>创建Service</h1>
<p>方式一</p>
<pre><code>kubectl expose deployment nginx-deployment --type=NodePort --port=80</code></pre>
<p>方式二</p>
<p><code>kubectl create -f nginx-service.yaml</code></p>
<pre><code># nginx-service.yaml
apiVersion: v1
kind: Service
metadata:
  name: nginx-service
  labels:
    name: nginx-service
spec:
  type: NodePort
  ports:
    - port: 80
  selector:
    app: nginx # 名称与deployment中定义的app名称保持一致</code></pre>
<h1>测试</h1>
<p>我们这里设置了replicas=2，会产生两个容器（名称为 k8s_nginx_nginx-deployment-开头），
分别进入两个容器，增加一个文件</p>
<p><code>echo 'aaa' &gt;/usr/share/nginx/html/a.html</code></p>
<p>查询nodePort</p>
<p><code>kubectl get service</code></p>
<pre><code>NAME               TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)        AGE
kubernetes         ClusterIP   10.96.0.1       &lt;none&gt;        443/TCP        157m
nginx-deployment   NodePort    10.106.195.35   &lt;none&gt;        80:32352/TCP   6m51s</code></pre>
<p>如上nodePort为32352</p>
<p>测试</p>
<p><code>http://localhost:32352/a.html</code></p>
<h1>清理</h1>
<pre><code>kubectl delete -n default deployment nginx-deployment
kubectl delete -n default service nginx-service
kubectl delete -n default ingress test-ingress</code></pre>
<h1>进阶 —— ingress-nginx</h1>
<p><a href="http://www.cuiwei.net/p/1857935436">k8s ingress 两种部署方式nodePort和hostNetwork</a></p>
<h1>API 参考</h1>
<p><a href="https://kubernetes.io/zh/docs/reference/">https://kubernetes.io/zh/docs/reference/</a></p>
<p><a href="https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.21/#deployment-v1-apps">Kubernetes API 参考 v1.21</a></p>
<p>历史版本</p>
<p><a href="https://v1-20.docs.kubernetes.io/docs/reference/generated/kubernetes-api/v1.20/#deployment-v1-apps">https://v1-20.docs.kubernetes.io/docs/reference/generated/kubernetes-api/v1.20/#deployment-v1-apps</a></p></div>]]></description>
            <guid isPermaLink="false">k8s——一个简单示例</guid>
        </item>
        <item>
            <title><![CDATA[docker基本操作]]></title>
            <description><![CDATA[<link rel="stylesheet" href="https://www.cuiwei.net/static/css/github-markdown.min.css?t=20260430" type="text/css" media="screen" /><div class="markdown-body"><h2>镜像</h2>
<pre><code>docker search nginx ⬅️来查看可用镜像
docker pull redis:latest ⬅️下载镜像
docker images ⬅️查看本地镜像
docker rmi {IMAGE_ID} ⬅️删除一个或多个镜像
docker rmi `docker images -q` ⬅️删除全部镜像
docker build -f ./Dockerfile -t php:v1-swoole . 

docker rm -f $(docker ps -aq) &amp;&amp; docker rmi -f $(docker images -q) ⬅️删除全部镜像和容器</code></pre>
<h2>容器</h2>
<pre><code>docker ps -a ⬅️查看全部容器
docker start {CONTAINER_ID} ⬅️启动容器
docker stop {CONTAINER_ID} ⬅️停止一个容器
docker restart {CONTAINER_ID} ⬅️重启一个容器
docker stop $(docker ps -q) ⬅️停止全部容器
docker rm {CONTAINER_ID} ⬅️删除一个或多个容器
docker rm $(docker ps -aq) ⬅️删除全部容器

docker stop $(docker ps -q) &amp; docker rm $(docker ps -aq)  ⬅️停用并删除全部容器

docker attach {CONTAINER_ID} ⬅️登录容器
docker exec -it {CONTAINER_ID} /bin/bash ⬅️登录容器

//将主机/www/runoob目录拷贝到容器96f7f14e99ab的/www目录下。
docker cp /www/runoob 96f7f14e99ab:/www/

//将容器96f7f14e99ab的/www目录拷贝到主机的/tmp目录中。
docker cp  96f7f14e99ab:/www /tmp/
</code></pre>
<h3>本机使用容器内的命令</h3>
<p>注意：一定要设置工作目录，否则就会在容器的默认工作目录执行</p>
<pre><code>vi ~/.bashrc
alias phpunit='docker exec -it -w /var/www/project1 aaa vendor/bin/phpunit'
alias phpcomposer='docker exec -it -w /var/www/project1 aaa composer'</code></pre>
<p>我有很多项目，为每个项目都设置一个命令也不现实，所以就有了下面的脚本，同样是写在<code>~/.bashrc</code></p>
<pre><code># 取消之前的 composer 别名定义
unalias composer 2&gt;/dev/null

# 定义项目根目录变量
PROJECT_ROOT="/Users/cuiwei/PhpstormProjects"

# 定义容器名称变量
CONTAINER_NAME="server-docker-php-fpm-1"

# 定义一个通用的 composer 函数
composer() {
    local host_dir=$(pwd)
    if [[ "$host_dir" != $PROJECT_ROOT/* ]]; then
       echo "Error: You must run this command from a subdirectory of $PROJECT_ROOT, e.g., $PROJECT_ROOT/test1/."
       return 1
    fi

    local container_dir=$(echo "$host_dir" | sed "s|^$PROJECT_ROOT|/var/www|")
    echo "host_dir: $host_dir"
    echo "container_dir: $container_dir"
    docker exec -it -w "$container_dir" $CONTAINER_NAME composer "$@"
}</code></pre>
<h2>docker network</h2>
<pre><code>docker network ls ⬅️显示所有 bridge
docker network create --driver bridge web-network ⬅️创建一个叫做 web-network 的网桥，使用的连接方式是 bridge
docker inspect web-network ⬅️查看 web-network 网络里面的容器
docker network connect web-network {CONTAINER} ⬅️手动将某个容器加入网桥</code></pre>
<h2>docker run</h2>
<pre><code>基于一个镜像启动一个容器，如果此镜像不存在则自动下载

-a stdin: 指定标准输入输出内容类型，可选 STDIN/STDOUT/STDERR 三项；
-d: 后台运行容器，并返回容器ID；
-i: 以交互模式运行容器，通常与 -t 同时使用；
-P: 随机端口映射，容器内部端口随机映射到主机的端口
-p: 指定端口映射，格式为：主机(宿主)端口:容器端口
-t: 为容器重新分配一个伪输入终端，通常与 -i 同时使用；
--name="nginx-lb": 为容器指定一个名称；
--dns 8.8.8.8: 指定容器使用的DNS服务器，默认和宿主一致；
--dns-search example.com: 指定容器DNS搜索域名，默认和宿主一致；
-h "mars": 指定容器的hostname；
-e username="ritchie": 设置环境变量；
--env-file=[]: 从指定文件读入环境变量；
--cpuset="0-2" or --cpuset="0,1,2": 绑定容器到指定CPU运行；
-m :设置容器使用内存最大值；
--net="bridge": 指定容器的网络连接类型，支持 bridge/host/none/container: 四种类型；
--link=[]: 添加链接到另一个容器；
--expose=[]: 开放一个端口或一组端口；
--volume , -v: 绑定一个卷

docker run --name php-fpm -v C:\Users\work\nginx/www:/www -d php:fpm /bin/sh -c "while true; do echo hello world; sleep 1; done"
docker run --name php-fpm7.4 -v C:\Users\work\nginx/www:/www -d php:7.4-fpm /bin/sh -c "while true; do echo hello world; sleep 1; done"

docker run -v C:\Users\work\nginx/www:/usr/share/nginx/html:ro -v C:\Users\work\nginx/conf:/etc/nginx:ro --link php-fpm7.4 --name nginx -p 8080:80 -d nginx
docker run --name mysqld -e MYSQL_ROOT_PASSWORD=123456 -d mysql

docker run -di --name docker-php -v "C:\codebase\docker-php\app":/var/www --network web-network docker-php-image
docker run -di --name docker-nginx -p 8080:80 -v "C:\codebase\docker-php\nginx\conf.d":/etc/nginx/conf.d/ -v "C:\codebase\docker-php\app":/var/www  --network web-network docker-nginx-image
docker run -di --name docker-php-fpm -v "C:\codebase\docker-php\app":/var/www --network web-network docker-php-fpm-image
</code></pre>
<h2>docker-compose</h2>
<p>Compose 是用于定义和运行多容器 Docker 应用程序的工具</p>
<pre><code>docker-compose up -d ⬅️后台运行
docker-compose down ⬅️停止并删除`docker-compose.yml`中的所以容器，及network
docker-compose -f ./docker-compose.yml restart docker-php-fpm ⬅️重启某个服务（比如：docker-php-fpm）
docker-compose -f ./docker-compose.yml up -d docker-php-fpm ⬅️删除某个容器及镜像后需要重新构建时执行（比如：docker-php-fpm）
</code></pre>
<h2>其他</h2>
<p><a href="https://www.cuiwei.net/p/1648842654">docker 从容器创建新镜像，及镜像的备份和恢复</a></p></div>]]></description>
            <guid isPermaLink="false">docker基本操作</guid>
        </item>
        <item>
            <title><![CDATA[Windows下的包管理器Chocolatey]]></title>
            <description><![CDATA[<link rel="stylesheet" href="https://www.cuiwei.net/static/css/github-markdown.min.css?t=20260430" type="text/css" media="screen" /><div class="markdown-body"><p>Chocolatey是一个Windows下的包管理器，类似于Linux下的apt-get或yum。</p>
<h1>安装</h1>
<p>以管理员身份打开PowerShell，执行以下两条命令</p>
<pre><code>PS C:\Users\work&gt; Set-ExecutionPolicy unrestricted
PS C:\Users\work&gt; Set-ExecutionPolicy Bypass -Scope Process -Force; [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072; iex ((New-Object System.Net.WebClient).DownloadString('https://chocolatey.org/install.ps1'))
</code></pre>
<h1>使用</h1>
<p>Chocolatey 官方对应有个<a href="https://community.chocolatey.org/packages">package页面</a>，可以在这了查找是否有需要的包，也可以直接使用命令来查找具体命令和功能,常用的命令如下</p>
<pre><code>choco search &lt;keyword&gt;    模糊搜索
choco list &lt;keyword&gt;      和search 功能类似
choco search &lt;keyword&gt; -all     查询所有可用的包
choco upgrade &lt;package&gt;    包更新
choco list -localonly    查看本地安装的包
choco install &lt;package&gt; 安装包
choco install &lt;package1 package2 package3...&gt;     一次安装多个包
choco uninstall &lt;package&gt;     卸载包
choco install &lt;package&gt; -version &lt;v&gt;     安装所需版本的包
choco version &lt;package&gt;        查看指定包是否有新版本

choco install microsoft-edge
choco install winrar
choco install 7zip
choco install adobereader
choco install git
choco install vlc
choco install googlechrome
choco install firefox</code></pre>
<h1>其他Windows下的包管理器</h1>
<pre><code>WinGet，scoop</code></pre>
<h1>参考</h1>
<p><a href="https://chocolatey.org/install#individual">https://chocolatey.org/install#individual</a></p></div>]]></description>
            <guid isPermaLink="false">Windows下的包管理器Chocolatey</guid>
        </item>
        <item>
            <title><![CDATA[微信小程序使用自定义字体 - iconfont]]></title>
            <description><![CDATA[<link rel="stylesheet" href="https://www.cuiwei.net/static/css/github-markdown.min.css?t=20260430" type="text/css" media="screen" /><div class="markdown-body"><h1>步骤</h1>
<p>1、在<a href="https://www.iconfont.cn">iconfont</a>网站获取iconfont.ttf字体文件</p>
<p>2、<a href="https://transfonter.org">字体文件转化成base64格式</a>
<img src="https://www.cuiwei.net/data/upload/2021-07-07/162564514824283.jpg" alt="WX202107071603322x.png" /></p>
<p>下载后解压，得到<code>stylesheet.css</code>，将此文件里的代码复制到<code>app.wxss</code></p>
<p>3、再次回到iconfont
<img src="https://www.cuiwei.net/data/upload/2021-07-07/162564570981077.jpg" alt="WX202107071614112x.png" /></p>
<p>点击图片中的css文件链接，将里面的除了@font-face部分，其他都复制到<code>app.wxss</code>，最终<code>app.wxss</code>文件内容如下</p>
<pre><code>@font-face {
  font-family: 'iconfont';
  src: url('data:font/woff2;charset=utf-8;base64,d09GMgABA...') format('woff2'),
  url('data:font/woff;charset=utf-8;base64,d09GRgABAAAAA...') format('woff');
  font-weight: 500;
  font-style: normal;
  font-display: swap;
}

.iconfont {
  font-family: "iconfont" !important;
  font-size: 24px;
  font-style: normal;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}
.icon-shou:before {
  content: "\e69b";
  color: #ff6400;
  margin-right: 10px;
}

.icon-ji:before {
  content: "\e600";
  color: #015CD7;
  margin-right: 10px;
}</code></pre>
<h1>使用</h1>
<pre><code>&lt;text class="iconfont icon-ji"&gt;&lt;/text&gt;</code></pre>
<h1>配合vant-weapp一起使用</h1>
<pre><code>&lt;van-icon class-prefix="iconfont icon-ji" name="extra" /&gt;</code></pre></div>]]></description>
            <guid isPermaLink="false">微信小程序使用自定义字体 - iconfont</guid>
        </item>
        <item>
            <title><![CDATA[ffmpeg的使用]]></title>
            <description><![CDATA[<link rel="stylesheet" href="https://www.cuiwei.net/static/css/github-markdown.min.css?t=20260430" type="text/css" media="screen" /><div class="markdown-body"><p>应用场景1</p>
<pre><code>ffmpeg -i http://xxx.com/index.m3u8 -c copy -bsf:a aac_adtstoasc output.mp4</code></pre>
<p>通过浏览器看视频的时候，想下载下来。打开开发者工具，network，如果看到浏览器在不断的加载<code>.ts</code>结尾的文件，这时候筛选下<code>m3u8</code>,如果找到了，就可以用这种方法下载</p>
<p>应用场景2</p>
<pre><code>#mov转mp4
ffmpeg -i 123.mov -vcodec libx264 -preset fast -crf 20 -y -vf "scale=1280:-1" -acodec libmp3lame -ab 128k new.mp4</code></pre>
<pre><code>#webm转mp4
ffmpeg -i input.webm -crf 17 -c:v libx264 output.mp4</code></pre>
<pre><code>#压缩mp4，适合画面不大变化的
ffmpeg -i input.mp4 -r 10 -b:a 32k output.mp4</code></pre>
<pre><code>#从mp4视频中提取出音频
ffmpeg -i input.mp4 -acodec pcm_s16le -f s16le -ac 1 -ar 16000 -f wav output.wav</code></pre>
<pre><code>#视频缩略图（视频所有帧图片）
ffmpeg -i input.mp4 -f image2 %05d.jpg</code></pre>
<pre><code>#输出指定时间的图片
ffmpeg -i input.flv -ss 00:00:02 -frames:v 1 out.png</code></pre>
<pre><code>#mp4转m3u8
ffmpeg -i ./aaa.mp4 -hls_time 10 -hls_list_size 0 -hls_segment_filename ./aaa_%05d.ts ./aaa.m3u8

#-hls_time 设置每片的长度，单位为秒
#-hls_list_size n: 保存的分片的数量，设置为0表示保存所有分片
#-hls_segment_filename ：段文件的名称，%05d表示5位数字</code></pre>
<h2>使用视频文件直播</h2>
<h3>本地视频文件</h3>
<p>以<code>~/Movies/11月5日.mp4</code>为例</p>
<h3>获取推流地址</h3>
<p>以哔哩哔哩为例：直播中心 -&gt; 我的直播间 -&gt; 开播设置</p>
<ul>
<li>服务器地址：<code>rtmp://live-push.bilivideo.com/live-bvc/</code></li>
<li>串流密钥：<code>?streamname=live_3333&amp;key=777&amp;schedule=rtmp&amp;pflag=1</code></li>
</ul>
<p>推流地址=服务器地址+串流密钥，即：<code>rtmp://live-push.bilivideo.com/live-bvc/?streamname=live_3333&amp;key=777&amp;schedule=rtmp&amp;pflag=1</code></p>
<h3>开始直播</h3>
<pre><code>cuiwei@weideMacBook-Pro ~ % ffmpeg -re -i ~/Movies/11月5日.mp4 -vcodec copy -acodec aac -b:a 96k -f flv "推流地址"</code></pre>
<p>参数说明</p>
<p> &quot;-vcodec copy&quot; 这种<code>-</code> + 字母的 就是一个完整的配置项 '-配置key 配置value'</p>
<ul>
<li><code>-re</code> 就是real-time 直播必须带这参数</li>
<li><code>-i "xxx.mp4"</code> 就是input 媒体输入</li>
<li><code>-stream_loop -1</code> 循环播放</li>
<li><code>-vcodec copy</code> 就是video decode 视频解码 copy就是沿用输入视频的解码方式</li>
<li><code>-acodec aac</code> 就是audio decode 音频解码 aac是音频的解码方式</li>
<li><code>-b:a 96k</code> 就是bit rate 单位是 kb/s</li>
<li><code>-f flv</code> 就是<code>force format flv</code> 强制输出flv格式</li>
</ul>
<h2>参考</h2>
<p><a href="https://zhuanlan.zhihu.com/p/110716546">https://zhuanlan.zhihu.com/p/110716546</a></p>
<p><a href="https://www.bilibili.com/read/cv27181646/">https://www.bilibili.com/read/cv27181646/</a></p></div>]]></description>
            <guid isPermaLink="false">ffmpeg的使用</guid>
        </item>
        <item>
            <title><![CDATA[支付宝支付回调的处理]]></title>
            <description><![CDATA[<link rel="stylesheet" href="https://www.cuiwei.net/static/css/github-markdown.min.css?t=20260430" type="text/css" media="screen" /><div class="markdown-body"><p>支付宝异步通知会有很多参数，正常POST接收就行</p>
<blockquote>
<p>注意：支付的异步通知和退款的异步通知是同一个url</p>
</blockquote>
<pre><code>$data=$_POST;
</code></pre>
<h1>验证签名</h1>
<pre><code>其验签步骤为：
第一步： 在通知返回参数列表中，除去sign、sign_type两个参数外，凡是通知返回回来的参数皆是待验签的参数。
TIPS： 生活号异步通知组成的待验签串里需要保留sign_type参数。
第二步： 将剩下参数进行url_decode, 然后进行字典排序，组成字符串，得到待签名字符串：
第三步： 将签名参数（sign）使用base64解码为字节码串。
第四步： 使用RSA的验签方法，通过签名字符串、签名参数（经过base64解码）及支付宝公钥验证签名。
第五步：在步骤四验证签名正确后，必须再严格按照如下描述校验通知数据的正确性。

$signature=$data['sign'];
unset($data['sign_type'], $data['sign']);
ksort($data);

$verify=$this-&gt;_sign_verify(urldecode(http_build_query($data)), $signature);
if ($verify==1){
    echo 'ok';
}else echo 'failure';

    /**
     * 支付回调（验证签名 RSA2
     * @param $data
     * @param $signature
     * @return bool
     */
    public function _sign_verify($data, $signature): bool {
        $alipay_public_key = '';//支付宝公钥
        $res = "-----BEGIN PUBLIC KEY-----\n" .
            wordwrap($alipay_public_key, 64, "\n", true) .
            "\n-----END PUBLIC KEY-----";
        //调用openssl内置方法验签，返回bool值
        return (openssl_verify($data, base64_decode($signature), $res, OPENSSL_ALGO_SHA256) === 1);
    }</code></pre>
<h1>其他</h1>
<p>异步通知返回的数据是明文的，无需解密</p>
<pre><code>    //验证交易状态$data['trade_status']是否为TRADE_SUCCESS
    //验证$data['app_id']是否正确
    //通过我们的支付单号$data['out_trade_no']来处理后续流程
    //通知应答
    echo 'success';

if ($data['app_id'] == $config['app_id'] &amp;&amp; !empty($data['out_trade_no'])) {
    if (in_array($data['trade_status'], ['TRADE_SUCCESS'])){//支付成功或部分退款
        if (empty($data['out_biz_no'])){
            //支付成功
            echo "success";
        }else{
            //部分退款
            //out_biz_no为退款编号
            echo 'success';
        }
    }
}elseif (in_array($data['trade_status'], ['TRADE_CLOSED']) &amp;&amp; !empty($data['out_biz_no'])){
    //全额退款
    echo 'success';
}
</code></pre>
<h1>参考</h1>
<p>支付宝异步通知说明</p>
<p><a href="https://opensupport.alipay.com/support/helpcenter/193/201602472200">https://opensupport.alipay.com/support/helpcenter/193/201602472200</a></p>
<p>交易退款接口是否会触发异步通知</p>
<p><a href="https://opensupport.alipay.com/support/helpcenter/193/201602484851?ant_source=zsearch">https://opensupport.alipay.com/support/helpcenter/193/201602484851?ant_source=zsearch</a>#</p></div>]]></description>
            <guid isPermaLink="false">支付宝支付回调的处理</guid>
        </item>
        <item>
            <title><![CDATA[对接支付宝支付]]></title>
            <description><![CDATA[<link rel="stylesheet" href="https://www.cuiwei.net/static/css/github-markdown.min.css?t=20260430" type="text/css" media="screen" /><div class="markdown-body"><ul>
<li>注册支付宝企业账号</li>
<li>申请支付产品</li>
<li>成为支付宝开发者</li>
</ul>
<p>下面假如您已经申请了“手机网站支付”</p>
<h1>开发设置</h1>
<p><img src="https://www.cuiwei.net/data/upload/2021-06-22/162437493066101.jpg" alt="WX202106202336282x.png" />
<img src="https://www.cuiwei.net/data/upload/2021-06-22/162437495273885.jpg" alt="WX202106202307012x.png" />
<img src="https://www.cuiwei.net/data/upload/2021-06-22/162437496598464.jpg" alt="WX202106202312442x.png" />
主要设置了<code>接口加签方式</code>和<code>IP白名单</code>,其他用不到</p>
<h3>接口加签方式</h3>
<p>普通公钥与公钥证书区别1️⃣</p>
<h3>应用私钥、公钥和支付宝公钥2️⃣</h3>
<ul>
<li>应用公钥（public key）需提供给支付宝账号管理者上传到支付宝开放平台。</li>
<li>应用私钥（private key）由开发者自己保存，需填写到代码中供签名时使用。</li>
<li>支付宝公钥，应用公钥上传后会得到对应的支付宝公钥，供验签时使用（如支付回调）</li>
</ul>
<h3>应用网关是什么意思</h3>
<p>官方文档这样描述3️⃣</p>
<blockquote>
<p>生活号、口碑、现金红包、单笔转账接口等异步通知发送到对应appid应用的应用网关中...
<img src="https://www.cuiwei.net/data/upload/2021-06-22/162437538279678.jpg" alt="202001101578653995811_8322b1b5f748e970c216c89ea7e384ce.png" /></p>
</blockquote>
<h1>签名</h1>
<pre><code>    /**
     * 签名
     * @param $data
     * @param $private_key
     * @return array
     */
    public function _sign($data, $private_key): array {
        ksort($data);
        //待签名字符串
        $string_to_be_signed = urldecode(http_build_query($data));

        $res = "-----BEGIN RSA PRIVATE KEY-----\n" .
            wordwrap($private_key, 64, "\n", true) .
            "\n-----END RSA PRIVATE KEY-----";

        openssl_sign($string_to_be_signed, $sign, $res, OPENSSL_ALGO_SHA256);

        $signature = base64_encode($sign);
        $data['sign'] = $signature;
        return $data;
    }</code></pre>
<h1>alipay.trade.wap.pay(手机网站支付接口2.0)</h1>
<p>这个是不需要网络请求的服务端接口，服务端完成参数构建和签名后抛个前端即可，没有网络请求！！！
返回给前端的可以是一个拼装好的form表单的html代码(POST)，也可以是一个链接（GET）</p>
<h1>参考</h1>
<p>1️⃣
<a href="https://opendocs.alipay.com/open/291/105971/#普通公钥与公钥证书区别">https://opendocs.alipay.com/open/291/105971/#普通公钥与公钥证书区别</a></p>
<p>2️⃣在线生成应用私钥和公钥
<a href="https://miniu.alipay.com/keytool/create">https://miniu.alipay.com/keytool/create</a></p>
<p>3️⃣
<a href="https://opensupport.alipay.com/support/helpcenter/193/201602472200">https://opensupport.alipay.com/support/helpcenter/193/201602472200</a></p>
<p>开发指引
<a href="https://opensupport.alipay.com/support/codelab/detail/711/713">https://opensupport.alipay.com/support/codelab/detail/711/713</a></p></div>]]></description>
            <guid isPermaLink="false">对接支付宝支付</guid>
        </item>
        <item>
            <title><![CDATA[基于JQuery的富文本编辑器 - Simditor的使用]]></title>
            <description><![CDATA[<link rel="stylesheet" href="https://www.cuiwei.net/static/css/github-markdown.min.css?t=20260430" type="text/css" media="screen" /><div class="markdown-body"><p>Simditor已经好久没更新了，issue也关闭，那为什么还要用呢，因为喜欢！</p>
<p>下面总结一下使用方法</p>
<h1>安装（两种安装方式）</h1>
<p>1.通过bower和npm安装（不能使用最新版本</p>
<pre><code>#注意 版本号只能小于等于2.3.221️⃣
npm install simditor@2.3.22</code></pre>
<p>2.普通方式引入(可以使用最新版本2.3.28</p>
<pre><code>    &lt;link rel="stylesheet" type="text/css" href="static/simditor/simditor.css" /&gt;
    &lt;link rel="stylesheet" type="text/css" href="static/simditor/html/simditor-html.css" /&gt;

    &lt;script type="text/javascript" src="static/simditor/jquery.min.js"&gt;&lt;/script&gt;
    &lt;script type="text/javascript" src="static/simditor/module.js"&gt;&lt;/script&gt;
    &lt;script type="text/javascript" src="static/simditor/hotkeys.js"&gt;&lt;/script&gt;
    &lt;script type="text/javascript" src="static/simditor/uploader.js"&gt;&lt;/script&gt;
    &lt;script type="text/javascript" src="static/simditor/simditor.js"&gt;&lt;/script&gt;

    &lt;script type="text/javascript" src="static/simditor/html/beautify-html.js"&gt;&lt;/script&gt;
    &lt;script type="text/javascript" src="static/simditor/html/simditor-html.js"&gt;&lt;/script&gt;</code></pre>
<h1>使用</h1>
<p>下面以vue为例2️⃣，封装simditor.vue组件</p>
<pre><code>&lt;template&gt;
  &lt;div class="simditor"&gt;
    &lt;textarea :id="id"&gt;&lt;/textarea&gt;
  &lt;/div&gt;
&lt;/template&gt;
&lt;script&gt;
import $ from 'jquery'
import 'simple-module';
import 'simple-hotkeys';
import 'simple-uploader';
import Simditor from 'simditor'
import 'simditor/styles/simditor.css'
export default {
  name: 'simditor',
  data() {
    return {
      editor: ''
    }
  },
  props: {
    id:'',  //这里传入动态id，一个页面能使用多个编辑器
    options: {  //配置参数
      type: Object,
      default() {
        return {}
      }
    }
  },
  mounted() {
    let vm = this
    this.editor = new Simditor(Object.assign({}, {
      textarea: $(`#${vm.id}`)
    }, this.options))
    this.editor.on('valuechanged', (e, src) =&gt; {
      this.valueChange(e, src)
    })
  },
  methods: {
    valueChange(e, val) {
      this.$emit('change', this.editor.getValue())
    }
  }
}
&lt;/script&gt;

&lt;!-- Add "scoped" attribute to limit CSS to this component only --&gt;
&lt;style scoped&gt;
&lt;/style&gt;
</code></pre>
<p>使用</p>
<pre><code>&lt;template&gt;
  &lt;div id="app"&gt;
    &lt;simditor
        id="test1"
        :options="options"
        @change="change"&gt;
    &lt;/simditor&gt;
  &lt;/div&gt;
&lt;/template&gt;

&lt;script&gt;
import Simditor from './components/simditor'
export default {
  name: 'app',
  data(){
    return {
      content:'',
      options: {
        placeHolder: 'this is placeHolder',
        toolbarFloat: false,
        toolbar: ['title', 'bold', 'italic', 'underline', 'strikethrough', 'fontScale', 'color', '|', 'ol', 'ul', 'blockquote', 'code', 'table', '|', 'link', 'image', 'hr', '|', 'indent', 'outdent', 'alignment'],//, 'html'
        pasteImage:true,
        tabIndent: true,
        upload:{
          url : 'http://...', //文件上传的接口地址
          params: null, //键值对,指定文件上传接口的额外参数,上传的时候随文件一起提交
          fileKey: 'file', //服务器端获取文件数据的参数名
          connectionCount: 3,
          leaveConfirm: '正在上传文件'
        }
      }
    }
  },
  components: {
    Simditor
  },
  methods:{
    change(val){
      this.content=val;
      console.log(val)  //以html格式获取simditor的正文内容
    },
  },
}
&lt;/script&gt;</code></pre>
<p>设置默认值</p>
<pre><code>&lt;simditor id="e1" ref="e1" :options="simditorOptions" @change="simditorChange"&gt;&lt;/simditor&gt;

this.$refs.e1.editor.setValue(123)
</code></pre>
<h1>参考</h1>
<p>1️⃣
<a href="https://github.com/mycolorway/simditor/pull/540">https://github.com/mycolorway/simditor/pull/540</a>
<img src="https://www.cuiwei.net/data/upload/2021-06-09/162322158942275.jpg" alt="image.png" /></p>
<p>2️⃣
<a href="https://blog.csdn.net/liuzhumin123/article/details/79828224">https://blog.csdn.net/liuzhumin123/article/details/79828224</a></p></div>]]></description>
            <guid isPermaLink="false">基于JQuery的富文本编辑器 - Simditor的使用</guid>
        </item>
        <item>
            <title><![CDATA[Android段子类app - 相乐搞笑]]></title>
            <description><![CDATA[<link rel="stylesheet" href="https://www.cuiwei.net/static/css/github-markdown.min.css?t=20260430" type="text/css" media="screen" /><div class="markdown-body"><h1>相乐搞笑(xiangle-android)</h1>
<p>之前分享了 <a href="https://github.com/chudaozhe/xiangle-ios/">xiangle-ios</a> ，现把Android版也分享出来</p>
<h1>项目介绍</h1>
<pre><code class="language-text">主框架: TabLayout+ViewPager
子页面: 所有子页面共用一个Activity,具体实现集中在Fragment
网络请求: retrofit2+rxjava3
图片加载: glide
列表加载: RecyclerView或ListView
文件存储: OSS(sts方式)
下拉刷新，上拉加载: SmartRefreshLayout
token存储: SharedPreferences

目录结构
adapter: 所有RecyclerView或ListView的适配器
bean: 主要是网络请求返回数据的实体
fragment: 子页面的fragment
listener: 两个监听器，列表(list)和详情(detail)（以接口返回的数据来区分）；每个监听器定义两个方法：onSuccess，onError；有网络请求的fragment需实现list/detail监听器
model: 所有接口的model
service: 所有接口的定义，定义完给model使用
utility: 工具类，如屏幕信息，app信息，retrofit，oss等
view: 自定义view，如九宫格，圆形头像，弹窗（DialogFragment）</code></pre>
<h1>介绍</h1>
<p>分享风趣幽默的段子/视频/图片</p>
<pre><code class="language-text">首页：视频/图片/文字 任你选择
详情：收藏/评论/点赞 雁过留声
发现：搜索/话题/活动 应有尽有
我的：收藏/评论/点赞 一个不少</code></pre>
<h1>截图</h1>
<p><img src="https://www.cuiwei.net/data/upload/2021-06-03/162271278231940.jpg" alt="3894968a24fa3320199ec14.jpg" />
<img src="https://www.cuiwei.net/data/upload/2021-06-03/162271279953387.jpg" alt="389496884283e5897cec945.jpg" />
<img src="https://www.cuiwei.net/data/upload/2021-06-03/162271281282088.jpg" alt="3894968a64dbc664054f892.jpg" />
<img src="https://www.cuiwei.net/data/upload/2021-06-03/162271282529039.jpg" alt="38949682d3f66e111c54efa.jpg" /></p>
<h1>快速开始</h1>
<p>1.使用Android Studio打开</p>
<h1>获取最新代码</h1>
<blockquote>
<p>github：<a href="https://github.com/chudaozhe/xiangle-android">https://github.com/chudaozhe/xiangle-android</a></p>
<p>gitee：<a href="https://gitee.com/chudaozhe/xiangle-android">https://gitee.com/chudaozhe/xiangle-android</a></p>
</blockquote></div>]]></description>
            <guid isPermaLink="false">Android段子类app - 相乐搞笑</guid>
        </item>
        <item>
            <title><![CDATA[iOS段子类app - 相乐搞笑]]></title>
            <description><![CDATA[<link rel="stylesheet" href="https://www.cuiwei.net/static/css/github-markdown.min.css?t=20260430" type="text/css" media="screen" /><div class="markdown-body"><h1>相乐搞笑(xiangle-ios)</h1>
<p>这个项目本来是用来创业的，但上架App Store几个月，下载量寥寥无几，可能方向不对，可能不懂运营...</p>
<p>现分享出来，看有没有需要的朋友</p>
<p>Android版在这里 <a href="https://github.com/chudaozhe/xiangle-android/">xiangle-android</a></p>
<h1>项目介绍</h1>
<pre><code class="language-text">布局采用纯代码的方式（frame+masonry），没有storyboard
主框架: 自定义UITabBarController+UIScrollView+自定义UINavigationController
网络请求: AFNetworking
图片加载: SDWebImage
列表加载: UITableView
文件存储: OSS(sts方式)
指示器(HUD): SVProgressHUD
下拉刷新，上拉加载: MJRefresh
json转模型: MJExtension
自动布局: Masonry
token存储: NSUserDefaults
包管理工具: CocoaPods

目录结构
Controller：控制器，里面针对不同模块建立对应子目录
Model：所有接口的model
View：cell,自定义view等，里面针对不同模块建立对应子目录
Bean：类似Java bean
Expand：扩展
Util：工具类</code></pre>
<h1>介绍</h1>
<p>分享风趣幽默的段子/视频/图片</p>
<pre><code class="language-text">首页：视频/图片/文字 任你选择
详情：收藏/评论/点赞 雁过留声
发现：搜索/话题/活动 应有尽有
我的：收藏/评论/点赞 一个不少</code></pre>
<h1>截图</h1>
<p><img src="https://www.cuiwei.net/data/upload/2021-06-03/162271243244938.jpg" alt="3894968c182f2137e5bcab9.jpg" />
<img src="https://www.cuiwei.net/data/upload/2021-06-03/162271244748433.jpg" alt="3894968ca50e9fddac82610.jpg" />
<img src="https://www.cuiwei.net/data/upload/2021-06-03/162271247242621.jpg" alt="3894968d09ecca8fe46f783.jpg" />
<img src="https://www.cuiwei.net/data/upload/2021-06-03/162271248344414.jpg" alt="38949685b26869fa8c031a1.jpg" /></p>
<h1>快速开始</h1>
<p>1.在项目根目录执行</p>
<pre><code>pod install</code></pre>
<p>2.使用 Xcode 打开<code>xiangle.xcworkspace</code>文件</p>
<h1>获取最新代码</h1>
<blockquote>
<p>github：<a href="https://github.com/chudaozhe/xiangle-ios">https://github.com/chudaozhe/xiangle-ios</a></p>
<p>gitee：<a href="https://gitee.com/chudaozhe/xiangle-ios">https://gitee.com/chudaozhe/xiangle-ios</a></p>
</blockquote></div>]]></description>
            <guid isPermaLink="false">iOS段子类app - 相乐搞笑</guid>
        </item>
        <item>
            <title><![CDATA[基于 vant-weapp 的企业展示型小程序]]></title>
            <description><![CDATA[<link rel="stylesheet" href="https://www.cuiwei.net/static/css/github-markdown.min.css?t=20260430" type="text/css" media="screen" /><div class="markdown-body"><p>该项目采用前后端分离的架构模式，主要包括3部分：</p>
<p>1、用户端：即小程序，UI组件使用的<code>Vant Weapp</code></p>
<p>请移步：<a href="https://github.com/chudaozhe/enterprise-weapp">https://github.com/chudaozhe/enterprise-weapp</a></p>
<p>2、管理员端：Vue + Element UI</p>
<p>请移步：<a href="https://github.com/chudaozhe/enterprise-admin">https://github.com/chudaozhe/enterprise-admin</a></p>
<p>3、服务端：GO + Mysql + Nginx + Redis</p>
<p>请移步：<a href="https://github.com/chudaozhe/enterprise-api">https://github.com/chudaozhe/enterprise-api</a></p>
<h2>在线体验</h2>
<pre><code>https://ent.uqiantu.com/console/
用户名：admin
密码：123456</code></pre>
<h2>快速开始</h2>
<p>1、启动服务</p>
<pre><code>docker-compose up -d</code></pre>
<p>2、连接mysql并导入<code>doc/all.sql</code></p>
<pre><code>ip: localhost
用户名：root
密码为空</code></pre>
<p>3、把管理员端的代码复制到<code>www/ent/web</code>目录</p>
<blockquote>
<p>注意是执行<code>npm run build</code>后得到的代码</p>
</blockquote>
<p>默认无需操作</p>
<p>4、访问测试接口，如果能打开说明服务正常</p>
<p><a href="http://localhost/v1/user/test">打开测试</a></p>
<p>5、配置用户端，即小程序端的接口前缀</p>
<p>默认无需操作</p>
<p>6、打开管理后台</p>
<p><a href="http://localhost/console/">打开登陆</a></p>
<pre><code>用户名：admin
密码：123456</code></pre>
<h2>常见问题</h2>
<h3>如何配置域名？</h3>
<p>该项目默认通过<code>localhost</code>访问，你也可以修改为自己的域名</p>
<p>1、在服务端 修改对应的配置文件，比如要修改test环境</p>
<pre><code>vi app/config/app-test.json

把里面的 app_host 字段修改为你的域名</code></pre>
<p>2、修改nginx配置文件</p>
<pre><code>vi etc/nginx/local.conf

把里面的 server_name 字段修改为你的域名</code></pre>
<h2>截图</h2>
<h3>管理员端</h3>
<p><img src="https://ent.uqiantu.com/data/screenshots/admin/11.jpg" alt="11.jpg" />
<img src="https://ent.uqiantu.com/data/screenshots/admin/22.jpg" alt="22.jpg" />
<img src="https://ent.uqiantu.com/data/screenshots/admin/33.jpg" alt="33.jpg" /></p>
<h3>用户端</h3>
<p><img src="https://ent.uqiantu.com/data/screenshots/user/home.jpg" alt="home.jpg" />
<img src="https://ent.uqiantu.com/data/screenshots/user/cases.jpg" alt="cases.jpg" />
<img src="https://ent.uqiantu.com/data/screenshots/user/news.jpg" alt="news.jpg" /></p>
<p>获取最新代码</p>
<blockquote>
<p>github：<a href="https://github.com/chudaozhe/gin-vue-weapp">https://github.com/chudaozhe/gin-vue-weapp</a></p>
<p>gitee：<a href="https://gitee.com/chudaozhe/gin-vue-weapp">https://gitee.com/chudaozhe/gin-vue-weapp</a></p>
</blockquote></div>]]></description>
            <guid isPermaLink="false">基于 vant-weapp 的企业展示型小程序</guid>
        </item>
        <item>
            <title><![CDATA[JavaScript语言基础 - 语句]]></title>
            <description><![CDATA[<link rel="stylesheet" href="https://www.cuiwei.net/static/css/github-markdown.min.css?t=20260430" type="text/css" media="screen" /><div class="markdown-body"><p>语句也称为流控制语句</p>
<h1>if语句</h1>
<pre><code class="language-text">let i=2;
if(i&gt;1){
    console.log(111);
}</code></pre>
<h1>do-while语句</h1>
<p>do-while语句是一种后测试循环语句，循环体内的语句至少执行一次</p>
<pre><code class="language-text">let i=0;
do{
    i+=2;
    console.log(i);//2,4,6,8,10
}while(i&lt;10);</code></pre>
<h1>while语句</h1>
<p>while语句是一种先测试循环语句</p>
<pre><code class="language-text">let i=0;
while(i&lt;10){
    i+=2;
    console.log(i);//2,4,6,8,10
}</code></pre>
<h1>for语句</h1>
<p>for语句是一种先测试循环语句,由初始化、条件表达式、循环后表达式 3项组成</p>
<pre><code class="language-text">for(i=0; i&lt;10; i++){
    console.log(i);
}
//等价于
let i=0;
while(i&lt;10){
    console.log(i);
    i++;
}
//初始化，条件表达式，循环后表达式，都不是必须的
for(;;){//无限循环
    console.log(1);
}
let i=0;
for(; i&lt;10;){//等价于while循环
    console.log(i);
    i++;
}</code></pre>
<h1>for-in语句</h1>
<p>for-in用于枚举对象中的非符号键属性(遍历key</p>
<pre><code class="language-text">for(const propName in window){//打印BOM对象window的所有属性
    console.log(propName);
}</code></pre>
<h1>for-of语句</h1>
<p>for-of用于遍历可迭代对象的元素（遍历value</p>
<pre><code class="language-text">for(const i of [1,2,3,4,5]){//遍历数组中所有元素
    console.log(i);//1，2，3，4，5
}</code></pre>
<h1>标签语句</h1>
<p>标签语句用于给语句加标签，应用场景是嵌套循环</p>
<pre><code class="language-text">start: for(const i of [1,2,3,4,5]){
    console.log(i);//1，2，3，4，5
}</code></pre>
<h1>break和continue语句</h1>
<pre><code class="language-text">
start: for(const i of [1,2,3,4,5]){//遍历数组中所有元素
    console.log(i);//1，2，3，4，5
    for(const j of [4,5,6,7,8]){
        console.log(j);
        if(i===3 &amp;&amp; j===5){
            break start;
        }
    }
}</code></pre>
<h1>with语句</h1>
<p>with语句主要用来限制代码的作用于，with语句影响性能切难于调试，一般不建议使用</p>
<pre><code class="language-text">with(location){
    let qs=search.substring(1);
    let hostName=hostname;
    let url=href;
}
//等价于
let qs=location.search.substring(1);
let hostName=location.hostname;
let url=location.href;</code></pre>
<h1>switch语句</h1>
<pre><code class="language-text">let a=1;
switch(a){
    case 0:
        //跳过
    case 1:
        console.log(a);
        break;
    default:
        console.log(11);
}</code></pre></div>]]></description>
            <guid isPermaLink="false">JavaScript语言基础 - 语句</guid>
        </item>
        <item>
            <title><![CDATA[JavaScript集合引用类型 - Array]]></title>
            <description><![CDATA[<link rel="stylesheet" href="https://www.cuiwei.net/static/css/github-markdown.min.css?t=20260430" type="text/css" media="screen" /><div class="markdown-body"><p>es数组也是一组有序的数据</p>
<h2>创建数组</h2>
<p>与对象一样，在使用数组字面量表示法创建的数组不会调用Array构造函数</p>
<pre><code class="language-text">let arr1=[];//等价于let arr1=new Array()
let arr2=["1", "2"];//包含2个元素的数组, 等价于let arr2=new Array("1", "2")
let arr3=new Array(2);//length为2的数组</code></pre>
<h3>from()和of()</h3>
<p>es6新增两个创建数组的静态方法</p>
<p>from()</p>
<pre><code class="language-text">console.log(Array.from("abcd"));//["a", "b", "c", "d"]
//通过集合，映射创建数组
let m=new Map().set(1, 2).set(3, 4);
let s=new Set().add(1).add(2).add(3);
console.log(Array.from(m));//[[1, 2], [3, 4]]
console.log(Array.from(s));//[1, 2, 3]
//浅复制
let a1=[1,2,3];
let a2=Array.from(a1);
console.log(a2);
console.log(a1===a2);//false
//可变参数
function getArgsArray(){
    return Array.from(arguments);
}
console.log(getArgsArray(1,2,3));</code></pre>
<p>of() 把一组参数转换成数组</p>
<pre><code class="language-text">console.log(Array.of(1,2,3));
console.log(Array.of(undefined));</code></pre>
<h2>数组空位</h2>
<p>使用数组字面量初始化数组时，可以使用一串逗号来创建空位（hole），es6规范每个空位的值为undefined</p>
<pre><code class="language-text">let options=[,,,,,];//包含5个元素的数组
for(const option of options){
    console.log(option===undefined);//true,true,true,true,true
}</code></pre>
<h2>数组索引</h2>
<p>数组元素的数量保存在length属性中，通过修改length可以从数组末尾删除/添加元素</p>
<pre><code class="language-text">let colors=["red", "blue", "green"];
colors.length=2;
console.log(colors[2]);//undefined

colors[colors.length]="t";//在数组末尾添加元素</code></pre>
<h2>检测数组</h2>
<pre><code class="language-text">console.log(Array.isArray("jj"))//false
console.log(Array.isArray([1,2]))//true</code></pre>
<h2>迭代器方法</h2>
<pre><code class="language-text">let colors=["red", "blue", "green"];
//keys()返回数组的迭代器
console.log(Array.from(colors.keys()));//[0,1,2]
//values()返回数组元素的迭代器
console.log(Array.from(colors.values()));//["red", "blue", "green"]
//entries()返回索引/值对对迭代器
console.log(Array.from(colors.entries()));//[[0,"red"], [1, "blue"], [2,"green"]]
//解构
for(const [idx, element] of colors.entries()){
    console.log(idx);
    console.log(element);
}</code></pre>
<h2>复制和填充方法</h2>
<p>es6新增了两个方法：批量复制copyWithin(), 填充数组fill()</p>
<pre><code class="language-text">let colors=[1,1,1,1,1];
//用"red"填充整个数组
console.log(colors.fill("red"));//[red,red,red]
colors.fill(0);//重置
//用"red"填充索引大于等于1的
console.log(colors.fill("red", 1));//[0, "red", "red", "red", "red"]
colors.fill(0);//重置
//用"red"填充索引大于等于1, 且小于3的
console.log(colors.fill("red", 1, 3));//[0, "red", "red", 0, 0]
colors.fill(0);//重置

//与fill()不同，copyWithin()会按照指定范围浅复制数组中的部分内容，然后插入到指定索引开始的位置
let ints, reset=()=&gt;ints=[1,2,3,4,5,6,7];
reset();
console.log(ints.copyWithin(2, 0));//从ints中复制索引0开始的内容，插入到索引2开始的位置, [1, 2, 1, 2, 3, 4, 5]
reset();
console.log(ints.copyWithin(2, 0, 3));//从ints中复制 索引0开始到3结束 的内容，插入到索引2开始的位置, [1, 2, 1, 2, 3, 6, 7]
reset();</code></pre>
<h2>转换方法</h2>
<pre><code class="language-text">let colors=["red", "blue", "green"];
console.log(colors.toString());//red,blue,green
console.log(colors.toLocaleString());//red,blue,green
console.log(colors);//["red", "blue", "green"]
alert(colors);//red,blue,green

let p1={
    toLocaleString(){
        return "p1 local string";
    },
    toString(){
        return "p1 string"
    }
}
let p2={
    toLocaleString(){
        return "p2 local string";
    },
    toString(){
        return "p2 string"
    }
}
let ps=[p1, p2];
console.log(ps.toString());//p1 string,p2 string
console.log(ps.toLocaleString());//p1 local string,p2 local string
alert(ps);//p1 string,p2 string</code></pre>
<p>继承toLocaleString()/toString()都返回数组值的逗号分隔的字符串，如果想使用其他分隔符，则可以使用join()方法</p>
<pre><code class="language-text">console.log(colors.join(" || "));//red || blue || green</code></pre>
<h2>栈方法</h2>
<p>栈：一种限制插入和删除的数据结构，先进后出</p>
<pre><code class="language-text">let ids=[1, 2, 3];
ids.push(4, 5);//在在数组末尾添加
ids.push(6);//在在数组末尾添加
console.log(ids);
let end=ids.pop();//弹出末尾的一个
console.log(end);
console.log(ids);</code></pre>
<h2>队列方法</h2>
<p>队列在列表末尾添加数据，从列表开头获取数据</p>
<pre><code class="language-text">let ids=[1,2,3,4,5];
ids.push(6);//在数组末尾添加元素
ids.unshift(9,8,7);//在数组开头添加元素
console.log(ids);
let first=ids.shift();//弹出列表中第一个
console.log(first);
console.log(ids);</code></pre>
<h2>排序方法</h2>
<p>数组有两个方法可以用来对元素重新排序：reverse(), sort()</p>
<pre><code class="language-text">let ids=[1,2,3,5];
//反转数组元素
console.log(ids.reverse());//[5,4,3,2,1]

let ids2=[0,1,5,10,15];
//console.log(ids2.sort(compare));//[15, 10, 5, 1, 0]
console.log(ids2.sort((a, b)=&gt;a&lt;b ? 1 : (a&gt;b ? -1 : 0) ));//简写，[15, 10, 5, 1, 0]
function compare(value1, value2){
    if(value1&lt;value2){
        return 1;
    }else if(value1&gt;value2){
        return -1;
    }else return 0;
}</code></pre>
<h2>操作方法</h2>
<p>concat(), slice(), splice()</p>
<pre><code class="language-text">//concat()
let ids=[1,2,3];
//在末尾添加数组
console.log(ids.concat([5,6]).concat(7,8));

//slice()
let ids2=[1,2,3,4,5,6];
//截取数组的一部分
console.log(ids2.slice(1));//[2, 3, 4, 5, 6]
console.log(ids2.slice(1, 4));//[2, 3, 4]

//splice()
let ids3=[1,2,3,4];
let removed=ids3.splice(0, 1);//删除第一项
console.log(removed);//[1]
console.log(ids3);//直接改变了原数组，[2, 3, 4]

let ids4=[1,2,3,4];
removed=ids4.splice(1, 0, "5", "6");//在位置1插入"5"，"6"两个元素
console.log(removed);//空数组，[]
console.log(ids4);//直接改变了原数组，[1, "5", "6", 2, 3, 4]

let ids5=[1,2,3,4];
ids5.splice(1, 1, "7", "8");//在位置1插入"7"，"8"两个元素, 删除一个元素
console.log(removed);//空数组，[]
console.log(ids5);//直接改变了原数组，[1, "7", "8", 3, 4]</code></pre>
<h2>搜索和位置方法</h2>
<p>es提供两类搜索数组的方法：按严格相等搜索 和按断言函数搜索</p>
<h3>3个严格相等的搜索方法</h3>
<p>indexOf(), lastIndexOf()返回元素所在的索引，未找到返回-1</p>
<pre><code class="language-text">let ids=[1,2,3,4,5,4];
console.log(ids.indexOf(4));//从开头搜索，3
console.log(ids.lastIndexOf(4));//从末尾搜索，5
console.log(ids.includes(4));//从开头搜索，true</code></pre>
<h3>断言函数</h3>
<p>find(),findIndex()方法使用了断言函数 </p>
<pre><code class="language-text">const people=[{name:"name1", age:11},   {name:"name2", age:22},];
console.log(people.find((element, index, array)=&gt;element.age&lt;12));//{name: "name1", age: 11}
console.log(people.findIndex((element, index, array)=&gt;element.age&lt;12));//0</code></pre>
<h2>迭代方法</h2>
<p>遍历数组，非常重要的知识点</p>
<p>es为数组定义了5个迭代方法：map(), forEach(), filter(), every(), some()</p>
<pre><code class="language-text">let ids=[1,2,3,4,5];
//每个item结果都返回true,结果才是true
let result=ids.every((item, index, array)=&gt;item&gt;2);
console.log(result);//false

//只要有一个item结果返回true,结果就是true
let result=ids.some((item, index, array)=&gt;item&gt;2);
console.log(result);//true

//筛选出大于2的
let result=ids.filter((item, index, array)=&gt;{
    return item&gt;2;
})
console.log(result);//筛选出大于2的, [3, 4, 5]

//有返回值
let result=ids.map((item,index, array)=&gt;{
    //index, array 可以省略
    console.log(item);
    return item;
})
console.log(result);//[1, 2, 3, 4, 5]

//没有返回值
ids.forEach((item, index, array)=&gt;{
    //index, array 可以省略
    console.log("value="+item+" index="+index);
    //console.log(array);
})</code></pre>
<h2>归并方法</h2>
<p>es为数组提供2个归并方法：reduce(), reduceRight()，两个方法仅仅是遍历顺序不一样</p>
<pre><code class="language-text">let ids=[1,2,3,4];
//从第一项开始遍历至最后一项
let result=ids.reduce((prev, cur, index, array)=&gt;prev+cur);
console.log(result);

//从最后一项开始遍历至第一项
let result=ids.reduceRight((prev, cur, index, array)=&gt;prev+cur);
console.log(result);</code></pre></div>]]></description>
            <guid isPermaLink="false">JavaScript集合引用类型 - Array</guid>
        </item>
        <item>
            <title><![CDATA[微信支付api v3支付回调的处理]]></title>
            <description><![CDATA[<link rel="stylesheet" href="https://www.cuiwei.net/static/css/github-markdown.min.css?t=20260430" type="text/css" media="screen" /><div class="markdown-body"><p>微信支付回调返回的参数（POST）</p>
<ul>
<li>Wechatpay-Serial（header）</li>
<li>Wechatpay-Signature（header）</li>
<li>Wechatpay-Timestamp（header）</li>
<li>Wechatpay-Nonce（header）</li>
<li>主体（body）</li>
</ul>
<h1>验证签名</h1>
<pre><code>$verify=$smpw-&gt;_sign_verify([$timestamp, $nonce, $body], $signature);
if ($verify==1){
    echo 'ok';
}else echo 'failure';

    /**
     * 支付回调（验证签名
     * @param $data
     * @param $signature
     * @return int
     */
    public function _sign_verify($data, $signature): int {
        $message = implode("\n", $data) . "\n";
        $pu_key = openssl_pkey_get_public(file_get_contents('微信公钥的绝对地址'));//wxp_pub.pem1️⃣
        return openssl_verify(str_replace("\n\n", "\n", $message), base64_decode($signature), $pu_key, 'sha256WithRSAEncryption');
    }</code></pre>
<h1>参数解密</h1>
<pre><code>$obj=new AesUtil('API v3密钥');//1️⃣商户后台-&gt;账户中心-&gt;API安全-&gt;APIv3密钥
$body = json_decode($body, true);
if ($body['event_type']=='TRANSACTION.SUCCESS'){//通知类型
    $resource = $body['resource'];
    $decryption = $obj-&gt;decryptToString($resource['associated_data'], $resource['nonce'], $resource['ciphertext']);
    $result = json_decode($decryption, true);
    //var_dump($result);exit;
    //验证交易状态$result['trade_state']是否为SUCCESS
    //验证$result['mchid'], $result['appid']是否正确
    //验证实际支付金额$result['amount']['payer_total']和应支付金额是否一致
    //通过我们的支付单号$result['out_trade_no']来处理后续流程
    //通知应答
    echo json_encode(['code'=&gt;'SUCCESS', 'message'=&gt;'成功'], JSON_UNESCAPED_UNICODE);
}</code></pre>
<h1>参考</h1>
<blockquote>
<p><a href="https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_1_5.shtml">https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_1_5.shtml</a></p>
<p>1️⃣ 微信公钥的获取：<a href="https://www.cuiwei.net/p/1351071019">https://www.cuiwei.net/p/1351071019</a></p>
</blockquote></div>]]></description>
            <guid isPermaLink="false">微信支付api v3支付回调的处理</guid>
        </item>
        <item>
            <title><![CDATA[微信JS-SDK和WeixinJSBridge的区别]]></title>
            <description><![CDATA[<link rel="stylesheet" href="https://www.cuiwei.net/static/css/github-markdown.min.css?t=20260430" type="text/css" media="screen" /><div class="markdown-body"><h1>官方解释</h1>
<p>使用 WeixinJSBridge 预览图片</p>
<pre><code>WeixinJSBridge.invoke('imagePreview', {
    current: 'http://inews.gtimg.com/newsapp_bt/0/1693121381/641',
    urls: [ // 所有图片的URL列表，数组格式
        'https://img1.gtimg.com/10/1048/104857/10485731_980x1200_0.jpg',
        'https://img1.gtimg.com/10/1048/104857/10485726_980x1200_0.jpg',
        'https://img1.gtimg.com/10/1048/104857/10485729_980x1200_0.jpg'
    ]
}, function(res) {
    console.log(res.err_msg)
})</code></pre>
<p>实际上，微信官方是没有对外暴露过如此调用的，此类 API 最初是提供给腾讯内部一些业务使用，很多外部开发者发现了之后，依葫芦画瓢地使用了，逐渐成为微信中网页的事实标准。2015年初，微信发布了一整套网页开发工具包，称之为 JS-SDK，开放了拍摄、录音、语音识别、二维码、地图、支付、分享、卡券等几十个API。给所有的 Web 开发者打开了一扇全新的窗户，让所有开发者都可以使用到微信的原生能力，去完成一些之前做不到或者难以做到的事情。</p>
<p>同样是调用原生的浏览图片，使用 JS-SDK 调用图片预览组件</p>
<pre><code>wx.previewImage({
  current: 'https://img1.gtimg.com/10/1048/104857/10485726_980x1200_0.jpg',
  urls: [ // 所有图片的URL列表，数组格式
    'https://img1.gtimg.com/10/1048/104857/10485731_980x1200_0.jpg',
    'https://img1.gtimg.com/10/1048/104857/10485726_980x1200_0.jpg',
    'https://img1.gtimg.com/10/1048/104857/10485729_980x1200_0.jpg'
  ],
  success: function(res) {
    console.log(res)
  }
})</code></pre>
<p>JS-SDK是对之前的 WeixinJSBridge 的一个包装，以及新能力的释放</p>
<h1>其他</h1>
<p>使用 WeixinJSBridge 拉起微信支付</p>
<pre><code>onBridgeReady(field) {
  WeixinJSBridge.invoke('getBrandWCPayRequest', {
    "appId": field.appId,     //公众号ID，由商户传入
    "timeStamp": field.timeStamp.toString(),     //时间戳，自1970年以来的秒数
    "nonceStr": field.nonceStr,      //随机串
    "package": field.package,
    "signType": field.signType,     //微信签名方式：
    "paySign": field.paySign, //微信签名
  },
  function (res) {
    // if (res.err_msg == "get_brand_wcpay_request:ok") {
    // 使用以上方式判断前端返回,微信团队郑重提示：
    //res.err_msg将在用户支付成功后返回ok，但并不保证它绝对可靠。
    // }
    console.log(res);
    this.$router.push({path: '/order/waiting', query: {pay_no: field.pay_no}})
  });
},

if (typeof WeixinJSBridge == "undefined") {
  if (document.addEventListener) {
    document.addEventListener('WeixinJSBridgeReady', this.onBridgeReady(field), false);
  } else if (document.attachEvent) {
    document.attachEvent('WeixinJSBridgeReady', this.onBridgeReady(field));
    document.attachEvent('onWeixinJSBridgeReady', this.onBridgeReady(field));
  }
} else {
  this.onBridgeReady(field);
}</code></pre>
<p>使用 JS-SDK 拉起微信支付</p>
<pre><code>wx.chooseWXPay({
  timestamp: field.timeStamp, // 支付签名时间戳，注意微信jssdk中的所有使用timestamp字段均为小写。但最新版的支付后台生成签名使用的timeStamp字段名需大写其中的S字符
  nonceStr: field.nonceStr, // 支付签名随机串，不长于 32 位
  package: field.package, // 统一支付接口返回的prepay_id参数值，提交格式如：prepay_id=\*\*\*）
  signType: field.signType, // 签名方式，默认为'SHA1'，使用新版支付需传入'MD5'
  paySign: field.paySign, // 支付签名
  success: function (res) {
    // 支付成功后的回调函数
    console.log(res);
    this.$router.push({path: '/order/waiting', query: {pay_no: field.pay_no}})
  },
  // 支付取消回调函数
  cancel: function (res) {
    console.log(res)
    alert('用户取消支付~')
  },
  fail: function (res) {
    console.log(res);
    this.$router.push({path: '/order/waiting', query: {pay_no: field.pay_no}})
  }
});</code></pre>
<p>参考：</p>
<blockquote>
<p><a href="https://developers.weixin.qq.com/miniprogram/dev/framework/quickstart/#小程序简介">https://developers.weixin.qq.com/miniprogram/dev/framework/quickstart/#小程序简介</a>
<a href="https://developers.weixin.qq.com/doc/offiaccount/OA_Web_Apps/JS-SDK.html#4">https://developers.weixin.qq.com/doc/offiaccount/OA_Web_Apps/JS-SDK.html#4</a></p>
</blockquote></div>]]></description>
            <guid isPermaLink="false">微信JS-SDK和WeixinJSBridge的区别</guid>
        </item>
        <item>
            <title><![CDATA[vue中使用微信jssdk]]></title>
            <description><![CDATA[<link rel="stylesheet" href="https://www.cuiwei.net/static/css/github-markdown.min.css?t=20260430" type="text/css" media="screen" /><div class="markdown-body"><p>安装(非官方)</p>
<pre><code>npm install weixin-js-sdk --save</code></pre>
<p>使用</p>
<pre><code>import wx from 'weixin-js-sdk';

mounted(){
    //jsconfig
    this.jsConfig();
},
methods: {
    jsConfig: async function() {
        let field = await jsSDK();//网络请求
        wx.config(field);
    },
}</code></pre></div>]]></description>
            <guid isPermaLink="false">vue中使用微信jssdk</guid>
        </item>
        <item>
            <title><![CDATA[微信公众号网页授权配置]]></title>
            <description><![CDATA[<link rel="stylesheet" href="https://www.cuiwei.net/static/css/github-markdown.min.css?t=20260430" type="text/css" media="screen" /><div class="markdown-body"><p>公众号后台-&gt;开发-&gt;接口权限-&gt;网页服务-&gt;网页授权
<img src="https://www.cuiwei.net/data/upload/2021-05-21/162160692742185.jpg" alt="1621606464935.jpg" />
<img src="https://www.cuiwei.net/data/upload/2021-05-21/162160750328573.jpg" alt="WX202105212230402x.png" /></p>
<p>公众号后台-&gt;开发-&gt;基本配置
<img src="https://www.cuiwei.net/data/upload/2021-05-23/162170387366345.jpg" alt="WX202105230116492x.png" /></p>
<p>开发者工具相关</p>
<p>公众号后台-&gt;开发-&gt;开发者工具-&gt;web开发者工具(绑定开发者微信号)
<img src="https://www.cuiwei.net/data/upload/2021-05-22/162169604087561.jpg" alt="WX202105222301362x.png" /></p></div>]]></description>
            <guid isPermaLink="false">微信公众号网页授权配置</guid>
        </item>
        <item>
            <title><![CDATA[解决 mysql8 报错 this is incompatible with sql_mode = only_full_group_by]]></title>
            <description><![CDATA[<link rel="stylesheet" href="https://www.cuiwei.net/static/css/github-markdown.min.css?t=20260430" type="text/css" media="screen" /><div class="markdown-body"><h1>报错内容</h1>
<pre><code>Error Code: 1055. Expression #1 of SELECT list is not in GROUP BY clause and contains nonaggregated column '{field}' which is not functionally dependent on columns in GROUP BY clause; this is incompatible with sql_mode=only_full_group_by 0.053 sec</code></pre>
<h1>确认运行模式</h1>
<blockquote>
<p>mysql&gt; select @@global.sql_mode;</p>
</blockquote>
<pre><code>ONLY_FULL_GROUP_BY,STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION</code></pre>
<h1>重新设置运行模式</h1>
<pre><code>vi /etc/my.cnf
[mysqld]
# 去掉ONLY_FULL_GROUP_BY即可
sql_mode=STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION</code></pre>
<h1>重启服务</h1>
<blockquote>
<p>service mysqld restart</p>
</blockquote></div>]]></description>
            <guid isPermaLink="false">解决 mysql8 报错 this is incompatible with sql_mode = only_full_group_by</guid>
        </item>
        <item>
            <title><![CDATA[前后端分离之项目部署]]></title>
            <description><![CDATA[<link rel="stylesheet" href="https://www.cuiwei.net/static/css/github-markdown.min.css?t=20260430" type="text/css" media="screen" /><div class="markdown-body"><p>前后端分离的架构模式被越来越多的中大型项目所采用，这就给项目部署提出了要求</p>
<h1>需求</h1>
<p>假如有这么一个系统</p>
<ul>
<li>用户端：提供给用户浏览的（纯前端项目，<a href="http://xx.com">http://xx.com</a>）</li>
<li>管理员端：供作者维护这个系统（纯前端项目，<a href="http://xx.com/admin">http://xx.com/admin</a>）</li>
<li>服务端：为用户端和管理员端提供接口（纯后端项目，<a href="http://xx.com/api">http://xx.com/api</a>）</li>
</ul>
<h1>nginx配置</h1>
<p>xx.com.conf</p>
<pre><code>server {
    listen       80;
    server_name  xx.com; # 用户端
    index index.php index.htm index.html default.html;
    root      /data/www/shop-h5/web;

    # 服务端
    location /api {
        proxy_pass   http://127.0.0.1:8089/;
        include conf.d/proxy.md;
    }
    # 管理员端
    location /admin/ {
        alias  /data/www/shop-admin/web/;
        break;
    }

}</code></pre>
<p>xx8089.conf</p>
<pre><code>server {
    listen 8089;
    index default.html index.htm index.html index.php; 
    root /data/www/shop/web;

    rewrite ^/admin/(.*)$ /admin.php/admin/$1 last;
    rewrite ^/user/(.*)$ /user.php/user/$1 last;

    location ~ \.php {
        fastcgi_pass   127.0.0.1:9000;
        fastcgi_split_path_info ^((?U).+.php)(/?.+)$;
        fastcgi_param PATH_INFO $fastcgi_path_info;
        fastcgi_param ENV_MODE production;
        include        fastcgi_params;
    }

}</code></pre></div>]]></description>
            <guid isPermaLink="false">前后端分离之项目部署</guid>
        </item>
        <item>
            <title><![CDATA[微信支付api v3获取平台证书]]></title>
            <description><![CDATA[<link rel="stylesheet" href="https://www.cuiwei.net/static/css/github-markdown.min.css?t=20260430" type="text/css" media="screen" /><div class="markdown-body"><h1>GET 获取平台证书列表</h1>
<blockquote>
<p><a href="https://api.mch.weixin.qq.com/v3/certificates">https://api.mch.weixin.qq.com/v3/certificates</a></p>
</blockquote>
<p>访问成功可得到类似数据</p>
<pre><code>[
    {
        "effective_time": "2021-05-19T18:40:14+08:00",
        "encrypt_certificate": {
            "algorithm": "AEAD_AES_256_GCM",
            "associated_data": "certificate",
            "ciphertext": "...==",
            "nonce": "c20fb6175ecb"
        },
        "expire_time": "2026-05-18T18:40:14+08:00",
        "serial_no": "50E3553125B..."
    }
]</code></pre>
<p>解密</p>
<pre><code>$obj=new AesUtil('API v3密钥');//商户后台-&gt;账户中心-&gt;API安全-&gt;APIv3密钥
echo $obj-&gt;decryptToString('associated_data...', 'nonce...', 'ciphertext...').PHP_EOL;</code></pre>
<p>将以上结果保存到wxp_cert.pem文件</p>
<p>然后获取公钥</p>
<blockquote>
<p>openssl x509 -in wxp_cert.pem -pubkey -noout &gt; wxp_pub.pem</p>
</blockquote>
<h1>附件</h1>
<p>官方提供的解密工具类</p>
<pre><code>class AesUtil{
  /**
    * AES key
    *
    * @var string
    */
  private $aesKey;

  const KEY_LENGTH_BYTE = 32;
  const AUTH_TAG_LENGTH_BYTE = 16;

  /**
    * Constructor
    */
  public function __construct($aesKey)
  {
      if (strlen($aesKey) != self::KEY_LENGTH_BYTE) {
          throw new InvalidArgumentException('无效的ApiV3Key，长度应为32个字节');
      }
      $this-&gt;aesKey = $aesKey;
  }

  /**
    * Decrypt AEAD_AES_256_GCM ciphertext
    *
    * @param string    $associatedData     AES GCM additional authentication data
    * @param string    $nonceStr           AES GCM nonce
    * @param string    $ciphertext         AES GCM cipher text
    *
    * @return string|bool      Decrypted string on success or FALSE on failure
    */
  public function decryptToString($associatedData, $nonceStr, $ciphertext)
  {
      $ciphertext = \base64_decode($ciphertext);
      if (strlen($ciphertext) &lt;= self::AUTH_TAG_LENGTH_BYTE) {
          return false;
      }

      // ext-sodium (default installed on &gt;= PHP 7.2)
      if (function_exists('\sodium_crypto_aead_aes256gcm_is_available') &amp;&amp;
          \sodium_crypto_aead_aes256gcm_is_available()) {
          return \sodium_crypto_aead_aes256gcm_decrypt($ciphertext, $associatedData, $nonceStr, $this-&gt;aesKey);
      }

      // ext-libsodium (need install libsodium-php 1.x via pecl)
      if (function_exists('\Sodium\crypto_aead_aes256gcm_is_available') &amp;&amp;
          \Sodium\crypto_aead_aes256gcm_is_available()) {
          return \Sodium\crypto_aead_aes256gcm_decrypt($ciphertext, $associatedData, $nonceStr, $this-&gt;aesKey);
      }

      // openssl (PHP &gt;= 7.1 support AEAD)
      if (PHP_VERSION_ID &gt;= 70100 &amp;&amp; in_array('aes-256-gcm', \openssl_get_cipher_methods())) {
          $ctext = substr($ciphertext, 0, -self::AUTH_TAG_LENGTH_BYTE);
          $authTag = substr($ciphertext, -self::AUTH_TAG_LENGTH_BYTE);

          return \openssl_decrypt($ctext, 'aes-256-gcm', $this-&gt;aesKey, \OPENSSL_RAW_DATA, $nonceStr,
              $authTag, $associatedData);
      }

      throw new \RuntimeException('AEAD_AES_256_GCM需要PHP 7.1以上或者安装libsodium-php');
  }
}</code></pre></div>]]></description>
            <guid isPermaLink="false">微信支付api v3获取平台证书</guid>
        </item>
        <item>
            <title><![CDATA[申请微信支付]]></title>
            <description><![CDATA[<link rel="stylesheet" href="https://www.cuiwei.net/static/css/github-markdown.min.css?t=20260430" type="text/css" media="screen" /><div class="markdown-body"><h1>注册商户号</h1>
<blockquote>
<p><a href="https://pay.weixin.qq.com/index.php/apply/applyment_home/guide_normal">https://pay.weixin.qq.com/index.php/apply/applyment_home/guide_normal</a></p>
</blockquote>
<p>所需资料</p>
<ul>
<li>姓名，手机号，邮箱</li>
<li>营业执照，对公账户，法人身份证</li>
<li>商户简称，客服电话</li>
<li>服务号appid(如果需要公众号支付的话)</li>
</ul>
<p>整个过程大概30分钟（其中审核等待20分钟）</p>
<h1>支付配置</h1>
<p>登陆商户后台</p>
<blockquote>
<p><a href="https://pay.weixin.qq.com/index.php/core/info">https://pay.weixin.qq.com/index.php/core/info</a></p>
</blockquote>
<p>产品中心-&gt;我的产品-&gt;支付产品
<img src="https://www.cuiwei.net/data/upload/2021-05-18/162134884215053.jpg" alt="WX202105182239462x.png" /></p>
<p>产品中心-&gt;开发配置-&gt;支付配置
<img src="https://www.cuiwei.net/data/upload/2021-05-18/162134846353230.jpg" alt="WX202105181822092x.png" /></p>
<p>产品中心-&gt;AppID账号管理
<img src="https://www.cuiwei.net/data/upload/2021-05-18/162133390040635.jpg" alt="WX202105181824442x.png" /></p>
<p>注：如果“关联状态”为“待授权”，应该到对应的appid的后台确认关联（比如服务号后台-&gt;微信支付-&gt;待关联商户号）</p>
<p>账户中心-&gt;API安全
<img src="https://www.cuiwei.net/data/upload/2021-05-19/162143409037747.jpg" alt="WX202105192219552x.png" />
<img src="https://www.cuiwei.net/data/upload/2021-05-19/162143411784294.jpg" alt="WX202105192220302x.png" /></p>
<p>查看证书序列号1</p>
<blockquote>
<p>openssl x509 -in apiclient_cert.pem -noout -serial</p>
</blockquote>
<p>查看证书序列号2
<img src="https://www.cuiwei.net/data/upload/2021-05-19/162143381048780.jpg" alt="WX202105191851352x.png" /></p></div>]]></description>
            <guid isPermaLink="false">申请微信支付</guid>
        </item>
        <item>
            <title><![CDATA[PHPMailer的使用 —— 发送邮件]]></title>
            <description><![CDATA[<link rel="stylesheet" href="https://www.cuiwei.net/static/css/github-markdown.min.css?t=20260430" type="text/css" media="screen" /><div class="markdown-body"><p>安装</p>
<pre><code>composer require phpmailer/phpmailer</code></pre>
<p>demo</p>
<pre><code>&lt;?php
require 'vendor/autoload.php';
use PHPMailer\PHPMailer\PHPMailer;

//test
sendmail('111@qq.com', 'test', 'ccc');

/**
* 邮件发送函数
* @param string $to    接收邮件者邮箱
* @param string $subject 邮件主题
* @param string $body    邮件内容
* @param string $attachment 附件列表
* @return boolean
*/
function sendmail($to, $subject = '', $body = '', $attachment = null){
    //邮件配置
    $config = [
        'SMTP_HOST'   =&gt; 'smtp.mxhichina.com',
        'SMTP_PORT'   =&gt; '25',
        'SMTP_USER'   =&gt; 'notifications-noreply@xx.com',
        'SMTP_PASS'   =&gt; '123456',
        'FROM_EMAIL'  =&gt; 'notifications-noreply@xx.com',
        'FROM_NAME'   =&gt; 'notifications-noreply',
        'REPLY_EMAIL' =&gt; '',
        'REPLY_NAME'  =&gt; ''
    ];

    $mail             = new PHPMailer;
    $mail-&gt;CharSet    = 'UTF-8';
    $mail-&gt;IsSMTP();
    // 1 = errors and messages
    // 2 = messages only
    $mail-&gt;SMTPDebug  = 0;                     // 关闭SMTP调试功能
    $mail-&gt;SMTPAuth   = true;                  // 启用 SMTP 验证功能
    // $mail-&gt;SMTPSecure = 'ssl';                 // 使用安全协议
    $mail-&gt;Host       = $config['SMTP_HOST'];
    $mail-&gt;Port       = $config['SMTP_PORT'];
    $mail-&gt;Username   = $config['SMTP_USER'];
    $mail-&gt;Password   = $config['SMTP_PASS'];
    $mail-&gt;SetFrom($config['FROM_EMAIL'], $config['FROM_NAME']);
    $replyEmail       = $config['REPLY_EMAIL']?$config['REPLY_EMAIL']:$config['FROM_EMAIL'];
    $replyName        = $config['REPLY_NAME']?$config['REPLY_NAME']:$config['FROM_NAME'];
    $mail-&gt;AddReplyTo($replyEmail, $replyName);
    $mail-&gt;Subject    = $subject;
    $mail-&gt;MsgHTML($body);
    $mail-&gt;AddAddress($to, '');
    // 添加附件
    if(is_array($attachment)){
        foreach ($attachment as $file){
            is_file($file) &amp;&amp; $mail-&gt;AddAttachment($file);
        }
    }
    return $mail-&gt;Send() ? true : $mail-&gt;ErrorInfo;
}</code></pre></div>]]></description>
            <guid isPermaLink="false">PHPMailer的使用 —— 发送邮件</guid>
        </item>
        <item>
            <title><![CDATA[PhpSpreadsheet(PHPExcel)的使用 —— 生成/读取excel]]></title>
            <description><![CDATA[<link rel="stylesheet" href="https://www.cuiwei.net/static/css/github-markdown.min.css?t=20260430" type="text/css" media="screen" /><div class="markdown-body"><p>PHPExcel已经不再维护，PhpSpreadsheet是PHPExcel的下一个版本</p>
<h1>安装</h1>
<pre><code>composer require phpoffice/phpspreadsheet</code></pre>
<h1>生成excel</h1>
<pre><code># conf.php
&lt;?php
//表头样式
use PhpOffice\PhpSpreadsheet\Style\Alignment;
use PhpOffice\PhpSpreadsheet\Style\Border;
//use PhpOffice\PhpSpreadsheet\Style\Font;

$alignment=['vertical'=&gt; Alignment::VERTICAL_CENTER, 'rotation'=&gt;0, 'wrap'=&gt;true];
return [
    'head'=&gt;[
        'font'=&gt;['bold' =&gt; true],
        'alignment'=&gt;[
            'horizontal'=&gt;Alignment::HORIZONTAL_CENTER,
            'vertical'=&gt;Alignment::VERTICAL_CENTER
        ],
        //'borders'=&gt;[
        //  'outline'=&gt;['borderStyle' =&gt; Border::BORDER_MEDIUM, 'color' =&gt; ['rgb' =&gt; '508630']],
        //  'bottom'=&gt;['borderStyle'=&gt;Border::BORDER_MEDIUM],
        //],
    ],
    'font'=&gt;[ //标题样式
        'font' =&gt; [
            'name'=&gt;'微软雅黑',
            'size'=&gt;12,
            'bold'=&gt;true,
            'italic'=&gt;false,
            //'underline'=&gt;Font::UNDERLINE_DOUBLE,
            //'strike'=&gt;false,
            'color'=&gt;['rgb' =&gt; 'ff0000']
        ],
    ],
    'alignment_left'=&gt;array_merge($alignment,array('horizontal' =&gt; Alignment::HORIZONTAL_LEFT)),
    'alignment_right'=&gt;array_merge($alignment,array('horizontal' =&gt; Alignment::HORIZONTAL_RIGHT)),
    'alignment_center'=&gt;array_merge($alignment,array('horizontal' =&gt; Alignment::HORIZONTAL_CENTER)),
];</code></pre>
<pre><code>#index.php
&lt;?php
require 'vendor/autoload.php';
use PhpOffice\PhpSpreadsheet\Spreadsheet;
use PhpOffice\PhpSpreadsheet\Writer\Xlsx;
use PhpOffice\PhpSpreadsheet\Worksheet\PageSetup;

$conf=include 'conf.php';
$spreadsheet = new Spreadsheet();
$data=[
    ['1', 'cw', '1862080', '2000-01-01'],
    ['2', 'cw', '1862080', '2000-01-01'],
    ['3', 'cw', '1862080', '2000-01-01'],
    ['4', 'cw', '1862080', '2000-01-01'],
];
$spreadsheet-&gt;setActiveSheetIndex(0)
    -&gt;setCellValue('A5','ID')
    -&gt;setCellValue('B5','姓名')
    -&gt;setCellValue('C5','电话')
    -&gt;setCellValue('D5','时间');

//设置选定sheet表名
$spreadsheet-&gt;getActiveSheet()-&gt;setTitle('Sheet1');
//设置字体样式
$spreadsheet-&gt;getActiveSheet()-&gt;getStyle('A1')-&gt;getFont()-&gt;setName('Arial')-&gt;setSize(25);//-&gt;setUnderline(true);-&gt;getColor()-&gt;setARGB('FFFF0000');-&gt;setBold(true);
//合并单元格 给单元格赋值(数值，字符串，公式)
$spreadsheet-&gt;getActiveSheet()-&gt;mergeCells('A1:D3')-&gt;setCellValue('A1', '活动数据列表');
$spreadsheet-&gt;getActiveSheet()-&gt;mergeCells('A4:D4')-&gt;setCellValue('A4', "导表时间：".date("Y-m-d H:i:s"));
$spreadsheet-&gt;getActiveSheet()-&gt;getStyle('A4')-&gt;getAlignment()-&gt;applyFromArray($conf['alignment_right']);

//设置单列宽度
$spreadsheet-&gt;getActiveSheet()-&gt;getColumnDimension('A')-&gt;setAutoSize(true);
$spreadsheet-&gt;getActiveSheet()-&gt;getColumnDimension('B')-&gt;setWidth(12);
//$spreadsheet-&gt;getActiveSheet()-&gt;getColumnDimension('G')-&gt;setRowHeight(50);
$spreadsheet-&gt;getActiveSheet()-&gt;getColumnDimension('C')-&gt;setWidth(12);
$spreadsheet-&gt;getActiveSheet()-&gt;getColumnDimension('D')-&gt;setWidth(20);

$spreadsheet-&gt;getActiveSheet()-&gt;getStyle('A1:D3')-&gt;applyFromArray($conf['head']);
//-&gt;getAlignment()-&gt;getHorizontal('');
//-&gt;getBorders()-&gt;getTop()-&gt;setBorderStyle('');
//-&gt;setWrapText(true);自动换行

$spreadsheet-&gt;getActiveSheet()-&gt;getStyle('A5:D5')-&gt;getAlignment()-&gt;applyFromArray($conf['alignment_center']);
$spreadsheet-&gt;getActiveSheet()-&gt;getStyle('A5:D5')-&gt;applyFromArray($conf['font']);
$spreadsheet-&gt;getActiveSheet()-&gt;getStyle('A:D')-&gt;getAlignment()-&gt;applyFromArray($conf['alignment_left']);

//内容部分
foreach($data as $k =&gt; $v) {
    $i=$k+6;
    $spreadsheet-&gt;setActiveSheetIndex(0)
        -&gt;setCellValue('A'.$i, $v[0])
        -&gt;setCellValue('B'.$i, $v[1])
        -&gt;setCellValue('C'.$i, $v[2])
        -&gt;setCellValue('D'.$i, $v[3]);
}

//设置打印页边距
$spreadsheet-&gt;getActiveSheet()-&gt;getPageMargins()-&gt;setTop(0);
$spreadsheet-&gt;getActiveSheet()-&gt;getPageMargins()-&gt;setRight(0);
$spreadsheet-&gt;getActiveSheet()-&gt;getPageMargins()-&gt;setLeft(0);
$spreadsheet-&gt;getActiveSheet()-&gt;getPageMargins()-&gt;setBottom(0);
//设置纸张类型
$spreadsheet-&gt;getActiveSheet()-&gt;getPageSetup()-&gt;setPaperSize(PageSetup::PAPERSIZE_A4);
//设置自动筛选
$spreadsheet-&gt;getActiveSheet()-&gt;setAutoFilter('A5:D5');
//设置自动换行
$spreadsheet-&gt;getActiveSheet()-&gt;getStyle('B6:B5')-&gt;getAlignment()-&gt;setWrapText(true);

$writer = new Xlsx($spreadsheet);

$writer-&gt;save('x1.xlsx');
echo 'ok';</code></pre>
<h1>读取</h1>
<pre><code>#read.php
&lt;?php
require 'vendor/autoload.php';
use PhpOffice\PhpSpreadsheet\IOFactory;

$inputFileName = __DIR__ . '/x1.xlsx';
//方法1.1, 1.2, 2都可以

//方法1：自动识别文件类型
//方法1.1
$spreadsheet = IOFactory::load($inputFileName);//自动识别文件类型
//方法1.2
//$spreadsheet= IOFactory::createReaderForFile($inputFileName)-&gt;setReadDataOnly(true)-&gt;load($inputFileName);

//方法2：指定文件类型
//$spreadsheet = IOFactory::createReader("Xlsx")-&gt;load($inputFileName);

$sheetData = $spreadsheet-&gt;getActiveSheet()-&gt;toArray(null, true, true, true);
var_dump($sheetData);
</code></pre>
<h1>问题</h1>
<p>如果提示以下错误，参考<a href="https://www.cuiwei.net/p/1021389132">编译安装php zip扩展</a></p>
<pre><code>Fatal error: Uncaught Error: Class "ZipArchive" not found</code></pre></div>]]></description>
            <guid isPermaLink="false">PhpSpreadsheet(PHPExcel)的使用 —— 生成/读取excel</guid>
        </item>
        <item>
            <title><![CDATA[编译安装php zip扩展]]></title>
            <description><![CDATA[<link rel="stylesheet" href="https://www.cuiwei.net/static/css/github-markdown.min.css?t=20260430" type="text/css" media="screen" /><div class="markdown-body"><p>源自一个错误（php操作excel的时候）</p>
<pre><code>Fatal error: Uncaught Error: Class "ZipArchive" not found</code></pre>
<p>解决方案</p>
<pre><code>cd /usr/local/src
wget http://pecl.php.net/get/zip-1.19.2.tgz
tar -xvzf zip-1.19.2.tgz 
cd zip-1.19.2
phpize 
./configure --with-php-config=/usr/bin/php-config
make &amp;&amp; make install

vi /data/apps/php/etc/php.ini 
extension=zip

service php-fpm reload</code></pre>
<p>如果configure这步提示 libzip 相关问题，参见：<a href="https://www.cuiwei.net/p/1697506232">CentOS编译安装libzip最新版</a></p></div>]]></description>
            <guid isPermaLink="false">编译安装php zip扩展</guid>
        </item>
        <item>
            <title><![CDATA[CentOS编译安装libzip最新版]]></title>
            <description><![CDATA[<link rel="stylesheet" href="https://www.cuiwei.net/static/css/github-markdown.min.css?t=20260430" type="text/css" media="screen" /><div class="markdown-body"><p>源自一个错误（安装php扩展: zip）</p>
<pre><code>checking for libzip &gt;= 0.11 libzip != 1.3.1 libzip != 1.7.0... no
configure: error: Package requirements (libzip &gt;= 0.11 libzip != 1.3.1 libzip != 1.7.0) were not met:

No package 'libzip' found
No package 'libzip' found
No package 'libzip' found
或者是 yum install libzip-devel 完，提示版本过低</code></pre>
<p>解决方案</p>
<pre><code>#依赖
yum install bzip2-devel

cd /usr/local/src
wget https://libzip.org/download/libzip-1.7.3.tar.gz
tar -xvzf libzip-1.7.3.tar.gz 
cd libzip-1.7.3
mkdir build
cd build/
cmake -DCMAKE_INSTALL_PREFIX=/data/apps/libs ..
make &amp;&amp; make install

#配置
export PKG_CONFIG_PATH=/data/apps/libs/lib64/pkgconfig:$PKG_CONFIG_PATH
#验证
pkg-config --list-all | grep libzip
</code></pre></div>]]></description>
            <guid isPermaLink="false">CentOS编译安装libzip最新版</guid>
        </item>
        <item>
            <title><![CDATA[CentOS编译安装cmake最新版]]></title>
            <description><![CDATA[<link rel="stylesheet" href="https://www.cuiwei.net/static/css/github-markdown.min.css?t=20260430" type="text/css" media="screen" /><div class="markdown-body"><p>由于使用的系统是centos7，所以一些yum源里的rpm包比较旧，需要手动编译</p>
<pre><code>cd /usr/local/src/
wget https://github.com/Kitware/CMake/releases/download/v3.20.2/cmake-3.20.2.tar.gz
tar xvzf cmake-3.20.2.tar.gz
cd cmake-3.20.2
./configure --prefix=/data/apps/cmake
make &amp;&amp; make install
ln -s /data/apps/cmake/bin/* /usr/bin/
</code></pre>
<p><a href="https://cmake.org/cmake/help/v3.20/manual/cmake.1.html">cmake参数说明</a></p></div>]]></description>
            <guid isPermaLink="false">CentOS编译安装cmake最新版</guid>
        </item>
        <item>
            <title><![CDATA[php生成站点地图sitemap]]></title>
            <description><![CDATA[<link rel="stylesheet" href="https://www.cuiwei.net/static/css/github-markdown.min.css?t=20260430" type="text/css" media="screen" /><div class="markdown-body"><pre><code>$data=[
    ['loc'=&gt;'https://www.cuiwei.net/', 'lastmod'=&gt;'2009-01-01'],//首页
];

$xml=createXML($data);
file_put_contents('sitemap.xml', $xml);

function createXML($data){
    $string = &lt;&lt;&lt;XML
&lt;?xml version="1.0" encoding="UTF-8"?&gt;
&lt;urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"&gt;
&lt;/urlset&gt;
XML;
    $xml = simplexml_load_string($string);
    foreach ($data as $item) {
        $url = $xml-&gt;addChild('url');
        if (is_array($item)) {
            foreach ($item as $key =&gt; $row) {
                $node = $url-&gt;addChild($key, $row);
            }
        }
    }
    return $xml-&gt;asXML();
}</code></pre>
<p>参考标准</p>
<blockquote>
<p><a href="https://www.sitemaps.org/protocol.html">https://www.sitemaps.org/protocol.html</a></p>
</blockquote></div>]]></description>
            <guid isPermaLink="false">php生成站点地图sitemap</guid>
        </item>
        <item>
            <title><![CDATA[mysql常用语句]]></title>
            <description><![CDATA[<link rel="stylesheet" href="https://www.cuiwei.net/static/css/github-markdown.min.css?t=20260430" type="text/css" media="screen" /><div class="markdown-body"><p>增删改查</p>
<pre><code>-- 查询
SELECT `title`, `content` FROM `article` WHERE `id` &gt; 0;

-- 新增
INSERT INTO `article`(`title`, `content`) VALUES('ttt','ccc');
INSERT INTO `article` VALUES(1, 'ttt','ccc');#省略字段名
INSERT INTO `article` (`title`, `content`) VALUES('ttt','ccc'), ('ttt2','ccc2');#批量插入
INSERT INTO `article` SET `title`='ttt', `content`='ccc' WHERE `id`=1;#使用update 语句的set方式插入数据

-- 更新
UPDATE `article` SET `title`='ttt2', `content`='ccc2' WHERE `id`=1;

-- 删除
DELETE FROM `article` WHERE `id`=1;</code></pre>
<p>复杂的</p>
<pre><code>--INSERT和SELECT一起用
INSERT INTO `order_goods` (`user_id`, `goods_id`, `title`, `prize`, `create_time`) SELECT ?, `id`, `title`, `prize`, ? FROM `goods` WHERE `id` = ?;#[1,2,3]</code></pre></div>]]></description>
            <guid isPermaLink="false">mysql常用语句</guid>
        </item>
        <item>
            <title><![CDATA[基于vue的markdown编辑器 - mavonEditor的使用]]></title>
            <description><![CDATA[<link rel="stylesheet" href="https://www.cuiwei.net/static/css/github-markdown.min.css?t=20260430" type="text/css" media="screen" /><div class="markdown-body"><h1>安装</h1>
<blockquote>
<p>npm install mavon-editor --save</p>
</blockquote>
<h1>基本使用</h1>
<p>全局注册(main.js</p>
<pre><code>import mavonEditor from 'mavon-editor'
import 'mavon-editor/dist/css/index.css'
Vue.use(mavonEditor)</code></pre>
<p>局部注册</p>
<pre><code>import { mavonEditor } from "mavon-editor";
import "mavon-editor/dist/css/index.css";
export default {
  data: function() {
    return {
      content: ""
    };
  },
components: {
   mavonEditor
},
}</code></pre>
<p><em>注：我这边尝试局部注册，会提示以下错误，暂时无解，只能选择全局的方式</em></p>
<pre><code>[Vue warn]: Failed to mount component: template or render function not defined.</code></pre>
<p>使用</p>
<pre><code>&lt;mavon-editor v-model="content"/&gt;</code></pre>
<h1>图片上传和回显</h1>
<p>注册方式同上，使用如下</p>
<pre><code>&lt;mavon-editor v-model="ruleForm.content" ref=md @imgAdd="$imgAdd" @imgDel="$imgDel"/&gt;

...
      $imgAdd(pos, file){
        // 将图片上传到服务器(formdata方式
        // var formdata = new FormData();
        // formdata.append('image', $file);

        // (x-www-form-urlencoded方式
          let _this = this;
          let data = {};

          this.uploadFile(file).then(async function(file){
            data.content = file.result.substr(22);
            data.type=file.type;
            data.width=file.width;
            data.height=file.height;
            data.size=file.size;
            data.title=file.name;
            let result=await addFile(data);//网络请求
            if(result.err===0){
              _this.$message({type:'success', message:'上传成功', duration:1000});
              _this.$refs.md.$imglst2Url([[pos, result.data.url]])
            }else{
              _this.$message({type:'error', message:result.msg, duration:1000});
            }
          });
      },
      //获取reader的result
      uploadFile:function (file){
        return new Promise(function(resolve, reject){
          let reader = new FileReader()
          reader.readAsDataURL(file);
          //reader.readAsArrayBuffer(file)
          reader.onload = function(){
            file.result=this.result;
            let image = new Image()
            image.src = this.result
            image.onload = function() {
              file.width=this.width;
              file.height=this.height;
              resolve(file)
            }
          }
        })
      },
      $imgDel(pos){
        console.log(pos);//["//blog.cw.net/data/upload/2021-05-11/162072210359836.jpg", file]
        #关于删除，仅仅是当前添加的可以删除；如果是从数据库读出来的，就没有删除按钮了
      },
...</code></pre></div>]]></description>
            <guid isPermaLink="false">基于vue的markdown编辑器 - mavonEditor的使用</guid>
        </item>
        <item>
            <title><![CDATA[php.ini常用配置]]></title>
            <description><![CDATA[<link rel="stylesheet" href="https://www.cuiwei.net/static/css/github-markdown.min.css?t=20260430" type="text/css" media="screen" /><div class="markdown-body"><h2>Language Options</h2>
<pre><code>#开启短标签
short_open_tag = On

#设置可执行php的目录，多个目录用冒号隔开
open_basedir = /data/www</code></pre>
<h2>Error handling and logging</h2>
<pre><code>#错误级别
error_reporting = E_ALL &amp; ~E_DEPRECATED

#禁止把错误输出到页面
display_errors = Off

#设置错误信息输出到文件
log_errors = On

#指定错误日志文件存储位置
error_log = /data/logs/php_errors.log
</code></pre>
<h2>Data Handling</h2>
<pre><code>#POST数据所允许的最大大小
post_max_size = 300M</code></pre>
<h2>File Uploads</h2>
<pre><code>#是否允许文件上传On/Off
file_uploads = On

#上传文件放置的临时目录
upload_tmp_dir = /data/tmp

#上传的文件的最大大小
upload_max_filesize = 200M

#最多上传多少个文件
max_file_uploads = 20</code></pre>
<h2>Module Settings</h2>
<pre><code>#设置时区
date.timezone = PRC

#设置session存储方式 files/memcache/redis
session.save_handler = files

#设置session文件存储位置"tcp://host1:11211?persistent=1&amp;weight=1&amp;timeout=1&amp;retry_interval=15"
session.save_path = "/data/tmp"

[curl]
; A default value for the CURLOPT_CAINFO option. This is required to be an
; absolute path.
; https://curl.se/ca/cacert.pem
curl.cainfo = /data/apps/php/cacert.pem

[xdebug]
;zend_extension的值根据自己的本地环境填写
zend_extension = xdebug.so
xdebug.idekey=PHPSTORM
xdebug.remote_enable = On
;宿主机ip
xdebug.remote_host=debug.cw.net
;xdebug.remote_port默认值为9000，这里需要跟phpstorm配置一致，下面有说明
xdebug.remote_port=9000
xdebug.remote_handler=dbgp
;产生的文件太大/tmp/trace.603883136.xt
xdebug.auto_trace = On</code></pre></div>]]></description>
            <guid isPermaLink="false">php.ini常用配置</guid>
        </item>
        <item>
            <title><![CDATA[Taro中引入vant-weapp]]></title>
            <description><![CDATA[<link rel="stylesheet" href="https://www.cuiwei.net/static/css/github-markdown.min.css?t=20260430" type="text/css" media="screen" /><div class="markdown-body"><p>Taro中引入Vant Weapp，不能直接通过第三方NPM包的形式直接调用。需下载资源到本地</p>
<p>需如下几步：</p>
<h2>下载vant-weapp文件</h2>
<pre><code>1.在/src/components下新建文件夹vant-weapp

2.在github上找到vant-weapp下载文件包，将对应的dist文件夹下内容复制到新建的vant-weapp文件夹下。</code></pre>
<h2>配置 copy 小程序原生文件</h2>
<p>vant 组件中包含一些小程序原生文件的依赖，目前 Taro 没有对这些依赖进行分析。因此需要配置 copy 把这些依赖移动到 dist 目录中，例如需要 copy wxs 和样式文件，这里简单粗暴的copy整个目录，配置如下</p>
<pre><code>// config/index.js
export default {
  ...
  copy:{
    patterns:[
      {from:'src/components/vant-weapp', to:'dist/components/vant-weapp'}
    ],
  },

}
</code></pre>
<h2>配置忽略 vant 的样式尺寸转换</h2>
<p>如果直接使用 vant 组件，会发现样式偏小的情况，这是因为 Taro 默认将 vant 的样式尺寸从 px 转换为了 rpx，可以通过配置 selectorblacklist 来禁止转换。</p>
<pre><code>// config/index.js
const config = {
  mini: {
    postcss: {
      pxtransform: {
        enable: true,
        config: {
          selectorBlackList: [/van-/]
        }
      },
    }
  },
}
</code></pre>
<h2>引用组件</h2>
<h3>全局</h3>
<p>vi src/app.config.js</p>
<pre><code>  usingComponents: {
    "van-button": "@vant/button/index",
  }
//上面的@vant是定义的别名，如下
// config/index.js
const path = require('path')
export default {
  alias: {
    '@vant': path.resolve(__dirname, '../src/components/vant-weapp')
  },

}</code></pre>
<h3>局部</h3>
<p>vi src/pages/index.config.js</p>
<pre><code>export default {
  navigationBarTitleText: '首页',
  usingComponents: {
    'van-button': '@vant/button/index'
  }
}</code></pre>
<h2>使用</h2>
<pre><code>&lt;template&gt;
  &lt;view&gt;
    &lt;van-button type='primary' :loading='true' loading-text='ing'&gt;Button&lt;/van-button&gt;
  &lt;/view&gt;
&lt;/template&gt;

&lt;script&gt;
export default {
  name: 'index'
}
&lt;/script&gt;</code></pre></div>]]></description>
            <guid isPermaLink="false">Taro中引入vant-weapp</guid>
        </item>
        <item>
            <title><![CDATA[nginx 编译安装]]></title>
            <description><![CDATA[<link rel="stylesheet" href="https://www.cuiwei.net/static/css/github-markdown.min.css?t=20260430" type="text/css" media="screen" /><div class="markdown-body"><p>下面以CentOS为例</p>
<h1>编译安装</h1>
<p>下载</p>
<pre><code>wget https://ftp.pcre.org/pub/pcre/pcre-8.44.tar.gz
wget http://nginx.org/download/nginx-1.20.0.tar.gz</code></pre>
<p>编译</p>
<pre><code># 创建所属用户和组，不创建家目录，不能ssh登录
useradd -M -s /sbin/nologin www

./configure --user=www --group=www --prefix=/data/apps/nginx --conf-path=/vagrant/apps/nginx/conf/nginx.conf --pid-path=/data/apps/nginx/logs/nginx.pid --with-http_stub_status_module --with-http_ssl_module --with-pcre=/vagrant/tgz/pcre-8.44 --with-http_realip_module --with-http_image_filter_module

make &amp;&amp; make install</code></pre>
<p>启动脚本</p>
<pre><code>vi /etc/init.d/nginx #详见附件1️⃣
#给执行权限
chmod +x /etc/rc.d/init.d/nginx</code></pre>
<p>开机启动</p>
<blockquote>
<p>chkconfig --level 345 nginx on</p>
</blockquote>
<p>使用</p>
<pre><code>service nginx start
service nginx reload
service nginx stop
service nginx restart</code></pre>
<h1>yum安装</h1>
<p>这个就没太多说的了，按照官方文档配置好yum源就行</p>
<blockquote>
<p><a href="http://nginx.org/en/linux_packages.html#RHEL-CentOS">http://nginx.org/en/linux_packages.html#RHEL-CentOS</a></p>
</blockquote>
<h1>附件</h1>
<p>1️⃣nginx启动脚本</p>
<pre><code>#!/bin/sh
#
# nginx - this script starts and stops the nginx daemin
#
# chkconfig:   - 85 15
# description:  Nginx is an HTTP(S) server, HTTP(S) reverse \
#               proxy and IMAP/POP3 proxy server
# processname: nginx
# config:      /data/apps/nginx/conf/nginx.conf
# pidfile:     /data/apps/nginx/logs/nginx.pid

# Source function library.
. /etc/rc.d/init.d/functions

# Source networking configuration.
. /etc/sysconfig/network

# Check that networking is up.
[ "$NETWORKING" = "no" ] &amp;&amp; exit 0

nginx="/data/apps/nginx/sbin/nginx"
prog=$(basename $nginx)

NGINX_CONF_FILE="/data/apps/nginx/conf/nginx.conf"

lockfile=/var/lock/subsys/nginx

start() {
    [ -x $nginx ] || exit 5
    [ -f $NGINX_CONF_FILE ] || exit 6
    echo -n $"Starting $prog: "
    daemon $nginx -c $NGINX_CONF_FILE
    retval=$?
    echo
    [ $retval -eq 0 ] &amp;&amp; touch $lockfile
    return $retval
}

stop() {
    echo -n $"Stopping $prog: "
    killproc $prog -QUIT
    retval=$?
    echo
    [ $retval -eq 0 ] &amp;&amp; rm -f $lockfile
    return $retval
}

restart() {
    configtest || return $?
    stop
    start
}

reload() {
    configtest || return $?
    echo -n $"Reloading $prog: "
    killproc $nginx -HUP
    RETVAL=$?
    echo
}

force_reload() {
    restart
}

configtest() {
  $nginx -t -c $NGINX_CONF_FILE
}

rh_status() {
    status $prog
}

rh_status_q() {
    rh_status &gt;/dev/null 2&gt;&amp;1
}

case "$1" in
    start)
        rh_status_q &amp;&amp; exit 0
        $1
        ;;
    stop)
        rh_status_q || exit 0
        $1
        ;;
    restart|configtest)
        $1
        ;;
    reload)
        rh_status_q || exit 7
        $1
        ;;
    force-reload)
        force_reload
        ;;
    status)
        rh_status
        ;;
    condrestart|try-restart)
        rh_status_q || exit 0
            ;;
    *)
        echo $"Usage: $0 {start|stop|status|restart|condrestart|try-restart|reload|force-reload|configtest}"
        exit 2
esac</code></pre></div>]]></description>
            <guid isPermaLink="false">nginx 编译安装</guid>
        </item>
        <item>
            <title><![CDATA[代理IP的使用]]></title>
            <description><![CDATA[<link rel="stylesheet" href="https://www.cuiwei.net/static/css/github-markdown.min.css?t=20260430" type="text/css" media="screen" /><div class="markdown-body"><h1>curl</h1>
<pre><code>curl http://www.icanhazip.com/
curl -x 127.0.0.1:3128 http://www.icanhazip.com/</code></pre>
<h1>socks5</h1>
<pre><code>curl --socks5 127.0.0.1:3129 http://www.icanhazip.com/</code></pre>
<h1>php</h1>
<pre><code>define(URL, 'http://www.icanhazip.com/');
define(PROXY, '127.0.0.1');
define(PORT, 3128);

$curl = curl_init();
curl_setopt($curl, CURLOPT_URL, URL);
curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, false);
curl_setopt($curl, CURLOPT_USERAGENT, $_SERVER['HTTP_USER_AGENT']);
curl_setopt($curl, CURLOPT_FOLLOWLOCATION, 1);
curl_setopt($curl, CURLOPT_AUTOREFERER, 1);
curl_setopt($curl, CURLOPT_PROXY, PROXY);
curl_setopt($curl, CURLOPT_PROXYPORT, PORT);
//curl_setopt($curl, CURLOPT_PROXYUSERPWD, "代理用户名:代理密码");

curl_setopt($curl, CURLOPT_TIMEOUT, 30);
curl_setopt($curl, CURLOPT_HEADER, 0);
curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
$tmpInfo = curl_exec($curl);
curl_close($curl);
echo $tmpInfo;</code></pre></div>]]></description>
            <guid isPermaLink="false">代理IP的使用</guid>
        </item>
        <item>
            <title><![CDATA[nvm nodejs版本管理工具]]></title>
            <description><![CDATA[<link rel="stylesheet" href="https://www.cuiwei.net/static/css/github-markdown.min.css?t=20260430" type="text/css" media="screen" /><div class="markdown-body"><p>可以很方便地切换 node 版本</p>
<h2>安装</h2>
<h3>macOS or Linux</h3>
<pre><code>#在线安装
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.38.0/install.sh | bash

#下载到本地再安装
cuiwei@weideMacBook-Pro nvm-master % sh install.sh 
=&gt; Downloading nvm from git to '/Users/cuiwei/.nvm'
=&gt; Cloning into '/Users/cuiwei/.nvm'...
remote: Enumerating objects: 345, done.
remote: Counting objects: 100% (345/345), done.
remote: Compressing objects: 100% (292/292), done.
remote: Total 345 (delta 38), reused 170 (delta 28), pack-reused 0
Receiving objects: 100% (345/345), 194.77 KiB | 280.00 KiB/s, done.
Resolving deltas: 100% (38/38), done.
* (HEAD detached at FETCH_HEAD)
  master
=&gt; Compressing and cleaning up git repository

=&gt; Profile not found. Tried ~/.bashrc, ~/.bash_profile, ~/.zshrc, and ~/.profile.
=&gt; Create one of them and run this script again
   OR
=&gt; Append the following lines to the correct file yourself:

export NVM_DIR="$HOME/.nvm"
[ -s "$NVM_DIR/nvm.sh" ] &amp;&amp; \. "$NVM_DIR/nvm.sh"  # This loads nvm

=&gt; Close and reopen your terminal to start using nvm or run the following to use it now:

export NVM_DIR="$HOME/.nvm"
[ -s "$NVM_DIR/nvm.sh" ] &amp;&amp; \. "$NVM_DIR/nvm.sh"  # This loads nvm</code></pre>
<p>导入环境变量</p>
<pre><code>echo 'export NVM_DIR="$HOME/.nvm"
[ -s "$NVM_DIR/nvm.sh" ] &amp;&amp; \. "$NVM_DIR/nvm.sh"  # This loads nvm' &gt;&gt;~/.zshrc</code></pre>
<p>使生效</p>
<pre><code>source ~/.zshrc</code></pre>
<h3>Windows</h3>
<p><a href="https://github.com/coreybutler/nvm-windows">https://github.com/coreybutler/nvm-windows</a></p>
<h2>常用命令</h2>
<pre><code>nvm install stable ⬅️安装最新稳定版 node（当前最新稳定版11.6.0）
nvm install &lt;version&gt; ⬅️安装指定版本 (install v10.15.0或install 10.15.0)
nvm uninstall &lt;version&gt; ⬅️卸载指定版本node,（如果删除的为当前使用版本，要解绑，则执行 nvm deactivate）
nvm use &lt;version&gt; ⬅️切换使用指定的版本node
nvm current ⬅️显示当前使用的版本
nvm ls ⬅️列出所有安装的版本
nvm ls available ⬅️列出官网上node的所有版本
nvm alias &lt;name&gt; &lt;version&gt; ⬅️给不同的版本号添加别名
nvm unalias &lt;name&gt; ⬅️删除已定义的别名
nvm alias default &lt;version&gt; ⬅️指定默认版本（设定后需要打开新的终端才生效）
nvm deactivate ⬅️解除当前版本绑定</code></pre>
<h2>.nvmrc配置文件</h2>
<p>记录当前项目使用的node.js版本</p>
<p>有了.nvmrc文件后，我们在终端没有指定版本时执行 <code>nvm use</code>, <code>nvm install</code>, <code>nvm exec</code>, <code>nvm run</code>, 和 <code>nvm which</code> 命令时会使用 <code>.nvmrc</code>文件指定的版本。</p>
<p>创建 .nvmrc 文件</p>
<pre><code>$ echo "18.12" &gt; .nvmrc
# 设置最新LTS版本
$ echo "lts/*" &gt; .nvmrc 
# 设置最新版本
$ echo "node" &gt; .nvmrc </code></pre>
<h2>卸载</h2>
<pre><code>Note:
  to remove, delete, or uninstall nvm - just remove the `$NVM_DIR` folder (usually `~/.nvm`)</code></pre></div>]]></description>
            <guid isPermaLink="false">nvm nodejs版本管理工具</guid>
        </item>
        <item>
            <title><![CDATA[CentOS服务器初始化配置]]></title>
            <description><![CDATA[<link rel="stylesheet" href="https://www.cuiwei.net/static/css/github-markdown.min.css?t=20260430" type="text/css" media="screen" /><div class="markdown-body"><p>这里以CentOS 7为例</p>
<p>selinux</p>
<pre><code>vi /etc/sysconfig/selinux
# SELINUX=enforcing
SELINUX=disabled</code></pre>
<p>修改时区</p>
<pre><code>1 date #查看时间是否正确，不正确则执行以下步骤
2 rm -rf /etc/localtime
3 ln -s /usr/share/zoneinfo/Asia/Shanghai /etc/localtime
4 设置时区
    tzselect
5 同步时间
    ntpdate cn.pool.ntp.org
6 date</code></pre>
<p>把主分区改为/data（可选）</p>
<pre><code>1 mkdir /data
2 vi /etc/fstab
/dev/mapper/VolGroup-lv_home /data                   ext4    defaults        1 2
3 mount -a
4 init 6</code></pre>
<p>设置yum源：epel源</p>
<pre><code>YUM
yum install epel-release
或者手动
rpm -ivh http://dl.fedoraproject.org/pub/epel/epel-release-latest-7.noarch.rpm

#更新下缓存
yum clean all &amp;&amp; yum makecache</code></pre>
<p>开启ssh登陆</p>
<blockquote>
<p><a href="https://www.cuiwei.net/p/1140462602">CentOS服务器开启SSH远程登录</a></p>
</blockquote>
<p>SCP（用于文件传输）</p>
<pre><code>yum install openssh-clients</code></pre>
<p>禁用防火墙firewalld（如果是正式环境，不建议禁用，建议设置对应的规则）</p>
<pre><code>systemctl stop firewalld.service
systemctl disable firewalld.service
firewall-cmd --state</code></pre></div>]]></description>
            <guid isPermaLink="false">CentOS服务器初始化配置</guid>
        </item>
        <item>
            <title><![CDATA[vagrant + virtualbox搭建一个可移动的开发环境]]></title>
            <description><![CDATA[<link rel="stylesheet" href="https://www.cuiwei.net/static/css/github-markdown.min.css?t=20260430" type="text/css" media="screen" /><div class="markdown-body"><h2>前言</h2>
<p>在日常开发中可能会遇到这样的问题</p>
<ul>
<li>新入职第一天不是在熟悉公司项目，而是在安装开发环境（搭建开发环境耗时）</li>
<li>在本地开发完一个功能，测试没问题，而部署到服务器上就跑不起来了（环境不一致）</li>
<li>在调试某个功能时发现自己这边跑不通，而同事那边没问题（环境不一致）</li>
</ul>
<p>vagrant+virtualbox 的出现，成功的解决了搭建开发环境耗时且不一致的问题</p>
<ul>
<li>vagrant box镜像同时支持Windows、Mac和Linux</li>
<li>一次安装，快速分发
可以给新入职的同事分分钟部署一个和大家一样的开发环境</li>
<li>使用简单
<pre><code>vagrant up ⬅️启动虚拟机
vagrant ssh ⬅️登陆虚拟机
vagrant reload ⬅️重载虚拟机，Vagrantfile文件有修改了才需要
vagrant halt ⬅️关闭虚拟机</code></pre>
<h2>安装</h2>
<pre><code>#下载box
http://www.vagrantbox.es
https://github.com/holms/vagrant-centos7-box/releases/download/7.1.1503.001/CentOS-7.1.1503-x86_64-netboot.box
#进入项目目录(虚拟机启动后系统自动挂载该目录到/vagrant)
cd PhpstormProjects
#添加本地box
vagrant box add {title} ../vagrant_package/CentOS-7.1.1503-x86_64-netboot.box
#初始化(在项目目录生成Vagrantfile文件1️⃣
vagrant init {title}
#启动
vagrant up
#连接
vagrant ssh
#登陆虚拟机后就可以安装自己需要的软件了，和普通服务器操作一致
#搭建一个和服务器一致的开发环境</code></pre>
<h2>备份</h2>
<pre><code>#进入项目目录
cd PhpstormProjects
#打包（会在当前目录生成一个package.box，根据个人需要把它移动到合适目录）
vagrant package</code></pre>
<h2>恢复备份</h2>
<pre><code>#box列表，查看已有的box
vagrant box list
#移除名称为php的box（box移除后，还需要手动删除virtualBox中的虚拟机）
vagrant box remove php
#恢复备份过的box
vagrant box add php ../vagrant_package/package-php.box
#启动
vagrant up</code></pre>
<h2>附件</h2>
<p>1️⃣Vagrantfile文件实例</p></li>
</ul>
<pre><code>#vim: set ft=ruby ts=2 :

Vagrant.configure("2") do |config|
   config.vm.box = "php"
   #config.vm.network "forwarded_port", guest: 80, host: 8090
   config.vm.network "public_network",  ip:"192.168.1.88"

   config.vm.synced_folder "./", "/vagrant"
   config.vm.provider "virtualbox" do |v|
      v.memory = 2048
      v.cpus = 2
   end

   config.vm.provision "shell", run:"always", inline: &lt;&lt;-SHELL
   service nginx start
   SHELL
end</code></pre></div>]]></description>
            <guid isPermaLink="false">vagrant + virtualbox搭建一个可移动的开发环境</guid>
        </item>
        <item>
            <title><![CDATA[php扩展的编译安装]]></title>
            <description><![CDATA[<link rel="stylesheet" href="https://www.cuiwei.net/static/css/github-markdown.min.css?t=20260430" type="text/css" media="screen" /><div class="markdown-body"><p>这里以redis为例
工具</p>
<pre><code>yum install autoconf</code></pre>
<p>下载及编译</p>
<pre><code>wget https://pecl.php.net/get/redis-5.3.4.tgz --no-check-certificate
tar -xvzf redis-5.3.4.tgz 
cd redis-5.3.4
phpize 
./configure --with-php-config=/usr/bin/php-config
make &amp;&amp; make install</code></pre>
<p>添加到php.ini</p>
<pre><code>vi php.ini
    959 extension=redis</code></pre>
<p>重载php-fpm，使生效</p>
<pre><code>service php-fpm reload</code></pre></div>]]></description>
            <guid isPermaLink="false">php扩展的编译安装</guid>
        </item>
        <item>
            <title><![CDATA[CentOS 环境编译安装php8.0]]></title>
            <description><![CDATA[<link rel="stylesheet" href="https://www.cuiwei.net/static/css/github-markdown.min.css?t=20260430" type="text/css" media="screen" /><div class="markdown-body"><p>如果是新服务器，建议先看下这篇文章：<a href="https://www.cuiwei.net/p/1585590655">CentOS服务器初始化配置</a></p>
<p>php8 编译安装和其他低版本基本一致</p>
<h1>准备</h1>
<pre><code>#编译工具
yum -y install gcc gcc-c++ make
#依赖
yum -y install zlib-devel libxml2-devel openssl openssl-devel gd-devel libmcrypt-devel libcurl-devel libicu-devel oniguruma-devel</code></pre>
<h1>编译</h1>
<pre><code>cd php-8.0.3

./configure --prefix=/data/apps/php --with-config-file-path=/data/apps/php/etc --with-mysqli=mysqlnd --with-pdo-mysql=mysqlnd --with-iconv-dir --with-freetype-dir --with-jpeg-dir --with-png-dir --with-libxml-dir --enable-xml --with-xpm-dir --disable-rpath --enable-bcmath --enable-shmop --enable-sysvsem --enable-inline-optimization --with-curl --enable-mbregex --enable-mbstring --with-mcrypt --with-gd --enable-gd-native-ttf --with-openssl --with-mhash --enable-pcntl --enable-sockets --with-ldap-sasl --with-xmlrpc --enable-zip --enable-soap --with-pear --with-zlib --enable-fpm --enable-intl

make &amp;&amp; make install

cp php.ini-production /data/apps/php/etc/php.ini
#简单编辑一下
vi php.ini
    971 [Date]
    972 ; Defines the default timezone used by the date functions
    973 ; http://php.net/date.timezone
    974 date.timezone = PRC
</code></pre>
<h1>php-fpm</h1>
<pre><code>cd /data/apps/php/etc/
mv php-fpm.conf.default php-fpm.conf
cd php-fpm.d/
mv www.conf.default www.conf
vi www.conf
     20 ; Unix user/group of processes
     21 ; Note: The user is mandatory. If the group is not set, the default user's group
     22 ;       will be used.
     23 user = vagrant
     24 group = vagrant

#设置php-fpm开机自启
#复制启动脚本
cp /vagrant/tgz/php-8.0.3/sapi/fpm/init.d.php-fpm  /etc/init.d/php-fpm
#给可执行权限
chmod +x /etc/init.d/php-fpm
#开机自启
chkconfig --level 345 php-fpm on

#常用命令
service php-fpm start ⬅️启动
service php-fpm reload ⬅️重载
service php-fpm restart ⬅️重启
service php-fpm stop ⬅️关闭
</code></pre>
<p>为了方便使用php相关命令，做个软链</p>
<pre><code>ln -s /data/apps/php/bin/* /usr/bin/</code></pre></div>]]></description>
            <guid isPermaLink="false">CentOS 环境编译安装php8.0</guid>
        </item>
        <item>
            <title><![CDATA[mariadb(mysql) 安装与使用，备份及恢复]]></title>
            <description><![CDATA[<link rel="stylesheet" href="https://www.cuiwei.net/static/css/github-markdown.min.css?t=20260430" type="text/css" media="screen" /><div class="markdown-body"><h1>为什么选择mariadb</h1>
<p>前提是开发环境。mariadb体积小，安装方便，兼容常用mysql指令</p>
<h1>安装</h1>
<pre><code>#Server version: 5.5.68-MariaDB MariaDB Server
yum install mariadb-server</code></pre>
<p>开机自启</p>
<pre><code>systemctl enable mariadb </code></pre>
<p>常用命令</p>
<pre><code>systemctl start mariadb  ️启动MariaDB
systemctl stop mariadb  ️停止MariaDB
systemctl restart mariadb  ️重启MariaDB</code></pre>
<p>开发环境设置root账号以任意ip，空密码登陆</p>
<pre><code>#将Host设置为%,表示任意ip
update user set Host='%' where Host='localhost';
#如下，保留一条即可
MariaDB [mysql]&gt; select Host, User, Password from user;
+------+------+----------+
| Host | User | Password |
+------+------+----------+
| %    | root |          |
+------+------+----------+
1 row in set (0.00 sec)

#重载权限
FLUSH PRIVILEGES;</code></pre>
<h1>备份</h1>
<p>下面提供一个备份所有数据库的脚本</p>
<pre><code>dir='/vagrant/apps/mysql/backup/'`date +%Y%m%d`;
filename='all_mysql'`date +%Y%m%d%H%M`'.sql.gz';
mkdir $dir
cd $dir
# 把所有数据库名导出到 databases.txt，排除掉mysql, information_schema, performance_schema
mysql -e "show databases;" -uroot | grep -Ev "Database|mysql|information_schema|performance_schema" &gt; databases.txt
# 把所有数据库导出到一个.sql.gz文件，排除掉mysql, information_schema, performance_schema
mysql -e "show databases;" -uroot | grep -Ev "Database|mysql|information_schema|performance_schema" | xargs mysqldump --skip-lock-tables -uroot --databases | gzip&gt; $filename</code></pre>
<h1>恢复</h1>
<pre><code># 解压
gzip -d all_mysql201912020333.sql.gz
# 导入
mysql -uroot &lt; all_mysql201912020333.sql.gz</code></pre></div>]]></description>
            <guid isPermaLink="false">mariadb(mysql) 安装与使用，备份及恢复</guid>
        </item>
        <item>
            <title><![CDATA[CentOS服务器开启SSH远程登录]]></title>
            <description><![CDATA[<link rel="stylesheet" href="https://www.cuiwei.net/static/css/github-markdown.min.css?t=20260430" type="text/css" media="screen" /><div class="markdown-body"><p>服务器默认是用户名+密码登陆，通常为了安全我们会改为SSH登陆</p>
<p>假如我们需要把root账号改为ssh登陆，如下</p>
<h2>客户端（如 本机</h2>
<pre><code>#生成 public key
ssh-keygen -t rsa -C "cw@localhost"
#...连连回车...
#查看 public key
cat ~/.ssh/id_rsa.pub</code></pre>
<h2>服务端（如 服务器</h2>
<pre><code>cd ~
#使其自动创建.ssh目录
ssh-keygen -t rsa -C "root@localhost"
echo "客户端用户的public key" &gt;&gt;authorized_keys
chmod 600 authorized_keys

vi /etc/ssh/sshd_config
17 Port 1018    #改掉22端口号，注意防火墙要开放此端口
RSAAuthentication yes #CentOS7.4以后就废除了，无需添加
48 PubkeyAuthentication yes
52 AuthorizedKeysFile      .ssh/authorized_keys
148 PasswordAuthentication no  #禁止使用密码登录

#重载sshd服务
systemctl reload sshd</code></pre>
<h2>验证</h2>
<pre><code>#在本机通过终端（mac）或putty（Windows）执行
ssh root@ip123</code></pre></div>]]></description>
            <guid isPermaLink="false">CentOS服务器开启SSH远程登录</guid>
        </item>
    </channel>
</rss>