<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Yuzi&apos;s Space</title><description>A Space that include my Life and Work</description><link>https://yuzi.dev/</link><language>zh_CN</language><item><title>使用Astro &amp; fuwari，和codex一起重构了博客</title><link>https://yuzi.dev/posts/frontend/refactoring-my-blog-with-astro-fuwari-and-codex/</link><guid isPermaLink="true">https://yuzi.dev/posts/frontend/refactoring-my-blog-with-astro-fuwari-and-codex/</guid><description>记录一次把个人博客从 Shiro 迁移到 Astro + Fuwari，并顺手完成文章迁移、URL 重构、GitHub 卡片、本地图片、404 页面和多 Banner 配置的过程。</description><pubDate>Tue, 14 Apr 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;这两天把博客重构了一遍，从原来的 Shiro 迁移到了 Astro + Fuwari。&lt;/p&gt;
&lt;p&gt;起因其实很简单。Shiro 已经很久没有维护了，而且它带着后端，实现上相对更重一些。偏偏这段时间我的后端服务器又迁移到了一个网络不太好的机房，性能问题一下就更明显了，打开慢、响应也不稳定。博客这种东西，我并不想长期盯着它的后端状态，所以最后还是决定改回纯前端、纯静态页面。&lt;/p&gt;
&lt;p&gt;至于为什么选 Astro，一方面是因为它现在已经足够成熟，另一方面也是因为它相比 Hexo 这种更老一代的方案，确实更现代一些。内容组织、组件化能力、构建速度这些方面都更舒服，拿来做博客很合适。主题最后选的是 Fuwari，样式比较顺眼，结构也足够清晰，改起来不算费劲。&lt;/p&gt;
&lt;p&gt;一开始我只是想把文章搬过来，结果真正开始迁移之后，才发现事情远不只是“复制粘贴”这么简单。&lt;/p&gt;
&lt;p&gt;旧文章的 frontmatter 并不统一，图片来源也很杂：有本地图片，有外链，有图床，有些文章还是单文件，有些更适合整理成 &lt;code&gt;index.md + 同目录图片&lt;/code&gt; 的结构。再往前翻，还能看到不少 Hexo 时代留下来的痕迹，比如摘要、标签、分类、链接结构都不太一致。&lt;/p&gt;
&lt;p&gt;于是第一步就变成了整理内容本身。Shiro 那边的文章一篇篇迁过来，补发布时间、补简介、定分类、定标签，再把 URL slug 单独抽出来。带图片的文章尽量改成文章目录结构，外链图片能下载的就下到本地，避免以后图床失效。&lt;/p&gt;
&lt;p&gt;这一步做完之后，文章本身就整齐了不少：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;frontmatter 统一了&lt;/li&gt;
&lt;li&gt;带图片的文章尽量都变成了同目录资源&lt;/li&gt;
&lt;li&gt;手记类内容统一归到 &lt;code&gt;随笔&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;一些重复标签也顺手做了合并&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;内容整理完之后，下一步就是路由。&lt;/p&gt;
&lt;p&gt;原来那套直接用中文标题做 slug 的方式虽然能用，但看上去总归不够利落。后来把文章 URL 改成了：&lt;/p&gt;
&lt;p&gt;&lt;code&gt;/posts/分类英文名/文章英文名&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;比如：&lt;/p&gt;
&lt;p&gt;&lt;code&gt;/posts/notes/first-time-keeping-a-car&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;为了避免每篇文章都维护两份分类字段，分类英文名没有写进 frontmatter，而是在代码里统一维护一张映射表；文章本身只保留 &lt;code&gt;urlSlug&lt;/code&gt;。这样改完之后，文章页、归档页、分页、上一篇下一篇这些地方都要跟着一起调整，RSS 和 sitemap 也要一起检查，最后顺手把尾部的 &lt;code&gt;/&lt;/code&gt; 也去掉了。&lt;/p&gt;
&lt;p&gt;除了路由，我还顺便把主题上不少细节也一起改了。&lt;/p&gt;
&lt;p&gt;比如：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;修了 Markdown 代码块文字居中的问题&lt;/li&gt;
&lt;li&gt;把 GitHub 仓库卡片从前端实时请求改成了构建时拉取缓存&lt;/li&gt;
&lt;li&gt;增加了自定义 404 页面&lt;/li&gt;
&lt;li&gt;给 &lt;code&gt;/archive&lt;/code&gt; 页补了一块摘要区域&lt;/li&gt;
&lt;li&gt;加了友链模块&lt;/li&gt;
&lt;li&gt;调整了全局字体和导航栏标题字体&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;banner 这一块改得尤其多。本来只是想调一下移动端位置，后来干脆扩成了多张 banner 随机显示。现在每一张图都可以单独配置：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;src&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;position&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;mobileHeight&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;mobileOffset&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;credit&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;右上角那块版权信息也顺手改了。现在桌面端默认显示作者，鼠标移上去后切换成图片描述和外链图标；移动端则改成第一次点击展开，第二次点击跳转。&lt;/p&gt;
&lt;p&gt;这次还有个比较不一样的地方，就是整个过程里我一直在和 &lt;code&gt;codex&lt;/code&gt; 一起做这件事。&lt;/p&gt;
&lt;p&gt;它最适合干的其实不是“替我决定博客该做成什么样”，而是那些很碎、很多、但又必须处理干净的工作。比如批量迁移文章、补 frontmatter、整理标签、扫描旧链接、同步改多个相关文件、把外链图片下载到本地之类的事情，交给它去做，速度确实快很多。&lt;/p&gt;
&lt;p&gt;当然也不是说一句话丢过去就可以完全不管了。像一些交互细节、文案、样式取舍，还是得自己盯着，很多地方也是来来回回改了好几轮才定下来。&lt;/p&gt;
&lt;p&gt;最后这一轮改下来，博客的结构基本都重新梳理了一遍：文章迁移了，链接规则统一了，静态资源更完整了，原来依赖后端的部分也尽量去掉了。之后维护起来应该会轻松很多。&lt;/p&gt;
&lt;p&gt;顺带一提，这篇文章也是Codex辅助我写的呢。&lt;/p&gt;
</content:encoded></item><item><title>第一次养车，困难重重</title><link>https://yuzi.dev/posts/notes/first-time-keeping-a-car/</link><guid isPermaLink="true">https://yuzi.dev/posts/notes/first-time-keeping-a-car/</guid><description>记录第一次真正接手一台车后的长途驾驶、保养、停车和通勤体验，以及对四轮与电动车出行价值的重新判断。</description><pubDate>Tue, 15 Oct 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;在国庆之前，我开车的经历仅限于在老家开我爸的车，老家仅有的公共交通在私家车面前速度完败，所以我一直觉得开车出行是比地铁公交舒服多的事情，直到……&lt;/p&gt;
&lt;h1&gt;第一次超长途&lt;/h1&gt;
&lt;p&gt;因为我爸现在也很少开车了，所以我想找个机会把车开来我工作的城市——武汉，虽然目前没有上班开车通勤的必要（走路15分钟），但是周末出个游还是不错的。于是我盘算，国庆节前回家，10.1当前开车载着父母和外婆来武汉旅游7天，然后送他们乘坐飞机回家，我就可以接手我爸的车了。&lt;/p&gt;
&lt;p&gt;这一趟还是很惊人的，全程800公里，我的目标是我一个人开完。我们从10.1早上8点左右出发，起初，我还是很有冲劲的，一直在保持高速积极超车，我妈非常的担心，觉得我开120都太快了，100就行了，然而我还是在狠狠的冲刺。没过多久我就发现了区间测速这个可恶的东西，它的区间，不是间隔的一段一段，而是连续的！！！走完一个区间又进入了下一个区间！！！这导致我还必须时不时压着速度走。&lt;/p&gt;
&lt;p&gt;我家的车是12年的新福克斯，除了倒车雷达和上坡辅助外，没有任何的辅助驾驶功能！是的，连定速巡航都没有！！所以我的右脚就需要不停地在两个踏板之间不断地移动，所以防止超速是一件很痛苦的事。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./kaizhou-service-area.webp&quot; alt=&quot;群山之间的开州服务区&quot; /&gt;&lt;/p&gt;
&lt;p&gt;万州这一片的山真是一座比一座高，父母在车上也在不住地感叹。虽然出行当天是十一，但路上并不是很堵，唯一一次堵车是因为事故，也只堵了十多分钟就恢复了。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./wushan-service-area.webp&quot; alt=&quot;巫山服务区&quot; /&gt;&lt;/p&gt;
&lt;p&gt;但逐渐接近武汉，车变得多了起来，地势也变得平坦了起来，限速也逐渐由100提到110最后来到120。人也开始疲惫起来，抓着方向盘太久，导致手都抬酸了，也不得不抓方向盘下半部分，甚至单手开一会。下了高速，发现离家还有40多公里，还要在三环上慢悠悠的开大半天，这时候才意识到武汉这座大城市的威力。最终，我们还是有惊无险的到达了目的地。&lt;/p&gt;
&lt;p&gt;成本算下来，我们加油都花了300多，国庆期间免了过路费，如果加起来也就是700多，接近成本1元/公里，4个人也就是0.25/公里，和动车差不多吧，当然还要算上开车的精力的话，开长途确实不是一个多么好的主意。&lt;/p&gt;
&lt;h1&gt;养车还是贵&lt;/h1&gt;
&lt;p&gt;这就来到了我司的主要业务了，咱们途虎就是干这行的，所以我9月底一回到家，就准备把车拿去虎子保养一番【工资回收计划{大嘘}】。在我家不远处正好还开了一家新店。&lt;/p&gt;
&lt;p&gt;检查一番，车子并没有什么大问题，但是前轮磨损得有些严重，建议更换。想着这次要开这么远，为了确保万无一失，就选择了更换一下，一条380多。再加上换个机油，这一次保养就花了我1.1k！&lt;/p&gt;
&lt;p&gt;停车又是一笔钱，一问小区物业才知道，小区因为没有地下停车场，车位紧张，所以只有户主才能办理车位！【甚至还要查行驶证上的名字是不是户主的】。没办法，只能把车停公司，还好公司还能给咱们员工提供优惠价车位【感动】，预缴了几个月，接近1k又花出去了，心痛中……&lt;/p&gt;
&lt;h1&gt;光谷的地狱交通&lt;/h1&gt;
&lt;p&gt;最可怕的，还是光谷的交通，这里让我【和我同事】直接改变了出行的心态。请看图，这是一个周五的晚高峰，前往光谷核心区的路线：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./guanggu-traffic.jpg&quot; alt=&quot;4.9公里，52分钟！！！！&quot; /&gt;&lt;/p&gt;
&lt;p&gt;这开车还没有走路快的原因，就是车多和人多。一出园区门，排队到主路的500米就要排13分钟，旁边一个小学放学，学生们拥挤在公交车站，直接占用了本就拥挤的路的两条道。再加上某些大路口的红绿灯设置地不合理【说的就是软件园路口！！！】极大的削减了开车出行的幸福度。&lt;/p&gt;
&lt;p&gt;说到底，还是因为光谷的地铁缺少，明明作为高新区，却只有一条2号线从武汉东站走过，光谷核心区只有有轨电车，但这玩意非高架段也慢啊！！！关山大道的有轨电车真应该改成高架重修！！！&lt;/p&gt;
&lt;h1&gt;电驴，启动！！打不过就加入&lt;/h1&gt;
&lt;p&gt;光谷的电驴也是非常的多，而四轮车在规规矩矩的等红绿灯、堵车时，他们却随意穿行，甚至直接占用机动车道行驶，真让我恨得牙痒痒。不过，对于短途出行，这玩意看着还真挺不错的，所以我决定搞台二手试试。&lt;/p&gt;
&lt;p&gt;就这样，在闲鱼上看上了一个个人卖家的九号电动车后，我和同事小鹏便开着车走了20多公里来到汉阳和他们进行线下交易。经过简单的试驾后，就敲定把它买下来了。但如何把它运回来又成了一个难题，我们本想的是把它塞进后备箱，但是塞进去了后备箱就关不上了【这下知道掀背车的好了】最后花了好几十找货拉拉把它运过来……&lt;/p&gt;
&lt;p&gt;在骑电动车上班后，它给我带来的幸福感比四轮多多了！！！现在我上班从出门到公司只需要8分钟！骑行时间在4-5分钟左右，真是上班像上厕所一样方便。中午可以选择回家睡觉，发版的晚上可以选择回家呆到20点再回公司发版【没错这篇文章的大部分就是这个时间写的】，午饭和晚饭可以去到800米外的大学美食街品鉴，极大的提高了打工生活的质量【真香】。&lt;/p&gt;
&lt;h1&gt;所以，四轮的意义是？&lt;/h1&gt;
&lt;p&gt;虽然养四轮车和开四轮车是一件如此槽心的事情，那为什么还要养呢？&lt;/p&gt;
&lt;p&gt;我觉得还是有三个方面，四轮车是无可替代的：一个是拓宽生活圈的范围，有了四轮车可以去地铁无法触及的有趣地方，而且可以快速地到达，比如去汉阳交易电动车，如果没有四轮的话可能我只有周末抽时间坐公交摇过去。第二个就是提供一个私密的空间，我可以与车上的乘客在路途上谈天说地，而无需担心说的话被别人听到，这点也是公共交通所不具有的。第三个就是对行李友好，&lt;/p&gt;
&lt;p&gt;相较于目前最新的车与我的车，我最大的需求就是智驾了，要求也不高，能在快速路上自己开，堵车的时候能帮我自动跟车别被插队了，能自动停车就好了。价格在15万以下，其实小鹏Mona03就挺好的，但我又想要混动😢，只能等一手笛子的智驾下放给秦了，或许5年后会有？&lt;/p&gt;
</content:encoded></item><item><title>谨慎使用Number.prototype.toFixed()</title><link>https://yuzi.dev/posts/frontend/be-careful-with-number-prototype-tofixed/</link><guid isPermaLink="true">https://yuzi.dev/posts/frontend/be-careful-with-number-prototype-tofixed/</guid><description>记录一次对 Number.prototype.toFixed() 行为的排查，以及由此引出的四舍五入实现差异。</description><pubDate>Fri, 07 Jun 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;今天在群里，leader 发了一句这样的话：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./example.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;toFixed()&lt;/code&gt; 的 MDN 文档：&lt;/p&gt;
&lt;p&gt;https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Number/toFixed&lt;/p&gt;
&lt;p&gt;但是 MDN 里却也用“四舍五入”来描述 &lt;code&gt;Number.prototype.toFixed()&lt;/code&gt;，但是图里的又确实发生了，询问 GPT 后得知，这是一个常常易被忽略的特性。&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;这是因为 JavaScript 中的浮点数表示和四舍五入规则有时会导致一些意外的结果。具体来说，&lt;code&gt;toFixed()&lt;/code&gt; 方法在处理四舍五入时遵循的是银行家舍入法（Bankers&apos; Rounding），也称为“偶数舍入法”。这种方法的规则是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果舍入的数字正好在两个数之间（例如 1.55 和 2.55），则舍入到最接近的偶数。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;在这个例子中：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;1.55&lt;/code&gt; 四舍五入到最近的偶数是 &lt;code&gt;1.6&lt;/code&gt;。【因为1.6比1.5更接近2（最近的偶数）】&lt;/li&gt;
&lt;li&gt;&lt;code&gt;2.55&lt;/code&gt; 四舍五入到最近的偶数是 &lt;code&gt;2.5&lt;/code&gt;。【因为2.5比2.6更接近2（最近的偶数）】&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这种舍入方法的目的是在大量数据中减少舍入误差的累积。&lt;/p&gt;
&lt;p&gt;如果需要始终按照传统的四舍五入规则（即四舍五入到最近的整数），我们可以使用其他方法来实现。例如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function roundToFixed(num, decimalPlaces) {
    var factor = Math.pow(10, decimalPlaces);
    return (Math.round(num * factor) / factor).toFixed(decimalPlaces);
}

console.log(roundToFixed(1.55, 1)); // 输出 &apos;1.6&apos;
console.log(roundToFixed(2.55, 1)); // 输出 &apos;2.6&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个函数使用 &lt;a href=&quot;https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Math/round&quot;&gt;Math.round()&lt;/a&gt; 来进行传统的四舍五入（它只留整数部分，所以先把小数点提上来，四舍五入后再降回去），然后再将结果格式化为指定的小数位数。这样可以避免银行家舍入法带来的意外结果。&lt;/p&gt;
</content:encoded></item><item><title>毕业季的时光</title><link>https://yuzi.dev/posts/notes/moments-of-graduation-season/</link><guid isPermaLink="true">https://yuzi.dev/posts/notes/moments-of-graduation-season/</guid><description>记录毕业季最后一次回到校园：新线运转、答辩、东莞之行，以及校园生活结束前的复杂心绪。</description><pubDate>Thu, 30 May 2024 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;重回校园&lt;/h2&gt;
&lt;p&gt;24 号晚，一到六点，我就像脱缰的野马般冲出了公司，去赶夕发朝至的 D35 次列车。这应该是我作为学生的倒数第二次回到校园。初入社会，拿着实习工资的我要招架包括房租在内的大批开支，各项开支自然是能缩则缩，所以就只好选择 AD 钙二等座来平衡旅程的花费与舒适度。从武昌到广州东，中间只停长沙一个站，过了长沙后，旁边的位置空了出来，这使得我可以以一个别扭的躺着的姿势躺着睡一夜。&lt;/p&gt;
&lt;p&gt;虽然只离开了广州一个月，但是这潮湿的气候立即让我不适应了，或许是适应了武汉的宜人湿度，在这里我一直感觉很闷。我突然很怀疑我是怎么在这呆了四年的……&lt;/p&gt;
&lt;p&gt;回来当天中午跟杰神去广工吃了个早茶，味道不错，打折下来价格也不贵。为什么我们学校没有？？气人。&lt;img src=&quot;./guanggong-brunch.jpg&quot; alt=&quot;广工的鹿其林酒家&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;新线运转&amp;amp;东莞之行&lt;/h2&gt;
&lt;p&gt;在 26 号，期待已久的广佛南环 + 广惠城际终于要开通了，我便拉上聪神一起去打卡。原本说是去广州南站打卡，但聪神非要去佛山西体验全程。但让我们没想得到是，在从肇庆过来的首发车抵达番禺站之前，番禺站会加开一趟车成为新的首发车。而新线开通的仪式也在番禺站举行，佛山西这边倒是冷冷清清，只有几个车迷起哄让站长把“暂未开通”的贴纸撕下。&lt;img src=&quot;./foshan-west-opening.jpg&quot; alt=&quot;佛山西站，站长正在展示刚揭下的贴纸&quot; /&gt;&lt;/p&gt;
&lt;p&gt;在车上，我还接受了一位来自佛山的记者的采访。起因是她看到了我拿的纪念册，还以为我多拿了【其实是帮聪神拿的，在这之后起码有两个人询问我能不能把我“多拿”的纪念册分给他们，怎么说呢，这很难评】，最后还好是应付过去了。但过了一两天在我高强度的搜索下也没有发现我出现在哪个相关视频里……&lt;/p&gt;
&lt;p&gt;下午坐着成绩到了东莞，与在那里工作的源神见了一面。东莞的市中心鸿福路真不错，有源神的公司 KK 集团旗下的超大的 KKV 和 X11 店，二次元属性拉满了。&lt;img src=&quot;./x11-haikyuu-wall.jpg&quot; alt=&quot;X11 的一墙的排球少年&quot; /&gt;&lt;/p&gt;
&lt;p&gt;另外国贸也非常的大，正门口就有一只大大的奶龙，商场中也到处布满了奶龙的元素，查了一下这只黄黄的龙居然是深圳的一家公司的，之前一直以为源自海外。不过发扬本土 IP 总是好的。&lt;/p&gt;
&lt;p&gt;傍晚经东莞 2 号线、穗深城际、13 号线、7 号线“绕路”回学校，这也是我第一次体验穗深城际，一条仍被国铁运营的城际。其现在的终点新塘南站真的非常的像我在 MC 里建设的 S1 线的冰南站。
&lt;img src=&quot;./xintangnan-platform.jpg&quot; alt=&quot;新塘南站【站台】与新塘站【站房】，同站不同名&quot; /&gt;&lt;img src=&quot;./xintangnan-track-layout.png&quot; alt=&quot;新塘南站站线配置图&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;答辩&lt;/h2&gt;
&lt;p&gt;答辩很倒霉，碰上了一个要求极为严格的老师，流程也巨长，对前面的人的拖沓不加以限制，到我这就老师叫我快点、跳过最后的总结。而点评更为严格，不少的格式问题被挑出，而我们组的指导老师却正好不怎么看格式，这就导致每个人都被点评了半天。&lt;/p&gt;
&lt;p&gt;最后两个老哥更惨，被大批特批内容太少，最后一位更是上台 PPT 都还没打开就被毙了。不过后面听其他老师说，不会有人的论文会被不通过的，因为学院放弃或者留级的人太多了，已经占完了不合格的名额……对此我不想再做过多的评价。其它组都答完了，我们组才答了一半。而其他组都出成绩了，我们组直到现在还没出，这让我真的很无语……&lt;/p&gt;
&lt;h2&gt;结语&lt;/h2&gt;
&lt;p&gt;29 号和杰神去长隆水上乐园浪了一天后，30 号的平静生活将我从前几天的过山车日子中拉了回来。1 号就要离开了，我坐在昨天已经出发去深圳的空荡荡的聪神的书桌上，不时能看见门外有同学拖着箱子离开，我才后知后觉地发现，校园生活真的要结束了。打开宿舍楼群看到了这样的一段话：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;凌晨不会再有地震洗衣机的呢喃，楼下能钻进来的柱子也装上了防护网。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;是啊，四年的时光已经过去了99%，我们变了很多，学校也变了很多。于是写下此文，记录下如掌心流沙般最后的时光。&lt;/p&gt;
</content:encoded></item><item><title>答辩准备中</title><link>https://yuzi.dev/posts/notes/preparing-for-defense/</link><guid isPermaLink="true">https://yuzi.dev/posts/notes/preparing-for-defense/</guid><description>毕业答辩前的短暂记录：排练、担心超时，以及临近正式答辩时的紧张感。</description><pubDate>Tue, 28 May 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;img src=&quot;./preparing-for-defense.webp&quot; alt=&quot;和聪神在理科南准备中&quot; /&gt;&lt;/p&gt;
&lt;p&gt;昨晚甚至做梦梦见一觉睡到了下午 3 点导致答辩迟到，害怕中……&lt;/p&gt;
&lt;p&gt;排练了一下，觉得可能得超时到10分钟才能讲完，希望不要被喊停😥。&lt;/p&gt;
</content:encoded></item><item><title>Bun 1.1，真的可用了吗？</title><link>https://yuzi.dev/posts/frontend/bun-1-1-is-it-really-usable/</link><guid isPermaLink="true">https://yuzi.dev/posts/frontend/bun-1-1-is-it-really-usable/</guid><description>记录一次在 Windows 上试用 Bun 1.1 的过程，主要观察调试、报错栈和打包能力是否已经足够可用。</description><pubDate>Thu, 11 Apr 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Bun 在这几天终于推出了 1.1 版本，终于补上了去年 10 月鸽到现在的 Windows 版，遂大喜，下载下来品鉴一番。&lt;/p&gt;
&lt;p&gt;其实我对 Bun 一直寄予厚望，一个大一统 JS/TS 生态链的庞然大物让人想想就激动，再也不用为 TS 配置、打包、运行而头疼不已、速度还一流，所以我在去年就开始关注它了，并几度尝试入坑，但是遗憾的是，至今仍是失败的。&lt;/p&gt;
&lt;h1&gt;试用 1.0 时的 bug&lt;/h1&gt;
&lt;p&gt;1.0 时，我便想试试把我的毕设的某一部分用 Bun 跑跑，结果不出所料地&lt;a href=&quot;https://github.com/apocas/dockerode/issues/747&quot;&gt;跑不起来&lt;/a&gt;，Bun 不支持 unix 连接到 docker daemon。关键功能无法使用，只好退坑。不过最近这个 issue 里说 Bun 1.1 已经解决了这个问题。&lt;/p&gt;
&lt;h1&gt;试用 1.1 时的坑&lt;/h1&gt;
&lt;p&gt;这次我想把一个使用 &lt;a href=&quot;https://pptr.dev/&quot;&gt;Puppeteer&lt;/a&gt; 的爬虫迁移到 Bun，结果一上来就遇到两个坑：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Windows 下无法使用 VS Code 进行 debug。这主要是因为它的 debug 插件只用 unix 连接到调试器，然而这个 unix 链接里的路径是 Windows 格式的路径，这下直接无法连接了。无法 debug。&lt;/li&gt;
&lt;li&gt;报错信息堆栈里没有原代码的位置提示。报错时只会弹出引用的库里的代码的报错位置，出错的原代码的位置一概不显示，我只能靠猜来修bug？？？？？&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;不过这都不要紧，我最需要的是它最激动人心的打包功能。打包后，像模像样地打出了一个 13MB、几万行的 js 文件，不错，把用到的库都打包出来了。一运行，报错。上 GitHub 一看，同样的 &lt;a href=&quot;https://github.com/oven-sh/bun/issues/4477&quot;&gt;issue&lt;/a&gt; 从去年 9 月躺到现在了。&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;最后还是用 vite-node 来进行 TS 小玩具的开发罢，Bun 还需要很长的时间来检验啊。有感兴趣的读者可以尝试一下。&lt;/p&gt;
&lt;p&gt;::github{repo=&quot;YuziO2/vite-node-ts-template&quot;}&lt;/p&gt;
</content:encoded></item><item><title>使用雷电4显卡坞作为All in one主机</title><link>https://yuzi.dev/posts/tinkering/using-thunderbolt-4-egpu-dock-as-all-in-one-host/</link><guid isPermaLink="true">https://yuzi.dev/posts/tinkering/using-thunderbolt-4-egpu-dock-as-all-in-one-host/</guid><description>记录用雷电 4 显卡坞搭配轻薄本组一台 All-in-one 主机的思路、硬件选择，以及实际体验中的优缺点。</description><pubDate>Tue, 12 Mar 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;因为作者目前只有一台轻薄本：联想小新Pro 14 2023，它的配置如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;CPU：i5-13500H&lt;/li&gt;
&lt;li&gt;内存：32GB 5200MHz 板载&lt;/li&gt;
&lt;li&gt;硬盘：1TB&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;如果想要玩游戏，缺少了一个最重要的配件：显卡。那么如果我想要敲开3A世界的大门的话，有以下两种选择：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;购入一台台式机，外加显示器、键盘鼠标、音响？&lt;/li&gt;
&lt;li&gt;购入一台游戏笔记本&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;很明显，两种方案都有些不爽的地方。第一种的话，成本较高，另外寝室地方太小，要装一台台式的话就废了整个桌子的空间，其次临近毕业，台式机的何去何从也是一个大问题；第二种的话，成本高昂，要达到同样的性能，台式机都能买两台了！而且同样是笔记本，是否与我的轻薄本的功能重叠了呢？&lt;/p&gt;
&lt;p&gt;就当我准备放弃这个想法的时候，显卡坞这个第三个方案出现在了我的视野中，我之前也有了解过显卡坞，不过大多都是Oculink的，它要么需要对笔记本进行大改以抠出一个直连主板的接口、要么就是购入新出的Thinkbook+之类的有OC口的笔记本。很显然这个方案不太行。但是适合我电脑的雷电4方案，在我看到了一款产品后让我心动了，这就是蜂鸟lite显卡坞。&lt;/p&gt;
&lt;h1&gt;方案简介&lt;/h1&gt;
&lt;p&gt;众所周知，雷电4拥有高达40Gbps的带宽，这使它外接显卡成了可能。不过雷电4方案是将显卡插在一个“微型电脑”——显卡坞上，由它和电脑之间充当“翻译”的角色。缺点就是比起OC直连会有些许损耗，优点是支持热插拔、不会烧CPU、通用性好【主要原因，因为我电脑支持2333】&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./mgVyZ3JJZ5FWUbR1p3Ecc19Z8-9697.jpg&quot; alt=&quot;我的电脑的接口介绍图片&quot; /&gt;
我很久之前搜索过显卡坞，结果都是一众2000-4000的货，“坞比卡贵”导致我很长时间没有关注过这个玩意。但我最近重新搜索后，发现蜂鸟lite这款去年11月的产品，699的售价和小巧的外型让我有了试一试的想法。&lt;/p&gt;
&lt;p&gt;在显卡选择方面，因为我专门用来玩游戏，且追求性价比，所以A卡就成为了我的不二选择，最后我选择了这个今年很火的甜品卡：RX6750 GRE 12GB。&lt;/p&gt;
&lt;p&gt;两款产品都有7天无理由，在抱着试试看的心态下下单了。价格：RX6750 GRE 12G ￥2279 + 显卡坞&amp;amp;雷电线&amp;amp;500W电源 ￥899 共3178元。&lt;/p&gt;
&lt;h1&gt;系统共存&lt;/h1&gt;
&lt;p&gt;思来想去，我想着显卡坞不能直接装在我现有的系统上，应该将游戏系统和生产力系统隔离。这样做最大的好处是在我游戏时不会让各种生产力软件来占用我的CPU和内存资源。但我也希望我的生产力系统的进程不会因为我要切换系统而被打断，所以需求总结一下：我需要生产力系统A和游戏系统B，切换过程是A休眠后启动B，B关机后启动A，A的进程仍在。&lt;/p&gt;
&lt;p&gt;首先我尝试了一下普通的双系统安装，但一个硬盘上只能有一个Windows Boot Manager，它在我启动休眠的系统时会直接跳过系统选择界面，也就是说我要从A进B必须关机或重启，这是我无法接收的。我又开始尝试将B的引导程序通过EasyBCD放入U盘中，让U盘来引导B系统。但无论是直接写入或借助Ventoy启动，休眠后再次启动都会启动到A系统，而且A的进程全部丢失，系统还会变得异常卡顿……&lt;/p&gt;
&lt;p&gt;最后祭出杀器，直接把我的闲置的固态硬盘拿来装Windows To Go，把系统装进固态移动硬盘中。在经过WTGA缓慢的部署后，终于将系统放上了移动硬盘。经测试，A休眠后按F12进入B，再重启后进入A后一切正常！这样一来就可以既不侵入现有系统又能释放系统全部实力了！&lt;/p&gt;
&lt;h1&gt;安装使用&lt;/h1&gt;
&lt;p&gt;等了两天，心心念念的东西终于来齐了。值得一提的是，显卡坞由顺丰从北京黄村装车，终到东莞石龙站，它是乘坐的行包专利X103一路赶来的。这或许是除了飞机之外最快的方式了吧。按照教程安装好电源和显卡，插入雷电4接口，启动！！！&lt;/p&gt;
&lt;p&gt;简单烤了下机，性能不错，功耗持续185W左右，达到了其TGP水平。玩了下游戏：原神高画质无压力、Apex高画质80-100帧左右、城市天际线2K高画质30帧左右（11万人的城市，实际体验很不错）。至于MC，由于加入了各种各样的奇怪的材质包和mod的缘故，大约在20-40帧左右，真乃大作也！测了一圈下来目前的配置是可以比较好的玩耍的。&lt;/p&gt;
&lt;p&gt;总结下来，还是挺不错的。如果你也是一个寝室局促的拥有雷电4轻薄本的大学生，想要低成本的玩上3A大作，这个方案是值得推荐的。现在我的本可以随时在轻薄带出门和火力全开游戏之间无缝切换。当然如果不差钱不差地，最好的还是直接轻薄本+台式罢！(👇一张完成图，占地还是挺小的)&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./49ca6007.webp&quot; alt=&quot;一张完成图，占地还是挺小的&quot; /&gt;&lt;/p&gt;
</content:encoded></item><item><title>24年1月总结：遗憾的旅行，毕设</title><link>https://yuzi.dev/posts/notes/january-2024-summary-regretful-trip-and-graduation-project/</link><guid isPermaLink="true">https://yuzi.dev/posts/notes/january-2024-summary-regretful-trip-and-graduation-project/</guid><description>回顾 2024 年 1 月：一场留下遗憾的大湾区旅行，以及在回家后推进毕业设计的日子。</description><pubDate>Wed, 31 Jan 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;今天来到了 1 月的最后一天，谨以此文回顾一下一月。&lt;/p&gt;
&lt;p&gt;月初，我花了两天的时间来规划了和 Leo 的大湾区的旅行，准备把广州、珠海、澳门、香港和深圳都转一圈。&lt;/p&gt;
&lt;p&gt;6 号进行了一个寝室的 4 人聚餐，地点还是熟悉的五栋牛肉火锅，在番禺广场转车时无意间见到了传说中的“蟑螂大楼”：&lt;img src=&quot;./roach-building.jpg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h1&gt;遗憾的旅行&lt;/h1&gt;
&lt;h2&gt;珠江新城的繁华&lt;/h2&gt;
&lt;p&gt;10 号我便和 Leo 开始了大湾区之行，那天在珠江新城领略了广州的繁华，也住了传说中的高级酒店，给我留下了难忘的印象。站在制高点饱览广州这座城市的夜景，在这夜幕下的万家灯火，倒也让我感伤了起来。
&lt;img src=&quot;./guangzhou-tower.jpg&quot; alt=&quot;熄灯后的广州塔&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;老城的悠行&lt;/h2&gt;
&lt;p&gt;11 号特种兵式地游览了越秀，虽然越秀去过不少次了，但是这次也给了我新的体验。在陈家祠门口，我们也碰到了一群正在排练舞狮的少年。在他们的矫健的身姿中我也领略到这一传统艺术的美。
&lt;img src=&quot;./lion-dance.jpg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;亲近自然之动物园、白云山&lt;/h2&gt;
&lt;p&gt;12 号走过了动物园和白云山。&lt;/p&gt;
&lt;p&gt;那天早上因为做噩梦，半夜把整个被子都打湿透了，我觉得这或许就是之后感冒的伏笔。&lt;/p&gt;
&lt;p&gt;早上一起床就感觉头非常昏沉，但还是强撑着走过了行程，也爬上了白云山巅——摩星岭。但是回到酒店后就倒头就睡，并开始发冷，晚上去旁边的小诊所看，果然是发烧了，但是因为症状不严重，就简单地开了些要就打发我回去了。&lt;/p&gt;
&lt;h2&gt;海滨漫游&lt;/h2&gt;
&lt;p&gt;13 号来到了珠海，早上游览了圆明新园，这个仿圆明园的景点让我非常入戏《甄嬛传》，景区里有个简陋的小缆车，出于对缆车的热爱我们还是花了几十元坐了个来回。&lt;/p&gt;
&lt;p&gt;下午沿着情侣路进行漫游，顺带一提在去的路上正好坐着滴滴经过了已经废弃的珠海有轨电车的全程。我不禁感叹，这玩意已经修到海边上了，如果能沿着海岸线修回拱北何愁没有客流？以至于停运呢？&lt;/p&gt;
&lt;p&gt;下午又偶遇了景山公园，它的缆车和滑车一下子又抓住了我的心。结果没想到的是，下面看着没什么人，结果一上去发现排队坐滑车下山的人排了快一百米！这时候又因为太阳快落山了，我们在山上吹着冷风瑟瑟发抖了一个小时。这或许也加重了我的感冒。
&lt;img src=&quot;./sunset-afterglow.jpg&quot; alt=&quot;余辉未尽&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;拥挤的澳门&lt;/h2&gt;
&lt;p&gt;14 号的澳门之行让人一言难尽，那天是周日，旅客多到爆炸。光是入住一个酒店都排了一个小时的队！上午游览完连贯公路的几个 xx 人酒店后，我感觉自己人实在不能继续游览，于是就叫 Leo 自己去完成当前的行程，而我在酒店休息，这时候人已经烧到了 39.5 度。我也因此错过了期待运转的轻轨过海段。&lt;/p&gt;
&lt;h2&gt;遗憾离场&lt;/h2&gt;
&lt;p&gt;晚上，由于我的身体状况，我只能提出终止我的旅程，购买了第二天从珠海回家的机票。不过在机场也俯瞰了澳门氹仔和港珠澳大桥
&lt;img src=&quot;./macau-bridge.jpg&quot; alt=&quot;氹仔和港珠澳大桥&quot; /&gt;&lt;/p&gt;
&lt;h1&gt;家中闲暇的生活&lt;/h1&gt;
&lt;p&gt;回家后就开始努力的完善毕设，在砍掉一些技术无关功能后，我在今天终于完成了我的毕设项目。2 月就是写论文的一个月啦！&lt;/p&gt;
&lt;p&gt;29 号是让我难忘的一天，无论从哪个角度~&lt;br /&gt;
运转了稀有的原色双层蓝皮车：
&lt;img src=&quot;./blue-train-1.jpg&quot; alt=&quot;&quot; /&gt;&lt;img src=&quot;./blue-train-2.jpg&quot; alt=&quot;&quot; /&gt;&lt;img src=&quot;./locomotive.jpg&quot; alt=&quot;机车&quot; /&gt;
回程坐了襄渝线路霸垃圾桶：&lt;img src=&quot;./trash-bin-train.jpg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;下午看了一场难忘的《星际穿越》。&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;难忘的1月结束了，希望二月能够越来越好。&lt;/p&gt;
</content:encoded></item><item><title>Node的Buffer转ArrayBuffer里的坑</title><link>https://yuzi.dev/posts/frontend/pitfalls-of-converting-node-buffer-to-arraybuffer/</link><guid isPermaLink="true">https://yuzi.dev/posts/frontend/pitfalls-of-converting-node-buffer-to-arraybuffer/</guid><description>记录一次下载小文件时体积异常膨胀的排查过程，问题最终定位到 Node Buffer 与 ArrayBuffer 转换时的额外字节。</description><pubDate>Thu, 25 Jan 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;在文章开始之前，感谢 GPT-4 给我的帮助和启发。&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;之前，下载的功能做好后，我就放着没管了，昨天下了个小文件（38B 大小）测试，发现下下来大小居然达到了 8KB！！！&lt;/p&gt;
&lt;p&gt;源文件：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#!/bin/bash

java -jar server.jar nogui
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;下载下来的样子：&lt;img src=&quot;./5e0c0435.png&quot; alt=&quot;这都是些啥？&quot; /&gt;&lt;/p&gt;
&lt;p&gt;然后就开始了排错，难道是对称加密的问题？GPT 也给出了解释：PKCS7 的算法会有一个填充，可能是这个填充没去掉。但是我的&lt;code&gt;WordArrayToArrayBuffer&lt;/code&gt;函数确实是正确地处理了这个填充问题，另外一个佐证就是，上传小文件是没有任何问题的，只有下载有问题。这么说问题就不是出在对称加密上。&lt;/p&gt;
&lt;p&gt;再审视一下 koa 端下载功能的代码实现：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const readable = fs.createReadStream(path, {
  start: current,
  end,
})
const buffer = await readStreamToBuffer(readable)
ctx.body = {
  contentBuffer: buffer.buffer,
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其中，buffer 的类型是 Node 的 Buffer，而其属性 buffer 则是提取出其底层的 ArrayBuffer。
将它们分别打印一下，果然就出现了问题：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;console.log(buffer.toString())
console.log(new TextDecoder(&apos;utf-8&apos;).decode(buffer.buffer))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;第一行打印出来的是正常的，第二行就出现各种奇奇怪怪的内容和乱码了！&lt;/p&gt;
&lt;p&gt;查阅后得知：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;在 Node.js 中，Buffer 对象是用于处理二进制数据的，它是 Uint8Array 的一个子类，但有一些额外的方法和属性。而 ArrayBuffer 是 JavaScript 的内建类型，用于处理二进制数据，它是 TypedArray 和 DataView 的基础。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;在之前的代码中，我试图将 Buffer 对象的.buffer 属性赋值给 contentBuffer。这在某些情况下可能会导致问题，因为 Buffer 对象的.buffer 属性实际上是底层的 ArrayBuffer，它可能比 Buffer 对象本身更大。这是因为 Node.js 可能会为 Buffer 对象预分配更多的内存，以便在需要时追加更多的数据。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;当试图将 Buffer 对象转换为 Uint8Array 时，你应该使用 Buffer 对象的.byteOffset 和.byteLength 属性，以确保只获取 Buffer 对象实际包含的数据，而不是整个底层的 ArrayBuffer。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;const buffer = await readStreamToBuffer(readable);
const uint8Array = new Uint8Array(buffer.buffer, buffer.byteOffset, buffer.byteLength);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;所以，原来是Node的Buffer转为原生的ArrayBuffer时我的理解出现了偏差，所以才出现了乱码。将下载部分按上述措施修改回来后，下载下来的小文件就没有任何问题了。看来对于各种API的用法、属性还是要多了解透啊！&lt;/p&gt;
</content:encoded></item><item><title>给退役的红米手机刷个机</title><link>https://yuzi.dev/posts/tinkering/flashing-a-retired-redmi-phone/</link><guid isPermaLink="true">https://yuzi.dev/posts/tinkering/flashing-a-retired-redmi-phone/</guid><description>记录给退役的红米 Note8 Pro 解锁、刷入 Magisk，并最终转向类原生 crDroid 系统的折腾过程。</description><pubDate>Tue, 23 Jan 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;上周回家后，把我妈的红米 Note8Pro 用我之前的 Note12Turbo 给换下来了，我也终于有一部现代的手机可以用来搞机了！&lt;/p&gt;
&lt;p&gt;经过 168 小时的漫长等待后，今天终于能解锁了！于是光速解锁刷机。&lt;/p&gt;
&lt;h1&gt;刷入 Magisk&lt;/h1&gt;
&lt;p&gt;教程：https://zhuanlan.zhihu.com/p/360655776&lt;/p&gt;
&lt;p&gt;根据教程中的方式刷入 TWRP，因为手机原本是基于安卓 11 的 MIUI12.5，所以我也选择了对应的版本刷入。&lt;/p&gt;
&lt;p&gt;然后根据教程刷入 Magisk：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;然后在电脑上&lt;a href=&quot;https://github.com/topjohnwu/Magisk/releases&quot;&gt;下载 Magisk&lt;/a&gt;（面具）安装包兼卡刷包和 &lt;a href=&quot;https://syxz.lanzoub.com/ijMsSng1j2b&quot;&gt;ADB 命令行&lt;/a&gt;。下载后将 Magisk 文件名更改为 magisk.zip（是的，后缀改 zip），移动到 ADB 命令行文件夹内，然后将手机在 TWRP 中用数据线连接电脑，电脑打开 ADB 命令行，依次输入以下命令：&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;adb push magisk.zip /tmp/
adb shell twrp install /tmp/magisk.zip
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;如果没有出错，手机上此时会显示正在刷入。刷入完成即 Root 成功，然后就可以开机了。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;开机后桌面上是没有 Magisk APP 的，需要自己安装。将电脑上的 magisk.zip 重命名成 magisk.apk，复制到手机里安装即可。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;然后当我准备利用 Magisk 安装谷歌全家桶时（通过&lt;a href=&quot;https://github.com/wacko1805/MagiskGapps&quot;&gt;MagiskGapps&lt;/a&gt;），然而其 readme 中赫然写道：“DOES NOT work on MIUI.”我还不信邪地试了一下，安装上了果然直接闪退。&lt;/p&gt;
&lt;p&gt;真气人，不过都解锁了，直接上个类原生吧！！！&lt;/p&gt;
&lt;h1&gt;刷入类原生系统&lt;/h1&gt;
&lt;p&gt;系统选择上，我选了用得比较多机型也比较全的&lt;a href=&quot;https://crdroid.net/&quot;&gt;crDroid&lt;/a&gt;，对于我的机型也适配到了安卓 14！&lt;/p&gt;
&lt;p&gt;下载完包后，进 TWRP 后进行一个双清，然后直接刷入！顺便也把上面的 Magisk 也补上。视频教程：https://www.bilibili.com/video/BV1n64y1u7p4&lt;/p&gt;
&lt;h1&gt;类原生体验&lt;/h1&gt;
&lt;p&gt;刷入后也进行了一个体验，类原生和 MIUI 还是有很大的不一样的，谷歌处处都体现了其细腻的设计，难不怪在国外使用谷歌全家桶体验也不输苹果！&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./cda954d7.jpg&quot; alt=&quot;一个设置页面&quot; /&gt;&lt;/p&gt;
&lt;h1&gt;搞个 Linux&lt;/h1&gt;
&lt;p&gt;最开始我想着用 Linux deploy 来装一个 Linux，但是这个玩意貌似年久失修且我尝试安装 Arch 时总是失败。最后还是转向了 Termux 的 proot 方案，这个方案倒也不需要 root 了。一个小插曲是，我用 openSUSE 的 openssh 时总是有问题，最后迫不得已还是转回了 Arch，看来还是 Arch 神教好啊。&lt;/p&gt;
&lt;p&gt;不过这个 Linux 毕竟不是完整的，所以有可能会出现奇奇怪怪的 bug，比如我运行 vite 良好，但是想放上局域网就崩溃了，上网一查貌似是 termux 和 node 中 os 模块的一个 API 实现有问题导致的。不过 vscode 自带端口转发，日常拿来开发应该问题不大【应该吧】。&lt;/p&gt;
&lt;h1&gt;后记&lt;/h1&gt;
&lt;p&gt;貌似刷完了发现并没有什么用，目前还没有什么正好需要跑在上面的服务，考虑到它基于proot没有systemd的特性，也不是很敢拿来把工作流扔上去。当是一次新奇的体验罢。类原生虽好，但拿来当主力系统应对各种奇怪的毒瘤软件还是有点力不从心了，目前当一个备用机来把玩吧。如果读者有什么新鲜的用法欢迎评论区留言！&lt;/p&gt;
</content:encoded></item><item><title>流式上传文件</title><link>https://yuzi.dev/posts/frontend/streaming-file-upload/</link><guid isPermaLink="true">https://yuzi.dev/posts/frontend/streaming-file-upload/</guid><description>从大文件上传的内存压力出发，介绍浏览器端分块、服务端鉴权和文件合并的一套流式上传实现思路。</description><pubDate>Thu, 04 Jan 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;a href=&quot;https://yuzi.dev/posts/frontend/next-koa-file-upload-and-encrypt&quot;&gt;书接上回&lt;/a&gt;，直接整个文件整存整取对于小文件来说还好，如果是几 G 的大文件，对内存的占用不能不忽视了。所以我们应该借鉴流式传输的思想，每一次只处理一块，处理完就丢出去（丢给下一步，或最终写入硬盘），这样同一个时刻只会占用服务器的小部分内存，减轻了服务器的压力。&lt;/p&gt;
&lt;h1&gt;浏览器端拆分&lt;/h1&gt;
&lt;p&gt;首先，我们在浏览器端就可以完成文件的读取，&lt;code&gt;file.stream().getReader()&lt;/code&gt;就可以帮助我们简单的完成流式读取。其简单用法如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;customRequest={async ({ file }) =&amp;gt; {
  if (file instanceof File) {
    const reader = file.stream().getReader()
    while (1) {
      const { value, done } = await reader.read()
      console.log(value, done)
      if (done || value === undefined) {
        // 读取完成
        break
      }
      //value 即为 Uint8Array 格式的 ArrayBuffer，可以在此发送这个 chunk，直到读取完成
    }
  }
}}
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;鉴权问题&lt;/h1&gt;
&lt;p&gt;流式上传简单地说就是把文件拆成几块分成几次发，但是拆分文件意味着会有几次甚至几十次的请求，如何鉴权呢？总不可能每次都查表吧，那样效率简直低到让人发指。所以我们可以借鉴「缓存」的思想，给一个文件的请求生成一个 Token，存进一个哈希表里，之后的请求就可以让前端带上这个 Token，就可以直接查快速的哈希表了，而不需要再去查数据库，上传完成后在哈希表里删去这个 Token 即可。&lt;/p&gt;
&lt;p&gt;基于以上思想，上传一个文件可以分成三部分：第一个 chunk，带上 id、path、name 等信息，next 鉴权后生成 Token 存入哈希表中，并将这个 Token 返回前端-&amp;gt;后续上传请求，只带 Token 和 order-&amp;gt;上传完成，带上 total，后端完成文件合并并放置在指定位置，删去 token。&lt;/p&gt;
&lt;p&gt;可以借助两张草稿理解这个过程（草稿和最终采用的方法并不完全一样，后面会提）：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./f1f40038.jpg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./73a1af5a.jpg&quot; alt=&quot;最后并没有使用 Promise.all()，而是一个 chunk 完成再传下一个&quot; /&gt;&lt;/p&gt;
&lt;p&gt;最初的想法是，把这些请求放进 promiseAll 中，resolve 了就代表上传已经完成。但是这样一股脑的发请求会直接卡死服务端，而且请求也会超时造成失败。所以最后采取的方法仍是一个请求一个请求的发，完成后发下一个请求，所以前端可以写成这样：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;customRequest={async ({ file }) =&amp;gt; {
  if (file instanceof File) {
    const reader = file.stream().getReader()
    let count = 0
    let token = &apos;&apos;
    while (1) {
      const { value, done } = await reader.read()
      console.log(value, done)
      if (done || value === undefined) {
        await uploadFile({ token, total: count - 1 })
        message.success(&apos;上传成功！&apos;)
        break
      }
      if (count === 0) {
        const res = await uploadFile(
          { id: instanceId, path, name: file.name },
          { buffer: value, count },
        )
        if (typeof res != &apos;string&apos;) token = res.token
      } else {
        console.log(token)
        await uploadFile({ token }, { buffer: value, count })
      }
      count++
    }
  }
}}
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;以合适的大小拆分&lt;/h1&gt;
&lt;p&gt;这样写好后，新的问题产生了，reader 每次读出的 chunk 大小都不一样，从几十 KB 到几 MB 不等，这样一个文件要请求很多很多次，我们可以试着把这个 buffer 攒一下，大一些了再请求，经过我的简单思考，就定在 10MB 吧！&lt;/p&gt;
&lt;p&gt;不过 ArrayBuffer 是没有 concat 方法来简单的合并两个 Buffer 的，不过简简单单的 polyfill 一下就行了【也不知道为啥 ES 里没有】&lt;/p&gt;
&lt;p&gt;方法来自&lt;a href=&quot;https://zh.javascript.info/arraybuffer-binary-arrays&quot;&gt;现代 JS 教程&lt;/a&gt;下面的评论，我加上了 TS 和解构，tsc 提示要使用迭代 ArrayBuffer 的特性需要 target 设置为 es2015 以上：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const concatBuffer = (...ArrayOfBuffer: Uint8Array[]) =&amp;gt; new Uint8Array(ArrayOfBuffer.flatMap((it) =&amp;gt; [...it]))

//用法：
buffer = concatBuffer(bufferA, bufferB,...)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;所以我们就可以以每次请求 10MB 的大小拆分文件上传了，经过整理后，最终的前端代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;customRequest={async ({ file }) =&amp;gt; {
  if (file instanceof File) {
    if (files.find((f) =&amp;gt; f.name == file.name)) {
      // 重名不行
      message.error(&apos;文件名重复了！&apos;)
      return
    }
    setUploadLoading(true)
    const reader = file.stream().getReader()
    let count = 0
    let token = &apos;&apos;
    let buffer = new Uint8Array()
    const concatBuffer = (...ArrayOfBuffer: Uint8Array[]) =&amp;gt;
      new Uint8Array(ArrayOfBuffer.flatMap((it) =&amp;gt; [...it]))
    while (1) {
      const { value, done } = await reader.read()
      if (done || value === undefined) {
        if (buffer.byteLength) {
          //如果还有剩余，上传
          await uploadFile({ token }, { buffer, count })
        }
        await uploadFile({ token, total: count - 1 })
        message.success(&apos;上传成功！&apos;)
        // 然后刷新文件列表
        setUploadLoading(false)
        break
      }
      if (count === 0) {
        const res = await uploadFile(
          { id: instanceId, path, name: file.name },
          { buffer: value, count },
        )
        if (typeof res != &apos;string&apos;) token = res.token
      } else {
        buffer = concatBuffer(buffer, value)
        if (buffer.byteLength &amp;lt; 1024 * 1024 * 10) {
          count-- //本轮不计数
        } else {
          await uploadFile({ token }, { buffer, count })
          buffer = new Uint8Array()
        }
      }
      count++
    }
  }
}}
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;Next 后端设置&lt;/h1&gt;
&lt;p&gt;在前篇，我们已经完成了二进制 buffer 的加解密工作，我们此时就可以直接使用。由于在前端已经拆分好了文件，文件的目标是 Koa，所以 Next 后端只需扮演一个传声筒的角色即可。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const uploadMap = new Map&amp;lt;string, { request: AxiosInstance }&amp;gt;() //Next 的哈希表只需要存向 koa 请求的工具函数即可
export async function POST(req: NextRequest) {
  try {
    if (req.nextUrl.searchParams.get(&apos;path&apos;)) {
      // 第一次请求上传文件
      const { request, instanceID, path } = await verify(req)
      const fileName = req.nextUrl.searchParams.get(&apos;name&apos;)!
      const token = crypto.randomBytes(32).toString(&apos;hex&apos;) //用 node 自己的 crypto 库即可生成一个 32 位的 Token
      uploadMap.set(token, {
        request,
      })
      const formData = await req.formData()
      const file = formData.get(&apos;buffer&apos;) as Blob
      await request.post(&apos;/file/upload&apos;, {
        path,
        id: instanceID,
        contentBuffer: await file.arrayBuffer(),
        fileName,
        token,
      })
      return response({ data: { token } })
    } else if (req.nextUrl.searchParams.get(&apos;total&apos;)) {
      // 上传完成
      const token = req.nextUrl.searchParams.get(&apos;token&apos;)!
      const total = Number(req.nextUrl.searchParams.get(&apos;total&apos;))
      const { request } = uploadMap.get(token)!
      await request.post(&apos;/file/upload&apos;, {
        total,
        token,
      })
      return response({ data: &apos;ok&apos; })
    } else if (req.nextUrl.searchParams.get(&apos;token&apos;)) {
      // 后续请求上传文件，token 验证，无需验证权限
      const token = req.nextUrl.searchParams.get(&apos;token&apos;)!
      if (!uploadMap.has(token)) throw &apos;Token invalid.&apos;
      const formData = await req.formData()
      const file = formData.get(&apos;buffer&apos;) as Blob
      const order = formData.get(&apos;count&apos;) as string
      const { request } = uploadMap.get(token)!
      await request.post(&apos;/file/upload&apos;, {
        contentBuffer: await file.arrayBuffer(),
        order,
        token,
      })
      return response({ data: &apos;ok&apos; })
    }
  } catch (e) {
    console.log(e)
    return response({ error: typeof e == &apos;string&apos; ? e : &apos;文件上传失败！&apos; })
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;Koa 写入文件配置&lt;/h1&gt;
&lt;p&gt;因为最开始写的是 PromiseAll 的方案，所以考虑到了乱序到达的问题，给加了 order，现在使用顺序请求的方式其实不需要 order，可以直接在一个文件上 appendFile 最后移动，特此说明。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const uploadMap = new Map&amp;lt;
  string,
  { id: string; path: string; name: string; count: number; total: number }
&amp;gt;() //count 和 total 其实已经不再需要了
router.post(&apos;/upload&apos;, async (ctx) =&amp;gt; {
  try {
    const { path, id, fileName, contentBuffer, token, order, total } = ctx
      .request.body as {
      path?: string
      id?: string
      fileName?: string
      contentBuffer?: Uint8Array //中间件已经解密处理过了，并转成了 ArrayBuffer
      token: string
      order: string
      total?: number
    }
    if (!token) throw &apos;token not found&apos;
    if (path &amp;amp;&amp;amp; id &amp;amp;&amp;amp; fileName &amp;amp;&amp;amp; contentBuffer) { //第一次请求，置 token
      uploadMap.set(token, { id, path, name: fileName, count: 0, total: -1 })
      fs.appendFileSync(
        &apos;./data/temp/upload/&apos; + token + &apos;-0&apos;,
        Buffer.from(contentBuffer),
      )
    } else if (total !== undefined) { //上传完成，合并文件
      const { count, id, path, name } = uploadMap.get(token)!
      uploadMap.set(token, { id, path, name, count, total })
      fs.readdirSync(&apos;./data/temp/upload&apos;)
        .filter((file) =&amp;gt; file.startsWith(token))
        .sort((a, b) =&amp;gt; parseInt(a.split(&apos;-&apos;)[1]) - parseInt(b.split(&apos;-&apos;)[1])) //把分块文件筛选出来排好序
        .forEach((file) =&amp;gt; {
          fs.appendFileSync(
            &apos;./data/ContainerData/&apos; + id + path + name,
            fs.readFileSync(&apos;./data/temp/upload/&apos; + file),
          ) //塞入目标文件
          fs.unlinkSync(&apos;./data/temp/upload/&apos; + file) //删去临时分块文件
        })
      uploadMap.delete(token) //删去哈希表中的 token
    } else if (order &amp;amp;&amp;amp; contentBuffer) { //中间的请求，通过 token 鉴权
      const { count, id, path, name, total } = uploadMap.get(token)!
      uploadMap.set(token, { id, path, name, count: count + 1, total })
      fs.appendFileSync(
        &apos;./data/temp/upload/&apos; + token + &apos;-&apos; + order,
        Buffer.from(contentBuffer),
      )
    }
    ctx.body = &apos;success&apos;
  } catch (error) {
    ctx.status = 400
    ctx.body = {
      error,
    }
  }
})
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;最后&lt;/h1&gt;
&lt;p&gt;以上的工作看着虽然简单，但也是结结实实地折腾了一天。经过这番折腾，我对二进制、对流的认识又深入了不少、也对性能优化有了新的理解。&lt;/p&gt;
&lt;p&gt;另外，中午我的指导老师邀请小组成员开会，会上我评估了一下未完成的工作，发现有十个甚至九个Feature亟待开发。然而我明天起也必须停止开发转向完善我的开题报告的工作去了。唉，时间紧任务重啊！！&lt;/p&gt;
</content:encoded></item><item><title>我的2023</title><link>https://yuzi.dev/posts/notes/my-2023/</link><guid isPermaLink="true">https://yuzi.dev/posts/notes/my-2023/</guid><description>回顾 2023 这一年里的手术、旅行、实习、折腾和求职经历，给自己这一年留下一个完整的阶段性总结。</description><pubDate>Sun, 31 Dec 2023 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;今天是 2023 的最后一天了，所以为了总结我的这一年，我特地留在了寝室里写下了这篇。今年实在是过于精彩，比前 20 年都要让我印象深刻，回望一下，估计每一个月的我都完全无法想象下一个月的我会是什么样在做什么。所以，流水账开始！&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./6ec41e5e.jpg&quot; alt=&quot;即将过完的一年&quot; /&gt;&lt;/p&gt;
&lt;h1&gt;1 月：手术、服务器、过年&lt;/h1&gt;
&lt;p&gt;去年的这个时候，我正在达州的中心医院的肛肠科躺着进入了 2023。这个痔疮手术真是要了人半条命，但是我之后也确实饮食偏向清淡了，做了一个真正的广东人！月初购入一款红米真无线耳机并使用至今。10 号独自跑到政务中心办理了赴港澳通行证。之后月中是没啥可说的过年，24 号在咸鱼淘到了一个小主机，放在家里当小服务器用，现在它承载着我的 tg 机器人、课设数据库、云盘等功能，还是很值的。30 号去成都进行了一次旅游。&lt;/p&gt;
&lt;p&gt;另外，我搭建了一个 MC 服务器，没想到时至今日我们还在这个服务器里辗转游玩。&lt;/p&gt;
&lt;h1&gt;2 月：成都之行、返校、奥特莱斯；徘徊&lt;/h1&gt;
&lt;p&gt;1 号到 6 号一直在成都旅行，游览了诸如都江堰、东安湖等著名景点，还光顾了罗小黑的主题店，给奶奶庆祝了 90 岁生日。17 号返回学校，第一次体会到在广州北站下车坐 9 号线的酸爽。22 号这天和 Lionaom 光顾了万国·奥特莱斯，低价买下了一双安踏的鞋「结果这双鞋的鞋带老是掉」。27 号这天和 Lionaom 去了南村万博的萨莉亚，还第一次喝了一些酒。&lt;/p&gt;
&lt;p&gt;代码方面，我冒出了毕设的一个想法：给 alist 写一个桌面应用程序，可以实现同步盘的功能，结果搭好了 electron 的框架写了个菜单后就被搁置最后放弃了。&lt;/p&gt;
&lt;p&gt;这一个月我其实是一直有些徘徊的：未来的路该怎么走？是考研还是找实习？当时我还是偏向考研的，还去专门看了一下考研的房子，但是最后还是选择了工作，原因是觉得目前考研花销太大（比如要租房，不租房的话寝室没有学习氛围很难坚持），还有报班之类的。所以最后我的决定是：先找工作，如果没有找到满意的工作，就参加下一年的考研。&lt;/p&gt;
&lt;p&gt;一张 2 月 19 日的服务器线路图：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./7ee1cc19.jpg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h1&gt;3 月：语文系统、换电脑、找实习&lt;/h1&gt;
&lt;p&gt;3 月，我接下了学校老师的一个任务：开发一个语文教材录入系统，基于 Vue3 的 vben，可是这玩意的过度封装有亿点严重，文档也不是很清楚，所以写起来还是有点痛苦，好在最后完成了。这个时候感觉我的开发工作对于我的 16+512 的 RedmiBook 有点吃力了，又想着要去找工作了要靠电脑吃饭了，于是厚脸皮地找父母要钱换了台笔记本，是 i5-13500H，32G+1T 的小新 Pro14，确实挺好用的。&lt;/p&gt;
&lt;p&gt;这个月便开始了找实习之路，期间还为某个公司出的题写了一个简单的可视化播放器。在 23 号开始了第一场面试，结果 31 号就找到工作了，感觉号线还挺走运的哈……就这样开启了在汇量实习的潘多拉魔盒。&lt;/p&gt;
&lt;p&gt;28 号这天和 Lion 不远千里去吃了五洞的牛肉火锅，好吃是好吃，也挺贵的。31 号这天晚去南村吃了顿”牛蛙庆功宴“。&lt;/p&gt;
&lt;p&gt;一张 3 月底的服务器线路图：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./d72dbbf3.png&quot; alt=&quot;延长了 2 号线，建设了 3 号线首通段和 4 号线首通段和二期工程&quot; /&gt;&lt;/p&gt;
&lt;h1&gt;4 月：入职、换手机、运转&lt;/h1&gt;
&lt;p&gt;1 号乘坐水巴进行了一个便宜的“珠江日游”。3 号入职，认识了和我一个学校一个公司的杰哥、同一天入职的前端开发洪某……开始了打工人的生活。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./985af83b.jpg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;上班的日子其实还好，工作对我来说难度刚好，又包饭又有工资拿，社交圈子还极为扩大了，周末也有闲心出去玩了。8 号和 Lionaom 去游览了海珠湖，感觉没啥好玩的。15 号看到海傍的万达电影院有《流浪地球 2》的告别场的 IMAX，心心念念 IMAX 的我拉着 Chaos 一齐去观赏了，还获得了两张海报，遂拿了一张贴在门背后。21 号乘坐 21 号线去了增城广场。&lt;/p&gt;
&lt;p&gt;22 号，我的手环脱胶了，屏幕都掉了出来，不过去商中花 10 块钱沾了一下就好了。不过要命的是，我的手机在一次上厕所时不小心掉进去了，虽然立马捞了上来，但是 K30Pro 孱弱的防水，它的屏幕开始出现了无法触控的现象，遂消费降级换了 Note12Turbo。&lt;/p&gt;
&lt;p&gt;这个月服务器的新线建设得如火如荼，在 9 号一个下午就大幅延长了 3 号线，还使其成为第一条拥有快车的线路。另外，服务器迁移上了腾讯云，我使用 NodeJS 重新实现了一个 tg 控制机器人，没想到这个竟然是我的毕业设计的伏笔。在某个下午与 Situ 去了黄埔的星巴克把这个项目 TS 化了，过程也踩了不少坑。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./5c861f19.png&quot; alt=&quot;延长了 4 号线作为其支线，同时预留了主线、延长了 1 号线，3 号线、开通了新线 L1 线，0 号线&quot; /&gt;&lt;/p&gt;
&lt;h1&gt;5 月：广告项目、职业规划大作业、购入台式机&lt;/h1&gt;
&lt;p&gt;1 号，去游玩了沙湾古镇和佛山的中心区，被挤死。这个月我在工作上主要搞了一个视频网页项目，扣动画、对细节的工作把我累得要死，在经历了两周的开发和两周的测试后项目可算是完成了。&lt;/p&gt;
&lt;p&gt;10 号，发工资了，结果这天我在广大派上看到了一个台式机，关键是机主为了装黑苹果还换了博通的网卡。一台既能打游戏又能装黑苹果的台式让我心动不已，于是头脑一热便购入了。&lt;/p&gt;
&lt;p&gt;17 号，我们寝室在图书馆五楼雅间组织了一个会议，为了完成职业规划课的大作业，这估计是今年最后一次寝室全员活动了，伤感……&lt;/p&gt;
&lt;p&gt;21 号游览了流花湖公园，让人惊喜的是这里的游乐设施，花了几十块钱就能体会到几百块钱人挤人的游乐场的快乐。27 号和 Situ 运转了 14 号线去到了东风，可惜刚好错过快车。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./983cd74f.jpg&quot; alt=&quot;0 号线成环了，4 号线缩回湾湾岛，新建 10 号线，新建 1-2 联络线并开行直通车&quot; /&gt;&lt;/p&gt;
&lt;h1&gt;6 月：江门游、实习生们的团建聚餐、滑板、回家、广九线运转&lt;/h1&gt;
&lt;p&gt;这个月几乎是天天出去玩了，4 个周末有 3 个都在出游。&lt;/p&gt;
&lt;p&gt;4 号这天，去了江门一日游，还大手笔地使用积分购买了一等座，参观了大名鼎鼎的小鸟天堂。7 号隔壁组 leader 带着我们几个实习生团建，可能是今年唯一一次运动？？8 号这天再次聚餐，送别我们的源神离职，吃萨莉亚吃到撑！10 号，种草了一款滑板并购入，发现这玩意拿来代步还是挺鸡肋的，主要是沥青路太震脚了，遂玩了没几次便雪藏。&lt;/p&gt;
&lt;p&gt;17 号早早地起床赶上早班飞机回家，由于机票降价，我可以在这个周末回家给我老爸庆祝父亲节&amp;amp;生日。正好爷爷奶奶也在家，于是就大办了一场，虽然我妈一开始不愿意我花钱回来，但是我真回来了她又很开心（笑）。&lt;/p&gt;
&lt;p&gt;20 号飞回来后，30 号又踏上了去香港的路，从广州东站一路出发经广深线，东铁线到达红磡，也算是完整地体验了一次广九铁路吧！&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./6b66a8f7.jpg&quot; alt=&quot;延长了 10 号线、3 号线，顺便把 4 号线接过来共线了&quot; /&gt;&lt;/p&gt;
&lt;h1&gt;7 月：香港游、搬去了出租屋&lt;/h1&gt;
&lt;p&gt;1-2 号特种兵式地游览了香港，具体可见：https://blog.yuzi.dev/posts/2a89736b.html&lt;/p&gt;
&lt;p&gt;这个月放暑假了，但是由于种种原因，我没有成功申请到留宿，而宿舍唯一有留宿资格的同学因为家里有事回去了，我迫于宿管压力只好搬去南亭住宿。&lt;/p&gt;
&lt;p&gt;MC 服务器上没啥进展，开通了 5 号线彩虹广场--三山仙池。&lt;/p&gt;
&lt;h1&gt;8 月：秋招提前批、长隆、CPGZ5&lt;/h1&gt;
&lt;p&gt;8 月，接受了百度的 3 次面试、快手的 2 次面试、字节的 2 次面试、美团的一次面试。除了百度表示通过了面试等待评估外，其它的都失败了。大厂还是难的嘞。&lt;/p&gt;
&lt;p&gt;13 号趁着生日优惠去了长隆、26 号跟 Chaos 去了 CPGZ，氛围很好，终于见识到大的同人展的样子了。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./c255b007.png&quot; alt=&quot;开通了超快的 S1 线，延长 5 号线、3 号线&quot; /&gt;&lt;/p&gt;
&lt;h1&gt;9 月：和大佬们团建、长隆水上乐园、澳门游、回家&lt;/h1&gt;
&lt;p&gt;9 月和各种大佬们聚餐，光是海底捞都去了两回。当然，那个时候自我感觉良好，觉得百度应该没啥问题，就积极参加大佬们的社交活动了。对于秋招也没有多上心了，9 月一次面试笔试也没有（好吧还是有一次面试，结果是个超小的初创，还是走的社招）。&lt;/p&gt;
&lt;p&gt;23 号游览了澳门，见识到了赌场的奢华，其中发生了一件趣事：一个保安见我要进赌场，笑着拦下我说小朋友不可以进，我表示我 21 岁了他还表示很震惊！emmm，我还是很年轻嘛！&lt;/p&gt;
&lt;p&gt;月初，突然被通知我两个月前报名的农行实习要去面试，于是我和 Veneno 和杰哥一齐参加了面试，面试十分地简单，虽然问了技术但不多。月中突然通知我和 Veneno 面试通过了。于是抱着体验国企的心态，我辞去了在汇量的长达 6 个月的实习，去参与为期 1 个月的农行实习。&lt;/p&gt;
&lt;p&gt;27 号，实习没两天，我便请假坐着高铁回家过国庆了。&lt;/p&gt;
&lt;p&gt;这个月南延了 S1 线，北延了 10 号线以及新建 13 号线（由 10 号线代运行），北延 L1 线，新建 L2 旅游专线。将 4 号线跨入 10 号线部分改为 14 号线，4 号线仍缩至湾湾岛。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./596ed2c4.png&quot; alt=&quot;线路图使用 RailMap Generator 生成 使用广州地铁风格&quot; /&gt;&lt;/p&gt;
&lt;h1&gt;10 月：达州周边游、农行打工&lt;/h1&gt;
&lt;p&gt;国庆期间在达州周边走了走，和父母去石梯吃了鱼、去石桥参观了完全没有商业化的列宁街。&lt;/p&gt;
&lt;p&gt;农行的上班日子非常折磨，我的单程通勤时间在 80-100 分钟左右，非常的极端。工作方面体验也比较糟糕，一个权限需要层层传达和等待、只有内网和带瞎眼拖影屏幕的开发机……14 号这个周六跟 Lionaom 去尝了尝“深井烧鹅”，非常的好吃！以及关了门的黄埔军校旧址，还乘坐了水巴到达了对面的鱼珠渡口。&lt;/p&gt;
&lt;p&gt;游戏方面，简单的上手了 Apex，虽然只是逛街的程度……&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./e9b14c4c.png&quot; alt=&quot;10 号线北延，不再担当 13 号线、L2 成环&quot; /&gt;&lt;/p&gt;
&lt;h1&gt;11 月：秋招失利、卖出台式和滑板、烫头发、购入 iPad Air、考虑考研&lt;/h1&gt;
&lt;p&gt;11 月中，百度开始陆续发放 offer，等了一周后发现周围的人都有 oc 了，自认定评估失败，准备考研或春招。&lt;/p&gt;
&lt;p&gt;自认为台式没有继续留着的必要，既然要考研了，留着干什么，也没有空打游戏了，于是挂上咸鱼，顺便也把许久没用的滑板也挂了上去。出乎意料的是，滑板倒是问的人很多并很快出出去了，台式却迟迟没有人问，过了几天被一个大学城的教练以一个不错的价格给买走了。&lt;/p&gt;
&lt;p&gt;参加了重庆懂车帝的面试，可惜二面明明聊得挺好的却被挂了，&lt;a href=&quot;https://yuzi.dev/notes/3&quot;&gt;那一周的心路历程&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;最戏剧性的一幕来了，周一晚，那时我已经接受百度 offer 评估失败的结果，我和 Lionaom 与 Situ 在北亭的中国汉堡用餐，我的二面面试官突然回了我几周前的邮件告知我通过了，顿时喜不自胜。第二天也如约收到了 OC（还是在运转 14 号线的快车上接到的，当时人在嘉禾望岗），但是 HR 说因为我是双非，要走“特殊审批通道”，大家都说都走到这一步了总不可能有差错吧，结果第二天（周三）又接到了 HR 的电话，委婉地告知大领导不要双非，让我另寻机会，因为之前有心理准备，我倒并不至于是遇见了晴天霹雳一般，又冷静地回到了“先做毕设，再看机会，保底考研”的规划上了。&lt;/p&gt;
&lt;p&gt;因为决心要考研了，又因为做毕设的时候感觉没有写写画画的规划思路很乱有掣肘，所以决定购入 iPad，在咸鱼上看到了一个不错的卖家，于是在 27 号这天购入了这个激活不到一个月的 iPad Air 5。&lt;/p&gt;
&lt;p&gt;28 号，登上许久没有玩的原神，把大保底拿出来抽了赛诺。MC 方面，这个月把 14 号线延长到了摘星谷，并在那里建了一个「家」。10 号线和 13 号线则继续共线南延，并于西延的 S1 支线交汇于金沙舟、新秀 2 号线康城支线，并改 12 号线经其运行，取消 2 号线西边的小交路，改为大裂谷 - 东郊钓场。MTRBBS 上的大佬开源了重庆单轨 2 号线，于是火速建设了几乎完全高架的 6 号线并使用该列车运行。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./5eb6ac5f.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h1&gt;12 月：毕设、面试、OC、运转&lt;/h1&gt;
&lt;p&gt;12 月开始疯狂肝毕设，成为了图书馆 101 的常客。攻克了&lt;a href=&quot;https://yuzi.dev/posts/frontend/react-reducer&quot;&gt;状态管理&lt;/a&gt;和&lt;a href=&quot;https://yuzi.dev/posts/frontend/next-koa-file-upload-and-encrypt&quot;&gt;二进制加解密&lt;/a&gt;等难点。另外为了抢座位还基于 Node 和无头浏览器搞了个自动预约系统。&lt;/p&gt;
&lt;p&gt;第二周，之前投了的三家公司发来了面试邀请，于是&lt;a href=&quot;https://yuzi.dev/notes/5&quot;&gt;那一周经历了疯狂的 9 场面试&lt;/a&gt;。幸运的是，我秋招捡漏成功，最后选择了了途虎养车的 offer，以后就是武汉打工人了！&lt;/p&gt;
&lt;p&gt;最后的一周，去了南沙湿地、三山森林公园、5&amp;amp;7 新线开通黄埔游、清远一日游。算是尽情的给这个 2023 划上了一个完美的句号。&lt;/p&gt;
&lt;h1&gt;最后&lt;/h1&gt;
&lt;p&gt;花了一个下午回顾了一下我的2023，有欢笑，有失落。我也要特别感谢我的新朋旧友们，特别是杰哥，Situ，Lionaom，源某，洪某，Leo等。要毕业离开校园、走向社会了，希望2024会越来越好。&lt;/p&gt;
</content:encoded></item><item><title>NextJS和Koa的文件上传&amp;下载以及二进制加密&amp;解密</title><link>https://yuzi.dev/posts/frontend/nextjs-and-koa-file-upload-download-binary-encryption-decryption/</link><guid isPermaLink="true">https://yuzi.dev/posts/frontend/nextjs-and-koa-file-upload-download-binary-encryption-decryption/</guid><description>梳理一个基于 NextJS 和 Koa 的文件管理方案，包括上传下载流程，以及请求链路中的二进制加密与解密。</description><pubDate>Sun, 24 Dec 2023 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;最近，我的毕设来到了对容器的文件管理的需求。而在管理文件中，最让人头大的则是文件的上传与下载了。&lt;/p&gt;
&lt;p&gt;首先先回顾一下项目的整体架构吧，Next 提供 ssr 渲染出的前端页面&amp;amp;handle API 请求；基于 Koa 的 Daemon 部署在各个可以公网访问到的 VPS 上，作为容器运行的环境。Daemon 提供 Http API 供 Next 调用。目前 Daemon 会有一个固定的 token，作为与 Next 通信的对称加密的密钥。因为部署 Next 的机器肯定有 https，而 Daemon 所在的节点可能没有，所以这第二程（Next-&amp;gt;Koa）数据是一定需要加密的，而第一程则不需要。&lt;/p&gt;
&lt;p&gt;所以一个请求的流程是，客户在浏览器发起请求到 Next route handler，Next 鉴权无误后利用 token 加密请求到 Daemon，Daemon 解密后执行请求的操作，后返回数据给 Next（同样有加解密），然后 Next 再把请求给客户。&lt;/p&gt;
&lt;h1&gt;文件上传到 Next&lt;/h1&gt;
&lt;p&gt;首先第一步是将文件上传至 Next，前端方面我直接使用 Antd 的&lt;code&gt;&amp;lt;Upload /&amp;gt;&lt;/code&gt;组件，并通过自定义请求&lt;code&gt;customRequest&lt;/code&gt;附带上想要操作的实例和目标路径到 params。请求体使用 FormData：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;export function uploadFile(
  params: {
    id: number
    path: string
  },
  data: { file: File },
): Promise&amp;lt;&apos;success&apos;&amp;gt; {
  const formData = new FormData()
  formData.append(&apos;file&apos;, data.file)
  return request({
    url: &apos;/instance/file&apos;,
    method: &apos;post&apos;,
    params,
    data: formData,
  })
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在 Next 端，我使用的是 13 的 APP router，提供了解析 FormData 的方法：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;export async function POST(req: NextRequest) {
    const file = (await req.formData()).get(&apos;file&apos;) as File
    const fileName = file.name
    const bytes = await file.arrayBuffer()
    const buffer = Buffer.from(bytes)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;阅读&lt;a href=&quot;https://developer.mozilla.org/zh-CN/docs/Web/API/File&quot;&gt;File 的文档&lt;/a&gt;可知，File 其实是继承自&lt;a href=&quot;https://developer.mozilla.org/zh-CN/docs/Web/API/Blob&quot;&gt;Blob&lt;/a&gt;，而 Blob 的方法&lt;code&gt;arrayBuffer()&lt;/code&gt;则会返回一个 promise，其会兑现一个包含 Blob 所有内容的二进制格式的 ArrayBuffer。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Buffer.from()&lt;/code&gt;则是 node 的方法，它将一个 ArrayBuffer 以 Uint8Array 方式读取出来，这时候如果我们使用&lt;code&gt;console.log(buffer.toString())&lt;/code&gt;的话就可以在终端打印出来上传的文件的内容了（如果上传的是文本文件的话）。&lt;/p&gt;
&lt;h1&gt;加解密二进制&lt;/h1&gt;
&lt;p&gt;下一步就是把文件从 Next 加密后发去 Daemon，所以接下来就要研究如何加解密二进制了。&lt;/p&gt;
&lt;p&gt;CryptoJS 支持加密&lt;code&gt;WordArray&lt;/code&gt;类型的二进制数据，而这个 WordArray 其实就是存着 32-bit 数的数组，大概长这样：&lt;code&gt;[0x00010203, 0x04050607]&lt;/code&gt;。
如何将我们的 ArrayBuffer 转为 WordArray，我参考了&lt;a href=&quot;https://juejin.cn/post/6847902225784799246&quot;&gt;这篇文章&lt;/a&gt;所提供的方法：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const ArrayBufferToWordArray = (arrayBuffer: ArrayBuffer) =&amp;gt; {
  const u8 = new Uint8Array(arrayBuffer, 0, arrayBuffer.byteLength)
  const len = u8.length
  const words: number[] = []
  for (let i = 0; i &amp;lt; len; i += 1) {
    words[i &amp;gt;&amp;gt;&amp;gt; 2] |= (u8[i] &amp;amp; 0xff) &amp;lt;&amp;lt; (24 - (i % 4) * 8)
  }
  return CryptoJS.lib.WordArray.create(words, len)
}

const WordArrayToArrayBuffer = (wordArray: CryptoJS.lib.WordArray) =&amp;gt; {
  const { words } = wordArray
  const { sigBytes } = wordArray
  const u8 = new Uint8Array(sigBytes)
  for (let i = 0; i &amp;lt; sigBytes; i += 1) {
    const byte = (words[i &amp;gt;&amp;gt;&amp;gt; 2] &amp;gt;&amp;gt;&amp;gt; (24 - (i % 4) * 8)) &amp;amp; 0xff
    u8[i] = byte
  }
  return u8
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;那么，就可以进行一个加解密测试了：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const file = (await req.formData()).get(&apos;file&apos;) as File
const fileName = file.name
const bytes = await file.arrayBuffer()
const ArrayBufferToWordArray = (arrayBuffer: ArrayBuffer) =&amp;gt; {
  const u8 = new Uint8Array(arrayBuffer, 0, arrayBuffer.byteLength)
  const len = u8.length
  const words: number[] = []
  for (let i = 0; i &amp;lt; len; i += 1) {
    words[i &amp;gt;&amp;gt;&amp;gt; 2] |= (u8[i] &amp;amp; 0xff) &amp;lt;&amp;lt; (24 - (i % 4) * 8)
  }
  return CryptoJS.lib.WordArray.create(words, len)
}
const wordBuffer = ArrayBufferToWordArray(bytes)
const iv = CryptoJS.lib.WordArray.random(16)
const token =
  &apos;预设的Token&apos;
const encryptedData = CryptoJS.AES.encrypt(wordBuffer, token, {
  iv,
  mode: CryptoJS.mode.CBC,
  padding: CryptoJS.pad.Pkcs7,
})
console.log(&apos;en&apos;, encryptedData)
const decryptedData = CryptoJS.AES.decrypt(encryptedData, token, {
  iv,
  mode: CryptoJS.mode.CBC,
  padding: CryptoJS.pad.Pkcs7,
})
const WordArrayToArrayBuffer = (wordArray: CryptoJS.lib.WordArray) =&amp;gt; {
  const { words } = wordArray
  const { sigBytes } = wordArray
  const u8 = new Uint8Array(sigBytes)
  for (let i = 0; i &amp;lt; sigBytes; i += 1) {
    const byte = (words[i &amp;gt;&amp;gt;&amp;gt; 2] &amp;gt;&amp;gt;&amp;gt; (24 - (i % 4) * 8)) &amp;amp; 0xff
    u8[i] = byte
  }
  return u8
}
const decryptedArrayBuffer = WordArrayToArrayBuffer(decryptedData)
console.log(&apos;de&apos;, Buffer.from(decryptedArrayBuffer).toString())
fs.writeFileSync(&apos;./&apos; + fileName, decryptedArrayBuffer)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;经测试，上传的文本文件成功打印出解密的内容，上传的图片文件成功写入到指定位置且正常打开！二进制的加解密工作完成！&lt;/p&gt;
&lt;h1&gt;改写 axios 和 koa 中间件&lt;/h1&gt;
&lt;p&gt;上面的工作只是在 Next 本地测试了一下，要实际使用，需要改写 next 和 koa 的中间件。&lt;/p&gt;
&lt;p&gt;对于请求，把 ArrayBuffer 放在 contentBuffer 字段里：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;export async function POST(req: NextRequest) {
  const { request, instanceID, path } = await verify(req)
  const file = (await req.formData()).get(&apos;file&apos;) as File
  const fileName = file.name
  const bytes = await file.arrayBuffer()
  
  const res = await request.post(&apos;/file/upload&apos;, {
    path,
    id: instanceID,
    contentBuffer: bytes,
    fileName,
  })
  return response({ data: res })
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后在请求的中间件中，将&lt;code&gt;contentBuffer&lt;/code&gt;抽离出来单独加密，再在其他内容加密完成后再塞进去。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  request.interceptors.request.use((req) =&amp;gt; {
    let encryptedBuffer: string | undefined
    const iv = Crypto.lib.WordArray.random(16)
    if (
      req.data.contentBuffer &amp;amp;&amp;amp;
      req.data.contentBuffer instanceof ArrayBuffer
    ) { // 存在待加密的二进制，加密！
      const wordBuffer = ArrayBufferToWordArray(req.data.contentBuffer)
      encryptedBuffer = Crypto.AES.encrypt(wordBuffer, token, {
        iv,
        mode: Crypto.mode.CBC,
        padding: Crypto.pad.Pkcs7,
      }).toString() // 加密，并把 CipherParams 转为文本
      delete req.data.contentBuffer // 从请求体中删去
    }
    // 进行加密其他普通文本类数据的工作……
    req.data = { data: encryptedData, iv, encryptedBuffer } //把加密后的文本和加密后的二进制放进请求体中
    return req
  })
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;对于 Koa 的中间件，也要相应的进行处理：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;app.use(async (ctx, next) =&amp;gt; {
  if (ctx.method == &apos;POST&apos;) {
    try {
      // 获取请求体
      const encryptedBody = ctx.request.body as {
        data: string
        iv: CryptoJS.lib.WordArray
        encryptedBuffer?: string
      }

      let decryptedArrayBuffer: Uint8Array | undefined
      if (encryptedBody.encryptedBuffer) {
        // 解密二进制部分
        const decryptedWordArr = CryptoJS.AES.decrypt(
          encryptedBody.encryptedBuffer,
          config.token,
          {
            iv: encryptedBody.iv,
            mode: CryptoJS.mode.CBC,
            padding: CryptoJS.pad.Pkcs7,
          },
        )
        decryptedArrayBuffer = WordArrayToArrayBuffer(decryptedWordArr)
      }

      // 解密请求体
      const decryptedBodyStr = CryptoJS.AES.decrypt(
        encryptedBody.data,
        config.token,
        {
          iv: encryptedBody.iv,
          mode: CryptoJS.mode.CBC,
          padding: CryptoJS.pad.Pkcs7,
        },
      ).toString(CryptoJS.enc.Utf8)
      const decryptedBody = JSON.parse(decryptedBodyStr)
      if (decryptedBody._validate !== 114) {
        throw new Error()
      }

      // 将解密后的数据放回请求体中
      ctx.request.body = {
        ...decryptedBody,
        contentBuffer: decryptedArrayBuffer,
      }
      // 后续操作……
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后在 Koa 的相应路由中写上相应的把 buffer 写入对应地址的文件中，文件的上传就大功告成啦！&lt;br /&gt;
文件的下载也同理，这个就之后在写啦。&lt;/p&gt;
&lt;h1&gt;可以优化的地方&lt;/h1&gt;
&lt;p&gt;至此，虽然文件的上传可以正确无误的达到目标了，但是却代价巨大。为何？因为我们在 Next 直接把文件从客户端拿了上来并存在了 buffer 里，然后把这个 buffer 整个又发给了 koa，在测试中我们还发生了“413request entity too large”的问题，通过改 bodyParser 的配置解决了。但是 buffer 这种整存整取的方式对内存的消耗实在是太大了！试想，如果用户要把一个 10G 的压缩包（比如说地图很大的服务端）传上来，传输过程中 Next 和 Daemon 都要占用 10G 的内存！！！！这显然是代价巨大难以接受的。&lt;/p&gt;
&lt;p&gt;所以如果我完成毕设的基本功能后如果还有时间精力，需要对这个点进行一下优化，改为使用浏览器到Next相同的“流式传输”来节省内存。那么流式传输的对称加密也是在优化中要攻克的一个知识点了，想必届时我还会再写一篇文章罢！&lt;/p&gt;
</content:encoded></item><item><title>23年第50周——轮番面试；感冒来袭；以及疯狂的气候</title><link>https://yuzi.dev/posts/notes/week-50-of-2023-interviews-cold-and-crazy-weather/</link><guid isPermaLink="true">https://yuzi.dev/posts/notes/week-50-of-2023-interviews-cold-and-crazy-weather/</guid><description>记录 2023 年第 50 周的密集面试、突如其来的感冒，以及广东反常到离谱的天气。</description><pubDate>Fri, 15 Dec 2023 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;在这之前，我怎么也想不到这一周我会有 9 场面试。&lt;/p&gt;
&lt;p&gt;前几周在做毕设之余，又进行了几次海投，然后这一周就开始了三家公司的面试之旅。又逢图书馆的 IC 系统发生了爆炸和迁移，我的面试地点也经历了多次坎坷——这周的前半段面试基本上都是在上周约的，所以我还有充分的时间和机会捡漏图书馆一楼的会议室；后面是几乎电话来直接约当天或者第二天的，再加上周三图书馆的服务器就爆炸了 &lt;s&gt;（图书馆甚至直接摆烂说同学们不用签到了）&lt;/s&gt; 。最后选定了一个绝佳的面试地点：宿舍楼下的架空层，虽然有人来人往的路人，但是对我没啥影响。&lt;/p&gt;
&lt;p&gt;至于整个面试体验还是不错的，没有像字节那样抓底层原理那么恐怖，但是该有的技术问题包括八股都不少。到文章落笔为止已经有两家走完了终面的流程，希望下一周能有个好的结果吧！&lt;/p&gt;
&lt;p&gt;生活方面，这一周瞬间入夏，今天更是到达了史无前例的地步，在宿舍的室温竟然达到了 28.4 摄氏度！！某一个晚上甚至要靠风扇才能睡着！
&lt;img src=&quot;./room-temperature.webp&quot; alt=&quot;15 号下午的宿舍&quot; /&gt;
然后随之而来的却是大降温：
&lt;img src=&quot;./temperature-drop.jpg&quot; alt=&quot;众所周知，广东没有秋天&quot; /&gt;&lt;/p&gt;
&lt;p&gt;然后我在这周也成功的感冒了，周四开始早晚甚至还不停的咳嗽，又跑去医务室拿了点药，希望能够早点恢复。这周基本上毕设动都没动，一方面是面试太多，另一方面也是因为头昏脑胀不想思考。周末也想给自己放个假，好好的出去玩。&lt;/p&gt;
&lt;p&gt;今天中午和杰神去了体训楼新开的高级餐厅吃饭「大嘘」，菜品很少且略贵，不过风景不错！
&lt;img src=&quot;./lunch.webp&quot; alt=&quot;点了份鸡扒意面，非常的新鲜，非常的美味！&quot; /&gt;&lt;/p&gt;
&lt;p&gt;最后，希望希望下周能有好消息吧！
&lt;img src=&quot;./weekend-mood.jpg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
</content:encoded></item><item><title>在React中使用简单的状态管理</title><link>https://yuzi.dev/posts/frontend/simple-state-management-in-react/</link><guid isPermaLink="true">https://yuzi.dev/posts/frontend/simple-state-management-in-react/</guid><description>从一个多步骤引导流程出发，比较类、状态机和 reducer 等方式，讨论如何在 React 中管理简单但分支明确的状态。</description><pubDate>Sat, 02 Dec 2023 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;今天早上，我的毕设来到了这样一个需求：引导用户创建一个实例，而这个引导过程会根据用户的选择而有两种不同的路径共五种状态。所以如何比较优雅地管理这些状态便成了一个重要的问题。&lt;br /&gt;
将这五个状态以 0,1,2,3,4 来表示的话，第一条路径就是&lt;code&gt;[0,1,2,4]&lt;/code&gt;第二条就是&lt;code&gt;[0,2,3,4]&lt;/code&gt;（不考虑返回操作），如图：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./1674a28e.jpg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h1&gt;牛刀小试：使用类&lt;/h1&gt;
&lt;p&gt;对于状态机的需求，我第一个想到了使用一个类创建的对象来管理这些状态，话不多说就之间开干！&lt;br /&gt;
首先使用 enum 定义一下状态：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;export enum Steps {
  StartUp,
  ChooseTemplate,
  SetInstanceConfigure,
  UploadZip,
  Finish,
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;随后定义一个类来操纵状态：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;export class StepState {
  private current = 0
  private steps: Steps[] = [Steps.StartUp]
  setCreateType(createType: &apos;template&apos; | &apos;custom&apos;) {
    if (createType === &apos;template&apos;) {
      this.steps.push(
        Steps.ChooseTemplate,
        Steps.SetInstanceConfigure,
        Steps.Finish,
      )
    } else {
      this.steps.push(Steps.SetInstanceConfigure, Steps.UploadZip, Steps.Finish)
    }
    this.current = 1
  }
  get currentStep() {
    return this.steps[this.current]
  }
  next() {
    this.current++
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后扔进组件里：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;export default function QuickStart() {
  const stepState = new StepState()
  const currentStep = useMemo(
    () =&amp;gt; stepState.currentStep,
    [stepState.currentStep],
  )
  const handleSetCreateType = (type: &apos;template&apos; | &apos;custom&apos;) =&amp;gt; {
    stepState.setCreateType(type)
  }
  switch (currentStep) {
    case Steps.StartUp:
      return &amp;lt;StartUp handleSetCreateType={handleSetCreateType} /&amp;gt;
    case Steps.ChooseTemplate:
      return &amp;lt;div&amp;gt;选择模板&amp;lt;/div&amp;gt;
    case Steps.SetInstanceConfigure:
      return &amp;lt;div&amp;gt;设置实例配置&amp;lt;/div&amp;gt;
    default:
      return &amp;lt;div&amp;gt;未知错误&amp;lt;/div&amp;gt;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然而结果是，点击了按钮后并没有反应。&lt;/p&gt;
&lt;p&gt;为什么？因为这个数据并不是&lt;strong&gt;响应式&lt;/strong&gt;的，我们调用我们自己类的方法改变了对象的值，React 是不知道的（&lt;code&gt;useMemo&lt;/code&gt;,&lt;code&gt;useEffect&lt;/code&gt;的 dependencies 是只能监听响应式数据的，普通数据放进去照样监听不到），所以页面没有任何变化。&lt;/p&gt;
&lt;p&gt;所以解决方法的是转入 React 的响应式生态，才能被监听到。&lt;/p&gt;
&lt;h1&gt;&lt;code&gt;useState&lt;/code&gt;? &lt;code&gt;useImmer&lt;/code&gt;?&lt;/h1&gt;
&lt;p&gt;最直接的方法是放进 useState 中，这样数据就是响应式的了，但是有一个问题是，useState 更新数据时是替换，如果我们使用解构赋值进行浅拷贝的话，类的方法就会尽数丢失。当然我们也可以用手动管理原型链的方法进行完全拷贝，但是感觉又不够优雅。&lt;/p&gt;
&lt;p&gt;这时，我想到了 React 官方教程里的库&lt;code&gt;Immer&lt;/code&gt;，它类似 Vue3，使用&lt;code&gt;Proxy&lt;/code&gt;来进行代理原始数据，使得我们达到可以直接‘修改’数据而保持响应式的效果。那么，我把&lt;code&gt;useImmer&lt;/code&gt;套在我创建的对象中如何呢？&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const [stepState, updateStepState] = useImmer(new StepState())
...
const handleSetCreateType = (type: &apos;template&apos; | &apos;custom&apos;) =&amp;gt; {
  updateStepState((draft) =&amp;gt; {
    draft.setCreateType(type)
  })
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后直接来了个报错：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./b52f205d.webp&quot; alt=&quot;&quot; /&gt;
通过报错可以看到，对于我们这种复杂的有函数的对象，Immer 也是无能为力的。&lt;/p&gt;
&lt;h1&gt;&lt;code&gt;useReducer&lt;/code&gt;和&lt;code&gt;useImmerReducer&lt;/code&gt;&lt;/h1&gt;
&lt;p&gt;在&lt;code&gt;use-immer&lt;/code&gt;库的介绍页，它的下方还介绍了另一个 hook：&lt;code&gt;useImmerReducer&lt;/code&gt;，我一看，这用法不是和 vuex 很像吗？它写着它基于&lt;code&gt;useReducer&lt;/code&gt;这个 React hook，立马翻开&lt;a href=&quot;https://react.dev/reference/react/useReducer&quot;&gt;官方文档&lt;/a&gt;查看。&lt;/p&gt;
&lt;p&gt;简而言之，这就是 React 提供的一个可以用于状态管理的短小精悍的 hook，而我在学习 React 时居然漏掉或者说是跳过了它！&lt;br /&gt;
它的简单用法如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import { useReducer } from &apos;react&apos;;

function reducer(state, action) {
  if (action.type === &apos;incremented_age&apos;) {
    return {
      age: state.age + 1
    };
  }
  throw Error(&apos;Unknown action.&apos;);
}

export default function Counter() {
  const [state, dispatch] = useReducer(reducer, { age: 42 });

  return (
    &amp;lt;&amp;gt;
      &amp;lt;button onClick={() =&amp;gt; {
        dispatch({ type: &apos;incremented_age&apos; })
      }}&amp;gt;
        Increment age
      &amp;lt;/button&amp;gt;
      &amp;lt;p&amp;gt;Hello! You are {state.age}.&amp;lt;/p&amp;gt;
    &amp;lt;/&amp;gt;
  );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;reducer 是一个函数，接受两个参数 state 和 action，state 是在调用 dispatch 时自动传入，action 则是 dispatch 的参数，他的返回值即作为新的 state。&lt;/p&gt;
&lt;p&gt;但是要注意，reducer 传进来的 state 是&lt;strong&gt;只读的&lt;/strong&gt;，正如同它的名字“切片”和 React 的不可变哲学一样，reducer 必须返回一个新的对象作为新的 state 切片！&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function reducer(state, action) {
  if (action.type === &apos;incremented_age&apos;) {
    state.age++;
  }
  return state;      //错误示例，这样什么也不会发生！数据不会变化。
  throw Error(&apos;Unknown action.&apos;);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;那么上文提到的&lt;code&gt;useImmerReducer&lt;/code&gt;就是来解决这个问题的，利用 Proxy，reducer 可以直接修改 state（在 immer 里叫 draft）的属性的值，而不必返回新切片（immer 帮我们做了），所以我们可以重构我们的代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;export interface StepState {
  current: number
  steps: Steps[]
}

function stepReducer(
  draft: StepState,
  action:
    | { type: &apos;setCreateType&apos;; payload: &apos;template&apos; | &apos;custom&apos; }
    | { type: &apos;next&apos; },
) {
  switch (action.type) {
    case &apos;setCreateType&apos;:
      if (action.payload === &apos;template&apos;) {
        draft.steps.push(
          Steps.ChooseTemplate,
          Steps.SetInstanceConfigure,
          Steps.Finish,
        )
      } else {
        draft.steps.push(
          Steps.SetInstanceConfigure,
          Steps.UploadZip,
          Steps.Finish,
        )
      }
      return void draft.current++
    case &apos;next&apos;:
      return void draft.current++
    default:
      throw new Error(&apos;未知类型&apos;)
  }
}

export default function QuickStart() {
  const [stepState, stepDispatch] = useImmerReducer(stepReducer, {
    current: 0,
    steps: [Steps.StartUp],
  } as StepState)
  const currentStep = useMemo(
    () =&amp;gt; stepState.steps[stepState.current],
    [stepState],
  )
  const handleSetCreateType = (type: &apos;template&apos; | &apos;custom&apos;) =&amp;gt; {
    stepDispatch({ type: &apos;setCreateType&apos;, payload: type })
  }
  switch (currentStep) {
    case Steps.StartUp:
      return &amp;lt;StartUp handleSetCreateType={handleSetCreateType} /&amp;gt;
    case Steps.ChooseTemplate:
      return &amp;lt;div&amp;gt;选择模板&amp;lt;/div&amp;gt;
    case Steps.SetInstanceConfigure:
      return &amp;lt;div&amp;gt;设置实例配置&amp;lt;/div&amp;gt;
    default:
      return &amp;lt;div&amp;gt;未知错误&amp;lt;/div&amp;gt;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样，问题完美解决。&lt;/p&gt;
&lt;h1&gt;总结&lt;/h1&gt;
&lt;p&gt;回顾整个问题解决的步骤，我学到了很多。首先我有把代码写得优雅而不是乱糊的意识，值得肯定。但是我在想解决思路的时候没有意识到React的响应式特性导致第一次写出了“使用类控制”的无法使用的结构。好在最后还是回到文档查找到了最合适的解决方案，这个切片的思想值得我牢记！&lt;/p&gt;
</content:encoded></item><item><title>写代码的中庸之道</title><link>https://yuzi.dev/posts/notes/the-middle-way-of-writing-code/</link><guid isPermaLink="true">https://yuzi.dev/posts/notes/the-middle-way-of-writing-code/</guid><description>记录一次在毕业设计开发中的取舍：从追求完美实现，转向围绕核心目标做更务实的技术决策。</description><pubDate>Fri, 24 Nov 2023 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;这周真是跌宕起伏的一周。周一晚接到的百度面试官的邮件，告诉我评估通过，我也在期待中在周二等到了 HR 的 OC。可惜好景不长，仅仅 24 小时之后的周三下午她便再次打来电话说，因为我的学历（是双非）的关系，我的 offer 估计要被取消了。说实话我其实并没有太大的波动，因为上周我就已经接受自己评估失败的假设了，没有气馁过多，我便做出了考研的决定。当然最后还是在权衡下决定目前最优先全身心投入做毕设，争取今年做完、24 年开始准备春招，2 月开始春招考研两手抓，并随着时间推移逐渐扩大重心到考研上。emmm，算是我的总结与规划了吧。&lt;/p&gt;
&lt;p&gt;这周我便开启了泡图书馆模式，推进我那基于 Next 的毕设。事实证明我选择 next 没有错，对于一个人开发而言，一套语言（TS）搞定前端后端数据库是很不错的开发体验，我甚至可以一个 interface 同时给前后端接口用。因为 Next 生态较新，且集中在国外，所以我也不得不对着英文文档学习。&lt;/p&gt;
&lt;p&gt;这篇文章为何要起这样一个标题呢？结合我的经历导致目标的变化，我也学会及时转变我的代码方式，这个转变大部分受到了来自阿杰的影响。我之前写代码是有些“学术”的，比如写 TS，力求完美的类型标注、力求最新的生态和技术栈。这周开发的一个阻塞点也是因此而起的，我的操作数据库的框架是 Sequelize，它目前的稳定版本是 6，但这个版本的类型支持不是很好，而他的版本 7 在 Alpha 阶段，利用了 TS 的注解使得类型支持又好又简洁，所以我就直接上 7 了。&lt;/p&gt;
&lt;p&gt;这是背景，而我想要实现的功能是通过邮件登录，需要实现这个功能，Next-Auth 需要接入数据库的支持，恰好它支持使用 Sequelize，对此还专门出了 adapter 来进行调用。但不幸的是，这个 adapter 只支持版本 6。这时就有多个选择：退回 6；或使用另一种框架例如 Prisma，但 Prisma 貌似不是纯 TS；或者弃用 next-auth 转而使用 clerk。我纠结了很久，因为每个方案都要对项目进行大动干戈，拖慢我的进度。但最后我想通了，这个问题的根源不就是要搞邮箱登录的功能吗？邮箱登录是我这个毕设的核心功能吗？不是。是必须功能吗？也不是，因为已经有密码注册和登录了。所以我最后决定，直接砍掉这个，直奔我的核心功能！所以中庸之道是：&lt;strong&gt;既要长远地、学术的写代码，也要不能死磕细节。对于不同需求要有不同的态度，分清轻重缓急，确保按时达成既定的目标。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;在写这篇文章的时候，我又想到了另一种实现方式。之所以前面的难做，是因为我想使用next-auth提供的邮箱登陆能力，而这个能力对现有系统的侵入太大了，甚至出现了不兼容的情况。既然这样，我自己实现一个不就好了吗？不过不是用在登录，而是用在忘记密码的找回！当然，这也是在核心功能做完了之后再考虑的锦上添花的需求。&lt;/p&gt;
</content:encoded></item><item><title>Linux休眠后立即被唤醒的解决方案</title><link>https://yuzi.dev/posts/tinkering/linux-suspend-wakes-up-immediately/</link><guid isPermaLink="true">https://yuzi.dev/posts/tinkering/linux-suspend-wakes-up-immediately/</guid><description>通过检查 /proc/acpi/wakeup、编写脚本并配合 systemd 服务，解决 Linux 休眠后立即被唤醒的问题。</description><pubDate>Mon, 20 Nov 2023 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;参考了&lt;a href=&quot;https://www.shuyz.com/posts/fix-linux-wakeup-right-after-suspend/&quot;&gt;这个帖子&lt;/a&gt;的解决方法。&lt;/p&gt;
&lt;p&gt;我的笔记本症状和其中一样，在休眠过后立即被唤醒，导致休眠无法使用。&lt;/p&gt;
&lt;p&gt;能够将笔记本从休眠状态唤醒的事件定义在 &lt;code&gt;/proc/acpi/wakeup&lt;/code&gt; 这个文件里，只要将无关的事件禁用，就可以查出是哪个事件唤醒系统了。
禁用或启用某个事件可以用开关控制：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;echo IGBE | sudo tee /proc/acpi/wakeup
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;所以可以撰写一个脚本 &lt;code&gt;suspend_event.sh&lt;/code&gt;（脚本来自原作者）：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#!/bin/sh
# @Author: lance
# @Date:   2015-10-07 22:38:51
# @Last Modified by:   lance
# @Last Modified time: 2015-10-07 22:42:21
#
# Some events will wakeup right after suspend, disable them
stat=$(cat /proc/acpi/wakeup)
wakers=(IGBE EXP2 XHCI EHC1)
for waker in ${wakers[@]}; do
    is_en=$(echo &quot;${stat}&quot; | grep $waker | grep disabled);
    if  [ -z &quot;$is_en&quot; ]; then
        echo disable wakeup of $waker...
        echo $waker | tee /proc/acpi/wakeup;
    fi
done
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;保存在一个地方，比如我放在了家目录下，给予执行权限，这里我直接 &lt;code&gt;chmod 777 ./suspend_event.sh&lt;/code&gt; 了。&lt;/p&gt;
&lt;p&gt;再编写一个 systemd 项目，让其开机启动：&lt;/p&gt;
&lt;p&gt;&lt;code&gt;sudo vim /etc/systemd/system/suspend_event.service&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[Unit]
Description=disable some events to wake up device
After=systemd-udev-settle.service

[Service]
Type=idle
ExecStart=/home/yuzi/suspend_event.sh
RemainAfterExit=no

[Install]
WantedBy=multi-user.target
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;最后执行 &lt;code&gt;sudo systemctl enable --now suspend_event.service&lt;/code&gt; 即可，这下休眠就正常了。&lt;/p&gt;
</content:encoded></item><item><title>2023第46周——被面试推着走</title><link>https://yuzi.dev/posts/notes/week-46-of-2023-pushed-by-interviews/</link><guid isPermaLink="true">https://yuzi.dev/posts/notes/week-46-of-2023-pushed-by-interviews/</guid><description>记录 2023 第 46 周，被三场面试推着走的一周，以及对底层原理学习的进一步决心。</description><pubDate>Fri, 10 Nov 2023 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;这一周经历了三次面试，充实着不像一周，被推着走一样。加上发生了其他的事情，让人感觉真是不同寻常的一周呢。&lt;/p&gt;
&lt;p&gt;周一和situ都参加了字节的面试，前面的八股部分我侃侃而谈，但是到了最后手撕，由于对闭包和高阶函数的不熟悉，导致我花了半天，经过面试官的多次引导下才勉强做了出来。当时想着估计没了。而聪神的字节加面在他面完的半个小时就有HR告知通过了，羡慕已经说累了。&lt;/p&gt;
&lt;p&gt;随后超星约面了周三的一面，在早早爬起来在架空层准备好面试后，我发现在腾讯会议里有好几个人，这种多面一让我有种不好的预感。果然，全程面试官是不停的八股拷问，把各种奇奇怪怪的题扔上来让我回答（比如各种死扣细节、各种布局啊什么什么的），感觉是想考倒我为止。面到最后，我感觉我整场一道题也没回答正确，随后就草草结束了。&lt;/p&gt;
&lt;p&gt;超星的面试结束后，字节的HR就来约二面了，然而周三这天正好原神4.2更新了，于是为了避免剧透，我一口气肝完了主线和传说任务。这次剧情给我留下的后劲还是挺大的。&lt;/p&gt;
&lt;p&gt;周四的字节二面，让我明显感受到了一种不同的感觉，我像往常一样介绍着我在实习的时候的项目，它基于乾坤微应用框架，面试官就抛出了一个独到的问题：“后端返回了菜单路由，和对应的组件路径，如果前端把组件路径改了，后端如何得知？”等诸如此类的开放性问题，问得十分地有水准，有深度。这位面试官十分喜欢深挖原理而不是注重表面的API，和超星的面试形成了鲜明的对比。最后的手撕一个带限制的Promise.all，其实原理恰好和8月那次字节一面的原理遥相呼应，所以比较顺利的A出来了。&lt;/p&gt;
&lt;p&gt;经过这次面试，我确实觉得要深入底层，了解源码、原理是一件很重要的事，所以我也准备开始系统性地学习相关的课程。&lt;/p&gt;
&lt;p&gt;百度的HR说下周开奖，我们部门的薪资方案看样子已经出了，静待好消息……&lt;/p&gt;
&lt;p&gt;今天（11.10 周五）晚上决定做个新发型，花了300大洋进行了一个烫，效果还挺不错的，比去年北亭那家好多了。&lt;/p&gt;
</content:encoded></item><item><title>【力扣117】填充每个节点的下一个右侧节点指针 II</title><link>https://yuzi.dev/posts/course-notes/leetcode-117-populating-next-right-pointers-ii/</link><guid isPermaLink="true">https://yuzi.dev/posts/course-notes/leetcode-117-populating-next-right-pointers-ii/</guid><description>记录力扣 117 的解题过程，分析一次错误思路，并给出修正后的广度优先遍历解法。</description><pubDate>Fri, 03 Nov 2023 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;题目&lt;/h1&gt;
&lt;p&gt;给定一个二叉树：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;struct Node {
  int val;
  Node *left;
  Node *right;
  Node *next;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;填充它的每个 next 指针，让这个指针指向其下一个右侧节点。如果找不到下一个右侧节点，则将 next 指针设置为 NULL。&lt;br /&gt;
初始状态下，所有 next 指针都被设置为 NULL。&lt;/p&gt;
&lt;p&gt;示例 1：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./example.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;输入：root = [1,2,3,4,5,null,7]&lt;br /&gt;
输出：[1,#,2,3,#,4,5,7,#]&lt;br /&gt;
解释：给定二叉树如图 A 所示，你的函数应该填充它的每个 next 指针，以指向其下一个右侧节点，如图 B 所示。序列化输出按层序遍历顺序（由 next 指针连接），&apos;#&apos; 表示每层的末尾。&lt;/p&gt;
&lt;p&gt;示例 2：&lt;br /&gt;
输入：root = []
输出：[]&lt;/p&gt;
&lt;h1&gt;解答&lt;/h1&gt;
&lt;p&gt;我采用广度优先遍历，初始队列为&lt;code&gt;[root]&lt;/code&gt;，在拿到一个节点的情况下，进行分类讨论：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;如果没有孩子：直接&lt;code&gt;continue&lt;/code&gt;跳过&lt;/li&gt;
&lt;li&gt;如果左右都有孩子，让左孩子的 next 指向右孩子&lt;/li&gt;
&lt;li&gt;定义 start 作为上图 5-&amp;gt;7 这种情况的起点，如果右存在则是右，否则是左孩子。&lt;/li&gt;
&lt;li&gt;定义 end 为终点，如果该节点没有 next，则 end 为 null，否则是 next 的左孩子，否则是 next 的右孩子。&lt;/li&gt;
&lt;li&gt;将孩子 push 进队列，继续循环。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;按照以上的思路，就有如下代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function connect(root: Node | null): Node | null {
  if (!root) return null;
  const queue = [root]
  while (queue.length) {
    const node: Node = queue.shift() as Node;
    if (!(node.left || node.right)) continue
    if (node.left &amp;amp;&amp;amp; node.right) node.left.next = node.right
    const start = node.right || node.left as Node
    const end = node.next ? (node.next.left || node.next.right) : null
    start.next = end
    node.left &amp;amp;&amp;amp; queue.push(node.left)
    node.right &amp;amp;&amp;amp; queue.push(node.right)
  }
  return root
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;提交后，有 34 / 55 个通过的测试用例，而失败的是这个用例：&lt;br /&gt;
&lt;code&gt;[1,2,3,4,5,null,6,7,null,null,null,null,8]&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./failed-case.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;画出这棵树的图可知，我们只考虑了&lt;code&gt;node.next&lt;/code&gt;，没有考虑&lt;code&gt;node.next&lt;/code&gt;没有孩子而&lt;code&gt;node.next.next&lt;/code&gt;有孩子的情况，造成了最后一行没有连起来。&lt;/p&gt;
&lt;p&gt;加上判断后，代码顺利通过～&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function connect(root: Node | null): Node | null {
  if (!root) return null;
  const queue = [root]
  while (queue.length) {
    const node: Node = queue.shift() as Node;
    if (!(node.left || node.right)) continue
    if (node.left &amp;amp;&amp;amp; node.right) node.left.next = node.right
    const start = node.right || node.left as Node
    let nextWithChild = node.next
    while (nextWithChild) {
      if (nextWithChild.left || nextWithChild.right) break
      nextWithChild = nextWithChild.next
    }
    const end = nextWithChild ? (nextWithChild.left || nextWithChild.right) : null
    start.next = end
    node.left &amp;amp;&amp;amp; queue.push(node.left)
    node.right &amp;amp;&amp;amp; queue.push(node.right)
  }
  return root
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;./accepted.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
</content:encoded></item><item><title>被考察很多次的发布订阅模式</title><link>https://yuzi.dev/posts/frontend/frequently-asked-pub-sub-pattern/</link><guid isPermaLink="true">https://yuzi.dev/posts/frontend/frequently-asked-pub-sub-pattern/</guid><description>梳理发布订阅模式与观察者模式的区别，并手写一个简单的 PubSub 与 EventEmitter 实现。</description><pubDate>Thu, 02 Nov 2023 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;在观察大佬们的面经时，常常提起“发布订阅模式”、“手写 EventEmitter”，遂学习，记录于此，对&lt;a href=&quot;https://juejin.cn/post/6978728619782701087&quot;&gt;这篇文章&lt;/a&gt;有大量~~（搬运）~~参考。&lt;/p&gt;
&lt;h1&gt;介绍&lt;/h1&gt;
&lt;p&gt;发布订阅模式其实是一种对象间一对多的依赖关系，当一个对象的状态发生改变时，所有依赖于它的对象都将得到状态改变的通知。&lt;/p&gt;
&lt;p&gt;发布订阅模式中，包含发布者，事件调度中心，订阅者三个角色。发布者和订阅者是松散耦合的，互不关心对方是否存在，他们关注的是事件本身。发布者借用事件调度中心提供的&lt;code&gt;publish&lt;/code&gt;方法发布事件，而订阅者则通过&lt;code&gt;subscribe&lt;/code&gt;进行订阅。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;特点&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;发布订阅模式中，对于发布者&lt;code&gt;Publisher&lt;/code&gt;和订阅者&lt;code&gt;Subscriber&lt;/code&gt;没有特殊的约束，他们好似是匿名活动，借助事件调度中心提供的接口发布和订阅事件，互不了解对方是谁。&lt;/li&gt;
&lt;li&gt;松散耦合，灵活度高，常用作事件总线。&lt;/li&gt;
&lt;li&gt;易理解，可类比于&lt;code&gt;DOM&lt;/code&gt;事件中的&lt;code&gt;dispatchEvent&lt;/code&gt;和&lt;code&gt;addEventListener&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;缺点&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;当事件类型越来越多时，难以维护，需要考虑事件命名的规范，也要防范数据流混乱。&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;和观察者模式的区别&lt;/h1&gt;
&lt;p&gt;定性上：观察者是经典软件&lt;code&gt;设计模式&lt;/code&gt;中的一种，但发布订阅只是软件架构中的一种&lt;code&gt;消息范式&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;组成上：观察者模式本身只需要&lt;code&gt;2个&lt;/code&gt;角色便可成型，即&lt;code&gt;观察者&lt;/code&gt;和&lt;code&gt;被观察者&lt;/code&gt;，其中&lt;code&gt;被观察者&lt;/code&gt;是重点。而发布订阅需要至少&lt;code&gt;3个&lt;/code&gt;角色来组成，包括&lt;code&gt;发布者&lt;/code&gt;、&lt;code&gt;订阅者&lt;/code&gt;和&lt;code&gt;发布订阅中心&lt;/code&gt;，其中&lt;code&gt;发布订阅中心&lt;/code&gt;是重点。&lt;/p&gt;
&lt;p&gt;实现上：&lt;/p&gt;
&lt;p&gt;观察者模式一般至少有一个可被观察的对象 Subject，可以有多个&lt;code&gt;观察者&lt;/code&gt;去观察这个对象。二者的关系是通过&lt;code&gt;被观察者主动&lt;/code&gt;建立的，&lt;code&gt;被观察者&lt;/code&gt;至少要有三个方法——添加观察者、移除观察者、通知观察者。&lt;/p&gt;
&lt;p&gt;当被观察者将某个观察者添加到自己的&lt;code&gt;观察者列表&lt;/code&gt;后，观察者与被观察者的关联就建立起来了。此后只要被观察者在某种时机触发&lt;code&gt;通知观察者&lt;/code&gt;方法时，观察者即可接收到来自被观察者的消息。&lt;/p&gt;
&lt;p&gt;被观察者是要有一个数组来容纳所有的观察者！&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;与观察者模式相比，发布订阅核心基于一个中心来建立整个体系。其中发布者和订阅者不直接进行通信，而是发布者将要发布的消息交由中心管理，订阅者也是根据自己的情况，按需订阅中心中的消息。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image1.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;关联：发布订阅的实现内部利用了&lt;code&gt;观察者模式&lt;/code&gt;，&lt;code&gt;订阅者&lt;/code&gt;和&lt;code&gt;发布订阅中心&lt;/code&gt;的关系类似&lt;code&gt;观察者&lt;/code&gt;和&lt;code&gt;被观察者&lt;/code&gt;的关系。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h1&gt;简单实现&lt;/h1&gt;
&lt;pre&gt;&lt;code&gt;class PubSub {
    constructor() {
        // 维护事件及订阅行为
        this.events = {}
    }
    /**
     * 注册事件订阅行为
     * @param {String} type 事件类型
     * @param {Function} callback 回调函数
     */
    subscribe(type, callback) {
        if (!this.events[type]) {
            this.events[type] = []
        }
        this.events[type].push(callback)
    }
    /**
     * 发布事件
     * @param {String} type 事件类型
     * @param  {...any} args 参数列表
     */
    publish(type, ...args) {
        if (this.events[type]) {
            this.events[type].forEach(callback =&amp;gt; {
                callback(...args)
            })
        }
    }
    /**
     * 移除某个事件的一个订阅行为
     * @param {String} type 事件类型
     * @param {Function} callback 回调函数
     */
    unsubscribe(type, callback) {
        if (this.events[type]) {
            const targetIndex = this.events[type].findIndex(item =&amp;gt; item === callback)
            if (targetIndex !== -1) {
                this.events[type].splice(targetIndex, 1)
            }
            if (this.events[type].length === 0) {
                delete this.events[type]
            }
        }
    }
    /**
     * 移除某个事件的所有订阅行为
     * @param {String} type 事件类型
     */
    unsubscribeAll(type) {
        if (this.events[type]) {
            delete this.events[type]
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;手写 EventEmitter&lt;/h1&gt;
&lt;p&gt;&lt;code&gt;EventEmitter&lt;/code&gt; （事件派发器）是 &lt;code&gt;Node.js&lt;/code&gt; 的核心模块 &lt;code&gt;events&lt;/code&gt; 中的类，用于对 &lt;code&gt;Node.js&lt;/code&gt; 中的事件进行统一管理，用 &lt;code&gt;events&lt;/code&gt; 特定的 &lt;code&gt;API&lt;/code&gt; 对事件进行添加、触发和移除等等，&lt;code&gt;EventEmitter&lt;/code&gt; 的核心就是事件触发与事件监听器功能的封装。&lt;/p&gt;
&lt;p&gt;简而言之，&lt;code&gt;EventEmitter&lt;/code&gt;就是一个典型的发布订阅模式，实现了事件调度中心。&lt;/p&gt;
&lt;p&gt;实现：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class EventEmitter {
    constructor() {
        // 维护事件及监听者
        this.listeners = {}
    }
    /**
     * 注册事件监听者
     * @param {String} type 事件类型
     * @param {Function} callback 回调函数
     */
    on(type, callback) {
        if (!this.listeners[type]) {  // 如果该事件类型不存在
            this.listeners[type] = [] // 为该事件类型设置数组，存放回调函数
        }
        this.listeners[type].push(callback) // 将回调函数放入该事件类型数组
    }
    /**
     * 发布事件
     * @param {String} type 事件类型
     * @param  {...any} args 参数列表，把 emit 传递的参数赋给回调函数
     */
    emit(type, ...args) {
        if (this.listeners[type]) { // 如果该事件类型存在
            this.listeners[type].forEach(callback =&amp;gt; {
                callback(...args) // 调用该事件类型数组中的每一个回调函数，并传入参数
            })
        }
    }
    /**
     * 移除某个事件的一个监听者
     * @param {String} type 事件类型
     * @param {Function} callback 回调函数
     */
    off(type, callback) {
        if (this.listeners[type]) {
            // 查询传入回调函数在该事件类型数组中的下标，并将其下标用 targetIndex 存储
            const targetIndex = this.listeners[type].findIndex(item =&amp;gt; item === callback)
            if (targetIndex !== -1) { // 说明该回调函数存在于事件类型数组中
                this.listeners[type].splice(targetIndex, 1) // 删除该回调函数
            }
            if (this.listeners[type].length === 0) { // 该事件类型数组为空
                delete this.listeners[type] // 删除该事件类型
            }
        }
    }
    /**
     * 移除某个事件的所有监听者
     * @param {String} type 事件类型
     */
    offAll(type) {
        if (this.listeners[type]) { // 如果该事件类型数组存在
            delete this.listeners[type] // 直接删除该事件类型
        }
    }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使用：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 创建事件管理器实例
const ee = new EventEmitter()

// 注册一个imagine事件监听者
ee.on(&apos;imagine&apos;, function() { console.log(&apos;前端收割机&apos;) })

// 发布事件imagine
ee.emit(&apos;imagine&apos;)
// 前端收割机

// 也可以emit传递参数
ee.on(&apos;imagine&apos;, function(name,address) { console.log(`大家好，我是${name},我来自${address}！`) })
ee.emit(&apos;imagine&apos;, &apos;前端收割机&apos;,&apos;广东&apos;) // 此时会打印两条信息，因为前面注册了两个imagine事件的监听者
// 前端收割机
// 大家好，我是前端收割机，我来自广东！

// 测试移除事件监听
const BeRemovedListener = function() { console.log(&apos;我是一个可以被移除的监听者&apos;) }

// 注册一个TestOff事件监听者
ee.on(&apos;TestOff&apos;, BeRemovedListener)

// 发布事件TestOff
ee.emit(&apos;TestOff&apos;)
// 我是一个可以被移除的监听者

// 移除事件监听
ee.off(&apos;TestOff&apos;, BeRemovedListener)
ee.emit(&apos;TestOff&apos;) // 此时事件监听已经被移除，不会再有console.log打印出来了

// 测试移除imagine的所有事件监听
ee.offAll(&apos;imagine&apos;)
console.log(ee) // 此时可以看到ee.listeners已经变成空对象了，再emit发送imagine事件也不会有反应了

&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>Hello Shiro!</title><link>https://yuzi.dev/posts/notes/hello-shiro/</link><guid isPermaLink="true">https://yuzi.dev/posts/notes/hello-shiro/</guid><pubDate>Tue, 31 Oct 2023 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;你好，Shiro！&lt;/p&gt;
&lt;p&gt;是新的个人主页 + 博客了，旧的（本站）仍然会继续使用。&lt;/p&gt;
&lt;p&gt;新的博客（基于 mix-space+Shiro）：https://yuzi.dev&lt;/p&gt;
&lt;p&gt;旧的博客（基于 hexo+particleX）：https://blog.yuzi.dev&lt;/p&gt;
&lt;p&gt;文章会陆续迁移～, 旧的博客应该会继续与新博客同步更新（直到我懒得维护为止）。&lt;/p&gt;
</content:encoded></item><item><title>快速启动一个Nginx的https服务器</title><link>https://yuzi.dev/posts/tinkering/quickly-start-an-nginx-https-server/</link><guid isPermaLink="true">https://yuzi.dev/posts/tinkering/quickly-start-an-nginx-https-server/</guid><description>使用 Docker 启动 Nginx，结合 acme.sh 与 Cloudflare DNS 快速配置一个可用的 HTTPS 反向代理服务。</description><pubDate>Mon, 30 Oct 2023 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;因为我的域名“yuzi.dev”是强制开启了 HSTS 的 &lt;code&gt;dev&lt;/code&gt; 域名，所以时常需要面对 HTTPS 问题，故以此文来记录这个过程。&lt;/p&gt;
&lt;h1&gt;启动 Nginx&lt;/h1&gt;
&lt;p&gt;这里使用 docker 启动，docker 安装不再赘述。
创建一个文件夹，以～/nginx 为例，在其中创建&lt;code&gt;docker-compose.yml&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;version: &quot;3.8&quot;
services:
  nginx:
    image: nginx:stable
    container_name: nginx-web
    hostname: nginx-web
    restart: always
    ports:
      - 23333:23333  #此处填写想要暴露的端口
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf
      - ./cert:/etc/nginx/cert
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;随后在同一目录创建&lt;code&gt;nginx.conf&lt;/code&gt;，内容如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;worker_processes 1;

events {
  worker_connections 1024;
}

http {
  #http 全局块
  include mime.types;
  default_type application/octet-stream;
  sendfile on;

  server {
    listen 23333;  #要暴露的地址
    server_name server.blog.yuzi.dev;

    #增加 ssl
    ssl on;
    ssl_certificate cert/cert.crt;
    ssl_certificate_key cert/cert.key;

    ssl_session_cache shared:SSL:1m;
    ssl_session_timeout 5m;

    ssl_protocols SSLv2 SSLv3 TLSv1.2;

    ssl_ciphers HIGH:!aNULL:!MD5;
    ssl_prefer_server_ciphers on;
    location / {
      proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
      proxy_set_header X-Forwarded-Proto $scheme;
      proxy_set_header Host $http_host;
      proxy_set_header X-Real-IP $remote_addr;
      proxy_set_header Range $http_range;
      proxy_set_header If-Range $http_if_range;
      proxy_redirect off;
      proxy_pass http://172.17.0.1:2333;  #转发到的地址，一般容器内访问主机的地址是 172.17.0.1
      # the max size of file to upload
      client_max_body_size 20000m;
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果要转发多个地址，视情况复制 &lt;code&gt;server&lt;/code&gt; 块（不同的端口）或不同的 &lt;code&gt;location&lt;/code&gt;（覆写路径）。&lt;/p&gt;
&lt;h1&gt;申请证书，以 CloudFlare 为例&lt;/h1&gt;
&lt;p&gt;我们使用 acme.sh 来申请，一键安装：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;curl  https://get.acme.sh | sh
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这会把 acme 安装到家目录的 &lt;code&gt;.acme.sh&lt;/code&gt; 文件夹中。
安装好后，&lt;code&gt;cd ~/.acme.sh&lt;/code&gt; 进入目录，随后在环境变量中填入 Cloudflare 的 API Key。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;export CF_Key=&quot;cloudflare中查看你的APIkey&quot;
export CF_Email=&quot;你的邮箱&quot;
./acme.sh  --issue  --dns dns_cf -d 你的域名
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;安装证书并重启 nginx&lt;/h1&gt;
&lt;pre&gt;&lt;code&gt;./acme.sh --installcert -d 你的域名 --keypath ./cert/cert.key  --fullchainpath ./cert/cert.crt
mv ./cert ~/nginx
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;随后重启，测试是否已经启用 HTTPS。&lt;/p&gt;
</content:encoded></item><item><title>给主题添加黑暗模式，以及白屏和首屏问题</title><link>https://yuzi.dev/posts/frontend/adding-dark-mode-and-fixing-white-screen/</link><guid isPermaLink="true">https://yuzi.dev/posts/frontend/adding-dark-mode-and-fixing-white-screen/</guid><description>之前学习了一下TailwindCSS，在它的黑暗模式页里详细地列出了黑暗模式的实现方式，深以为然，于是也想给自己的博客所使用的主题搞一个。</description><pubDate>Thu, 07 Sep 2023 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;之前学习了一下TailwindCSS，在它的&lt;a href=&quot;https://tailwindcss.com/docs/dark-mode&quot;&gt;黑暗模式页&lt;/a&gt;里详细地列出了黑暗模式的实现方式，深以为然，于是也想给自己的博客所使用的主题搞一个。&lt;/p&gt;
&lt;h1&gt;思路&lt;/h1&gt;
&lt;p&gt;首先一般有个按钮可以切换当前的主题，主题有三个状态：黑暗、浅色、跟随系统。&lt;/p&gt;
&lt;p&gt;如何检测系统的主题？使用这个方法：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;window.matchMedia(&apos;(prefers-color-scheme: dark)&apos;).matches)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;返回值为&lt;code&gt;true&lt;/code&gt;即为黑暗模式，&lt;code&gt;false&lt;/code&gt;即为浅色模式。&lt;/p&gt;
&lt;p&gt;然后就是如何读取主题了，这里我使用的是&lt;code&gt;localStorage&lt;/code&gt;，在&lt;code&gt;localStorage&lt;/code&gt;里存储一个&lt;code&gt;theme&lt;/code&gt;的值，然后在页面加载时检测这个值，如果有值就使用这个值，如果没有就使用系统的主题。&lt;/p&gt;
&lt;p&gt;如何切换主题？这里我使用的是&lt;code&gt;document.documentElement.classList&lt;/code&gt;，在&lt;code&gt;&amp;lt;html&amp;gt;&lt;/code&gt;标签上添加&lt;code&gt;dark&lt;/code&gt;类。&lt;/p&gt;
&lt;p&gt;有两种方法可以区分颜色：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;在&lt;code&gt;:root&lt;/code&gt;和 &lt;code&gt;:root.dark&lt;/code&gt;里定义颜色变量，然后在CSS里使用&lt;code&gt;var(--&amp;lt;variable-name&amp;gt;)&lt;/code&gt;来使用变量。&lt;/li&gt;
&lt;li&gt;或者在CSS里使用&lt;code&gt;&amp;lt;selector&amp;gt;.dark&lt;/code&gt;来选择黑暗模式下的样式。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;推荐使用第一种方法，因为第二种方法会导致CSS文件变得很大，而且不利于维护。&lt;/p&gt;
&lt;h1&gt;主要部分代码实现&lt;/h1&gt;
&lt;p&gt;在主题的main.js中：&lt;/p&gt;
&lt;p&gt;初始时，检测&lt;code&gt;localStorage&lt;/code&gt;里是否有&lt;code&gt;theme&lt;/code&gt;的值，如果有就使用这个值，如果没有就把&lt;code&gt;theme&lt;/code&gt;设置为&lt;code&gt;auto&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;data(){
  return {
    theme: localStorage.getItem(&quot;theme&quot;) || &quot;auto&quot;,
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在页面加载时，检测&lt;code&gt;theme&lt;/code&gt;，如果为&lt;code&gt;auto&lt;/code&gt;则检测系统的主题并设置颜色，不是则直接设置颜色。
在&lt;code&gt;beforeunload&lt;/code&gt;事件里，如果&lt;code&gt;theme&lt;/code&gt;为&lt;code&gt;auto&lt;/code&gt;则移除&lt;code&gt;localStorage&lt;/code&gt;里的&lt;code&gt;theme&lt;/code&gt;，否则就设置&lt;code&gt;localStorage&lt;/code&gt;里的&lt;code&gt;theme&lt;/code&gt;为当前的&lt;code&gt;theme&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;created() {
  if (this.theme === &apos;auto&apos;)
    this.isSystemDarkMode() ? this.setDarkMode(true) : this.setDarkMode(false);
  else
    this.theme === &quot;dark&quot; ? this.setDarkMode(true) : this.setDarkMode(false);
  window.addEventListener(&quot;beforeunload&quot;, () =&amp;gt; {
    if (this.theme === &quot;auto&quot;)
      localStorage.removeItem(&quot;theme&quot;);
    else
      localStorage.setItem(&quot;theme&quot;, this.theme)
  });
},
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;判断系统是否为黑暗模式、设置颜色、切换主题的方法：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;methods: {
        // 判断系统是否为黑暗模式
        isSystemDarkMode() {
            return window.matchMedia(&quot;(prefers-color-scheme: dark)&quot;).matches;
        },
        /**
         * @param {boolean} dark 
         */
        setDarkMode(dark) {
            if (dark) {
                document.documentElement.classList.add(&quot;dark&quot;);
                document
                .getElementById(&quot;highlight-style-dark&quot;)
                .removeAttribute(&quot;disabled&quot;);
            } else {
                document.documentElement.classList.remove(&quot;dark&quot;);
                document
                .getElementById(&quot;highlight-style-dark&quot;)
                .setAttribute(&quot;disabled&quot;, &quot;&quot;);
            }
        },
        // 点击按钮切换主题
        handleThemeSwitch() {
            this.theme = ((theme) =&amp;gt; {
                switch (theme) {
                case &quot;auto&quot;: // auto -&amp;gt; light
                    this.setDarkMode(false);
                    return &quot;light&quot;;
                case &quot;light&quot;: // light -&amp;gt; dark
                    this.setDarkMode(true)
                    return &quot;dark&quot;;
                case &quot;dark&quot;: // dark -&amp;gt; auto
                    this.isSystemDarkMode() ? this.setDarkMode(true) : this.setDarkMode(false);
                    return &quot;auto&quot;;
            }})(this.theme)
        },
    },
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;适配第三方组件&lt;/h1&gt;
&lt;p&gt;页面上有一些引入的第三方组件，比如说评论组件，HighLightJS这些组件的样式是在组件内部写死的，所以我们需要在组件加载时检测主题并设置颜色。&lt;/p&gt;
&lt;h2&gt;Waline&lt;/h2&gt;
&lt;p&gt;Waline是一个基于Vercel Serverless的评论系统，它的&lt;a href=&quot;https://waline.js.org/guide/features/style.html&quot;&gt;文档&lt;/a&gt;里有提到如何适配黑暗模式。我们是在html下加上&lt;code&gt;dark&lt;/code&gt;类，所以只需要在配置里写上我们的适配方法即可：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// comments.ejs
Waline.init({
  //.....
  dark: &quot;html.dark&quot;,
})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样Waline就会检测&lt;code&gt;html&lt;/code&gt;标签是否有&lt;code&gt;dark&lt;/code&gt;类，如果有就使用黑暗模式，没有就使用浅色模式。就适配完成了。&lt;/p&gt;
&lt;h2&gt;HighLightJS&lt;/h2&gt;
&lt;p&gt;HighLightJS的样式文件本身就是通过&lt;code&gt;&amp;lt;link&amp;gt;&lt;/code&gt;引入的，官网上也提供了许多不同的主题，我们可以导入浅色和深色两套主题，在浅色时disabled深色那套，深色时取消disabled即可。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# _config.yml
# 给出两套主题
highlight:
    enable: true
    style: github
    styleDark: github-dark
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后引入两套主题：(这里是import.ejs)&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;% if (theme.highlight.enable) { %&amp;gt;
&amp;lt;script src=&quot;https://cdn.staticfile.org/highlight.js/11.8.0/highlight.min.js&quot;&amp;gt;&amp;lt;/script&amp;gt;
&amp;lt;script src=&quot;https://cdn.staticfile.org/highlightjs-line-numbers.js/2.8.0/highlightjs-line-numbers.min.js&quot;&amp;gt;&amp;lt;/script&amp;gt;
&amp;lt;link
    rel=&quot;stylesheet&quot;
    href=&quot;https://cdn.staticfile.org/highlight.js/11.8.0/styles/&amp;lt;%- theme.highlight.style %&amp;gt;.min.css&quot;
/&amp;gt;
&amp;lt;link 
    rel=&quot;stylesheet&quot;
    id=&quot;highlight-style-dark&quot;
    disabled
    href=&quot;https://cdn.staticfile.org/highlight.js/11.8.0/styles/&amp;lt;%- theme.highlight.styleDark %&amp;gt;.min.css&quot;
/&amp;gt;
&amp;lt;script src=&quot;&amp;lt;%- url_for(&quot;/js/lib/highlight.js&quot;) %&amp;gt;&quot;&amp;gt;&amp;lt;/script&amp;gt;
&amp;lt;% } %&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后在切换颜色时切换&lt;code&gt;disabled&lt;/code&gt;属性即可：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// main.js
setDarkMode(dark) {
    if (dark) {
        document.documentElement.classList.add(&quot;dark&quot;);
        document
        .getElementById(&quot;highlight-style-dark&quot;)
        .removeAttribute(&quot;disabled&quot;);
    } else {
        document.documentElement.classList.remove(&quot;dark&quot;);
        document
        .getElementById(&quot;highlight-style-dark&quot;)
        .setAttribute(&quot;disabled&quot;, &quot;&quot;);
    }
},
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;白屏闪屏问题&lt;/h1&gt;
&lt;p&gt;我在自己的本地测试完成后，满心欢喜地推送到了GitHub，然后打开线上的博客，发现了一个问题：点击刷新后，背景变成白色立马变成黑色，几次都是如此，十分影响观感。&lt;/p&gt;
&lt;p&gt;观察页面源代码可知，负责切换主题的逻辑main.js是在body中，在本地调试时，请求非常迅速，main.js能够立即被请求到并执行，而线上有延迟，加载完head后可能要过好一会才能拿到main.js，再执行，所以会出现先白色背景后闪回深色的问题。&lt;/p&gt;
&lt;p&gt;解决方式就是把这一块单独放进head里执行。
(在layout.ejs中，略去了其它部分：)&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;head&amp;gt;
  &amp;lt;script&amp;gt;
    if (localStorage.getItem(&apos;theme&apos;) === &quot;dark&quot; ||
      (!(&quot;theme&quot; in localStorage) &amp;amp;&amp;amp;
        window.matchMedia(&quot;(prefers-color-scheme: dark)&quot;).matches)
    ) {
        document.documentElement.classList.add(&quot;dark&quot;);
    }
  &amp;lt;/script&amp;gt;
&amp;lt;/head&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样就可以保证页面出现时就已经根据&lt;code&gt;localStorage&lt;/code&gt;里的值把颜色设置好了。&lt;/p&gt;
</content:encoded></item><item><title>第一次香港之行</title><link>https://yuzi.dev/posts/notes/first-trip-to-hong-kong/</link><guid isPermaLink="true">https://yuzi.dev/posts/notes/first-trip-to-hong-kong/</guid><description>6月30日这天，周五，我和situ2001一起利用考完试这个周末来了一次香港之旅，也算是我期待已久的第一次出境旅行吧。游完后感触颇深，遂在这个无事可做的下午在工位上整理出我的游记。</description><pubDate>Fri, 07 Jul 2023 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;6月30日这天，周五，我和&lt;a href=&quot;https://situ2001.com&quot;&gt;situ2001&lt;/a&gt;一起利用考完试这个周末来了一次香港之旅，也算是我期待已久的第一次出境旅行吧。游完后感触颇深，遂在这个无事可做的下午在工位上整理出我的游记。&lt;/p&gt;
&lt;h1&gt;前言&lt;/h1&gt;
&lt;p&gt;写游记的缘起是周五的下午有空，看到了别人的游记：&lt;a href=&quot;http://www.trainnets.com/archives/16135&quot;&gt;《嘉镜铁路：鲜为人知的传奇》&lt;/a&gt;后，深受触动，也想为我的旅行留下点什么。&lt;/p&gt;
&lt;p&gt;想去香港大概是因为MTR吧，这个模组几乎完美的将地铁引入到了Minecraft中，使得我在22年重拾起了对MC的兴趣，并一发不可收拾到现在。而体验MTR模组 &lt;em&gt;(Minecraft Transit Railway)&lt;/em&gt; 的原型MTR &lt;em&gt;(Mass Transit Railway 港铁)&lt;/em&gt; 自然也成为愿望清单的一环。&lt;br /&gt;
当然还有其他的理由，比如最便宜的出境，看看和内地完全不同的社会，体验有轨“叮叮车”等等。&lt;/p&gt;
&lt;p&gt;在决心要去后，我经历了一波三折地跑政务中心，在1月底终于办好了通行证。但这个大三下的我经历了太多的奇妙转折。在多次确定后，出发的时间落在了考试结束后的第一个周末。&lt;/p&gt;
&lt;h1&gt;旅程&lt;/h1&gt;
&lt;h2&gt;出发&lt;/h2&gt;
&lt;p&gt;6.30号，我早早地准备下班，公司在猎德，我的路线选择自然是去最近的广州东站乘坐广深线到罗湖，同时等待从深圳科兴科学园下班过来的situ，一起过关进港。&lt;/p&gt;
&lt;p&gt;车次的选择上，我选择了标杆车C8003次，它从广州站出发，经停广州东站后直达深圳站，中间一站不停。据我所知，这样的标杆车C800x次在广深早晚高峰各对开一次。而我所乘坐的C8003在18:27从广州东出发。地图上说只需要15分钟就可以从公司到达广州东，我于是在45分出发，本以为可以提前到达，可是我还是低估了周五晚高峰的天河和广深的客流。&lt;/p&gt;
&lt;p&gt;当我在珠江新城时，地铁的盛况：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./023b8635.jpg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;最后到广州东站时，出站在它的商城里兜了好几个圈子过后，我吃惊的发现安检排队的人流居然有这么多！！&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./ad33a0b6.jpg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;此时距离开车只有十分钟了，我只好插队到队头，一边说着“抱歉，我的车马上要开了”一边往里面挤，还好检票口离得近，保安一直拿着喇叭喊：“这趟车直达深圳，中间不停！”&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./17ea9945.jpg&quot; alt=&quot;&quot; /&gt;
&amp;lt;div class=&quot;pic-caption&quot;&amp;gt;在上站台的地道前等待的人群&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;p&gt;执行这次车的是CRH1A，但是全程只能跑160km/h，座位方向一半一半定死，过道上也站满了人，真是大地铁了。&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;到达罗湖，和situ进行了汇合后，就要过关了。因为我的是L签，最开始还以为要走人工通道，最后一个热情的保安大爷告诉我们直接走自助通道按指纹就行，于是我们就很顺利地过了关。在23年的7月，出入境申报已经形同虚设，随便填一下健康然后就可以直接通过，回想起去年此时，真是想都不敢想的事情「指大摇大摆地不戴口罩去香港」。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./8f16b44e.jpg&quot; alt=&quot;&quot; /&gt;
&amp;lt;div class=&quot;pic-caption&quot;&amp;gt;7.1前夜的香港入境大厅，挂满了国旗和区旗&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;p&gt;进入香港后，我们在711购买了八达通后便来到了港铁的罗湖站，罗湖站是一个西班牙式的尽头站「不考虑直通车」，但是却是安排在侧式站台上车，从站厅到站台路程有点长，所以港铁会提前分流，在列车离开前3分钟关闭对应站台的门。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./c8fcd342.jpg&quot; alt=&quot;&quot; /&gt;
&amp;lt;div class=&quot;pic-caption&quot;&amp;gt;正在月台上等待上车，港铁的经典弯站台和没有屏蔽门，不过这可能是最后一段没有屏蔽门的日子了&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;p&gt;坐到了新红磡站，转乘了屯马线到尖东，走过了一个长长的通道来到了尖沙咀。我们在这里发现了一个中国银行的ATM，我就试了试取钱，还真的吐出来500港币！situ使用招商银行卡也取了1.5k，但是离谱的是，我被收了16元手续费，situ却只被收了10元！！！
&amp;lt;center style=&quot;display: flex;flex-wrap: nowrap;justify-content: center;&quot;&amp;gt;
&amp;lt;div&amp;gt;&amp;lt;img src=&quot;./1689131843196.jpg&quot;&amp;gt;&amp;lt;div class=&quot;pic-caption&quot;&amp;gt;红磡站的屯马线站台&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;
&amp;lt;div&amp;gt;&amp;lt;img src=&quot;./1689131843189.jpg&quot;&amp;gt;&amp;lt;div class=&quot;pic-caption&quot;&amp;gt;尖东往尖沙咀的长长的换乘通道&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&amp;lt;/center&amp;gt;&lt;/p&gt;
&lt;p&gt;酒店很小，还没有宿舍的一半大，厕所洗澡的话转个身都难，但是这样的酒店都需要300多一晚。&lt;br /&gt;
在放下行李后，我们决定去周围逛逛，下楼过后发现大名鼎鼎的重庆大厦就在我们的斜对面，经过的时候还真是一群南亚人在门口守着，没敢进。接着我们便去沿着星光大道走了一圈。
&lt;img src=&quot;./55e069ad.jpg&quot; alt=&quot;&quot; /&gt;&amp;lt;div class=&quot;pic-caption&quot;&amp;gt;眺望港岛的夜景&amp;lt;/div&amp;gt;
&lt;img src=&quot;./9d20280d.jpg&quot; alt=&quot;&quot; /&gt;&amp;lt;div class=&quot;pic-caption&quot;&amp;gt;  一块大屏，庆祝着香港回归26周年&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;p&gt;回公寓的路上还去吃了一顿麦当劳夜宵，花了40多，冰激凌腻死了。&lt;br /&gt;
当晚我睡得并不好，因为太兴奋的缘故，加上situ的巨大的鼾声让我久久不能入眠。&lt;/p&gt;
&lt;h2&gt;Day 1&lt;/h2&gt;
&lt;p&gt;7.1这天，我早早地就起床了，叫醒situ后，我们整备了一下就出发了，我们今天是实打实的“特种兵旅行”，计划去坐天星小轮、海洋公园、叮叮车、轻铁等等。真是一天之内把HK跑了个遍。&lt;/p&gt;
&lt;p&gt;起初是准备去海港城，在路上遇到了一家早餐店，花了20块钱买个一个鸡蛋仔充饥。到了海港城过后，发现大多数店铺要10点半才营业「我们去到的时候才9点」，而且感觉和其他的购物商场无异，我们逛了一圈就出来了。&lt;/p&gt;
&lt;p&gt;离开海港城过后，我们准备去尖沙咀码头搭乘天星小轮去港岛，在广场上有位大叔扛着一面国旗在不停的挥舞，点赞！&lt;/p&gt;
&lt;h3&gt;天星小轮&lt;/h3&gt;
&lt;p&gt;7.1当天去中环的轮船是免费的，当时还没有想好去哪就上船了，但是不知为何还是扣了5块钱。
&lt;img src=&quot;./55390bfc.jpg&quot; alt=&quot;&quot; /&gt;&amp;lt;div class=&quot;pic-caption&quot;&amp;gt;尖沙咀码头，免费坐船的横幅&amp;lt;/div&amp;gt;
&lt;img src=&quot;./4c1c0470.jpg&quot; alt=&quot;&quot; /&gt;&amp;lt;div class=&quot;pic-caption&quot;&amp;gt;小轮行驶在江中&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;p&gt;到达中环后，本来想去坐那个摩天轮的，结果也还没到开放时间，于是我们准备去会展的金紫荆广场打卡。我们选择了走到大路上搭乘公交出行。在这期间路过了一家Apple Store，在香港站的楼上，我们便去香港站逛了一下，参观一下111元单程的抢钱快线。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./3220e126.jpg&quot; alt=&quot;&quot; /&gt;
结果到了会展过后，被警察拦下了，被告知金紫荆广场正在举行活动，12点才开放……于是我们决定，坐上地铁，直奔海洋公园！
&lt;img src=&quot;./f888c762.jpg&quot; alt=&quot;&quot; /&gt;&amp;lt;div class=&quot;pic-caption&quot;&amp;gt;只有5个站，3节车的南港岛线。貌似港铁非常喜欢站前折返&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;h3&gt;海洋公园&lt;/h3&gt;
&lt;p&gt;钻过了长长的隧道后，海洋公园就呈现在了我们面前。首先映入眼帘的就是那四排长长的缆车了，我之前坐过青城山的缆车，还有白云山的缆车，但既能看到山又能看到海的缆车这还是第一次坐。但是走到售票处跟前，我们就被$498的票价给吓死了。慌忙打开携程，票价￥327，想着好不容易来了，就买了吧。于是入了园。&lt;img src=&quot;./a097ddf8.jpg&quot; alt=&quot;&quot; /&gt;&amp;lt;div class=&quot;pic-caption&quot;&amp;gt;海洋公园入口&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;p&gt;多图预警：
&amp;lt;center style=&quot;display: flex;flex-wrap: nowrap;justify-content: center;&quot;&amp;gt;
&amp;lt;div&amp;gt;&amp;lt;img src=&quot;./1689146397554.jpg&quot;&amp;gt;&amp;lt;div class=&quot;pic-caption&quot;&amp;gt;登上缆车前体验了一把旋转木马&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;
&amp;lt;div&amp;gt;&amp;lt;img src=&quot;./1689146397539.jpg&quot;&amp;gt;&amp;lt;div class=&quot;pic-caption&quot;&amp;gt;在缆车上回望，香港的山真的很高&amp;lt;/div&amp;gt;&amp;lt;/div&amp;gt;&amp;lt;/center&amp;gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./3a281b04.jpg&quot; alt=&quot;&quot; /&gt;
&lt;img src=&quot;./18a9e622.jpg&quot; alt=&quot;&quot; /&gt;&amp;lt;div class=&quot;pic-caption&quot;&amp;gt;港湾&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./d11001c7.jpg&quot; alt=&quot;&quot; /&gt;&amp;lt;div class=&quot;pic-caption&quot;&amp;gt;缆车途中&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;p&gt;下车过后，我们首先是玩了那个很刺激的过山车，我也是有好多年没有玩过山车了，确实是非常地惊险刺激。我也是连哄带骗地把situ拉去坐了他的第一次过山车，事后本人表示情绪稳定。🤪&lt;/p&gt;
&lt;p&gt;紧接着，我去尝试了360°的大摆锤「是的situ因为害怕所以没有去」，然后吃了$80的一碗面，吃完了我们排队了半个小时玩了下这个不那么刺激的项目：&lt;img src=&quot;./56e93352.jpg&quot; alt=&quot;&quot; /&gt;&amp;lt;div class=&quot;pic-caption&quot;&amp;gt;实际上还是能感受到向心力的，另外因为升的有点高，situ还是吓到力&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;p&gt;之后又乘坐了一个超快速体验向心力的装置，situ阴险地让我坐外面，然后我就体会了3分钟的130斤物体产生的向心力。&lt;/p&gt;
&lt;p&gt;《未完待续……》&lt;/p&gt;
</content:encoded></item><item><title>使用mkcert进行本地自签证书</title><link>https://yuzi.dev/posts/tinkering/local-self-signed-certificates-with-mkcert/</link><guid isPermaLink="true">https://yuzi.dev/posts/tinkering/local-self-signed-certificates-with-mkcert/</guid><description>在本地开发时，常常会有非https的“不安全”的提示，去获得权威CA的证书又不现实。但借助mkcert，可以方便的自签证书。</description><pubDate>Wed, 28 Jun 2023 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;在本地开发时，常常会有非https的“不安全”的提示，去获得权威CA的证书又不现实。但借助mkcert，可以方便的自签证书。&lt;/p&gt;
&lt;p&gt;首先，上地址：
::github{repo=&quot;FiloSottile/mkcert&quot;}&lt;/p&gt;
&lt;h1&gt;安装&lt;/h1&gt;
&lt;p&gt;按照介绍进行安装，我在Windows下使用scoop进行安装：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;scoop bucket add extras
scoop install mkcert
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其它系统或包管理器可参照介绍页的提示。&lt;/p&gt;
&lt;h1&gt;信任CA证书&lt;/h1&gt;
&lt;h2&gt;本地&lt;/h2&gt;
&lt;p&gt;mkcert可以一键将CA证书装在本机上并信任。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ mkcert -install
Created a new local CA 💥
The local CA is now installed in the system trust store! ⚡️
The local CA is now installed in the Firefox trust store (requires browser restart)! 🦊
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;他人&lt;/h2&gt;
&lt;p&gt;要让他人信任你的CA，可以把公钥发给他人，查看路径：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ mkcert -CAROOT
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;一般是在&lt;code&gt;~\AppData\Local\mkcert&lt;/code&gt;里，把&lt;code&gt;rootCA.pem&lt;/code&gt;发给别人即可（千万不要把key私钥发出去了哦！！）&lt;/p&gt;
&lt;h1&gt;生成证书&lt;/h1&gt;
&lt;p&gt;在一个文件夹内，输入&lt;code&gt;mkcert your.website.com&lt;/code&gt;「自行替换成你需要的地址，如localhost」，然后即可在你的文件夹里得到证书&lt;code&gt;your.website.com.pem&lt;/code&gt;和密钥&lt;code&gt;your.website.com-key.pem&lt;/code&gt;。&lt;/p&gt;
&lt;h1&gt;搭建反代服务器&lt;/h1&gt;
&lt;p&gt;以nginx为例，下载好nginx，把两个文件拖入.&lt;code&gt;/conf&lt;/code&gt;中，然后配置nginx.conf,以下是最简单的反代配置：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    server {
       listen       1145 ssl; #端口
       server_name  localhost;

       ssl_certificate      your.website.com.pem; #你的证书文件名
       ssl_certificate_key  your.website.com-key.pem; #你的密钥文件名

       ssl_session_cache    shared:SSL:1m;
       ssl_session_timeout  5m;

       ssl_ciphers  HIGH:!aNULL:!MD5;
       ssl_prefer_server_ciphers  on;

       location / {
            proxy_pass http://127.0.0.1:8080; #要反代的地址
       }
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以配置多个server，分别监听前端和后端。「可选」&lt;/p&gt;
&lt;p&gt;如果没有问题的话，加上https就可以看到锁了。
图中效果是配合修改系统hosts达成的：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./cb2e517c.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
</content:encoded></item><item><title>小米路由器AC2100刷入OpenWrt</title><link>https://yuzi.dev/posts/tinkering/flashing-openwrt-on-xiaomi-ac2100/</link><guid isPermaLink="true">https://yuzi.dev/posts/tinkering/flashing-openwrt-on-xiaomi-ac2100/</guid><description>今天闲来无事，终于在家里的小米路由器上刷入了OpenWRT，遂记录一下过程。</description><pubDate>Wed, 18 Jan 2023 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;今天闲来无事，终于在家里的小米路由器上刷入了OpenWRT，遂记录一下过程。&lt;/p&gt;
&lt;h2&gt;目录&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;#%E5%88%B7%E5%89%8D%E5%87%86%E5%A4%87&quot;&gt;刷前准备&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#%E4%B8%8B%E8%BD%BD%E5%B0%8F%E7%B1%B3%E6%BC%8F%E6%B4%9E%E5%8C%85%E5%88%B7%E5%85%A5breed&quot;&gt;下载小米漏洞包，刷入breed&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#%E5%9C%A8breed%E5%88%B7%E5%85%A5openwrt-kernel-%E5%BA%95%E5%8C%85&quot;&gt;在breed刷入OPENWRT KERNEL 底包&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#%E5%88%B7%E5%85%A5openwrt-%E6%AD%A3%E5%BC%8F%E5%9B%BA%E4%BB%B6&quot;&gt;刷入OPENWRT 正式固件&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#%E5%90%8E%E7%BB%AD%E7%9A%84%E4%B8%80%E4%BA%9B%E6%93%8D%E4%BD%9C&quot;&gt;后续的一些操作&lt;/a&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;#%E5%86%85%E7%BD%91%E4%B8%BB%E6%9C%BA%E4%B8%8D%E5%8F%AF%E8%BE%BE%E9%97%AE%E9%A2%98&quot;&gt;内网主机不可达问题&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#%E5%AE%89%E8%A3%85%E8%AF%81%E4%B9%A6%E5%8F%AF%E9%80%89&quot;&gt;安装证书（可选）&lt;/a&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;#%E5%AE%89%E8%A3%85acmesh&quot;&gt;安装acme.sh&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#%E5%AE%89%E8%A3%85%E8%AF%81%E4%B9%A6%E5%B9%B6%E9%87%8D%E5%90%AFuhttpd&quot;&gt;安装证书并重启uhttpd&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Openwrt是什么想必无需多言，有了它我们可以更好的玩转路由器。&lt;/p&gt;
&lt;p&gt;参考教程：&lt;a href=&quot;https://supes.top/%E7%BA%A2%E7%B1%B3-%E5%B0%8F%E7%B1%B3-ac2100-%E5%88%B7breed%E5%92%8Copenwrt%E6%95%99%E7%A8%8B/&quot;&gt;https://supes.top/红米-小米-ac2100-刷breed和openwrt教程/&lt;/a&gt;&lt;/p&gt;
&lt;h1&gt;刷前准备&lt;/h1&gt;
&lt;p&gt;如果你有一台带网口的电脑和一根网线，那使用网线将电脑和路由器连接即可。&lt;/p&gt;
&lt;p&gt;如果你的电脑没有网口，或者你没有网线，那可以使用双路由器方案。相信很多人家里都是：互联网-光猫-路由器-终端 的配置。而现在光猫很多都有路由器功能。我们可以把光猫的WIFI开关打开，以实现登录breed的功能。&lt;/p&gt;
&lt;h1&gt;下载小米漏洞包，刷入breed&lt;/h1&gt;
&lt;p&gt;教程参考地址：&lt;a href=&quot;https://www.right.com.cn/forum/thread-4066963-1-1.html&quot;&gt;https://www.right.com.cn/forum/thread-4066963-1-1.html&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;其实看上面的教程就能看懂，我在此简单说说。&lt;/p&gt;
&lt;p&gt;先下载有漏洞的固件：&lt;a href=&quot;http://cdn.cnbj1.fds.api.mi-img.com/xiaoqiang/rom/r2100/miwifi_r2100_firmware_4b519_2.0.722.bin&quot;&gt;http://cdn.cnbj1.fds.api.mi-img.com/xiaoqiang/rom/r2100/miwifi_r2100_firmware_4b519_2.0.722.bin&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;下载完成后进入后台 192.168.31.1-&amp;gt;&lt;strong&gt;常用设置&lt;/strong&gt;-&amp;gt;&lt;strong&gt;系统状态&lt;/strong&gt;-&amp;gt;手动升级  &lt;/p&gt;
&lt;p&gt;加载固件，可以保留数据 &lt;strong&gt;-&amp;gt;&lt;/strong&gt; 开始升级&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;进入后台192.168.31.1，复制自己的stok，将stok替换下面的CCCC：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;http://192.168.31.1/cgi-bin/luci/;stok=CCCCCCCCCCC/api/misystem/set_config_iotdev?bssid=Xiaomi&amp;amp;user_id=longdike&amp;amp;ssid=%0A%5B%20-z%20%22%24(dmesg%20%7C%20grep%20ESMT)%22%20%5D%20%26%26%20B%3D%22Toshiba%22%20%7C%7C%20B%3D%22ESMT%22%0Auci%20set%20wireless.%24(uci%20show%20wireless%20%7C%20awk%20-F%20&apos;.&apos;%20&apos;%2Fwl1%2F%20%7Bprint%20%242%7D&apos;).ssid%3D%22%24B%20%24(dmesg%20%7C%20awk%20&apos;%2FBad%2F%20%7Bprint%20%245%7D&apos;)%22%0A%2Fetc%2Finit.d%2Fnetwork%20restart%0A
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;此代码是用来检查NAND坏块的。路由器开机超过一小时建议先重启。运行代码后，你路由器的2.4g WiFi名称会改名成：比如  &quot;ESMT&quot;，&quot;Toshiba&quot;，&quot;Toshiba 90 768&quot;。 90和768是坏块。 如果ESMT或者Toshiba后面没数字，那恭喜你，没有坏块！！！&lt;/p&gt;
&lt;hr /&gt;
&lt;pre&gt;&lt;code&gt;http://192.168.31.1/cgi-bin/luci/;stok=CCCCCCCCCCC/api/misystem/set_config_iotdev?bssid=Xiaomi&amp;amp;user_id=longdike&amp;amp;ssid=%0Acd%20%2Ftmp%0Acurl%20-o%20B%20-O%20https%3A%2F%2Fbreed.hackpascal.net%2Fr1286%2520%255b2020-10-09%255d%2Fbreed-mt7621-xiaomi-r3g.bin%20-k%20-g%0A%5B%20-z%20%22%24(sha256sum%20B%20%7C%20grep%20242d42eb5f5aaa67ddc9c1baf1acdf58d289e3f792adfdd77b589b9dc71eff85)%22%20%5D%20%7C%7C%20mtd%20-r%20write%20B%20Bootloader%0A
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;此代码是用来刷BREED的。&lt;strong&gt;如果路由器在60秒内重启则代表刷BREED成功&lt;/strong&gt;(灯会从&lt;strong&gt;蓝&lt;/strong&gt;变&lt;strong&gt;橘&lt;/strong&gt;，最终变&lt;strong&gt;蓝&lt;/strong&gt;进入系统)&lt;strong&gt;。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;如果没重启，可能是stok过期了。进入后台复制新的stok即可。也有可能下载的BREED损坏，从新运行代码。也有可能没网络。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;成功后拔掉电源，按住reset同时接上电源等10秒即可进入breed。需要注意的是如果闪蓝灯那就是BREED。如果是闪橘灯那就是原厂UBOOT。&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;⚠️如果是没有用网线连接路由器的话，breed是不会有无线连接的，这时候就需要另一台路由器（比如光猫）了。
教程地址：&lt;a href=&quot;https://www.right.com.cn/forum/thread-1146483-1-1.html&quot;&gt;https://www.right.com.cn/forum/thread-1146483-1-1.html&lt;/a&gt;简单来说，先将路由器wan口的先拔出，插在lan口上，然后，因为天翼网关默认是192.168.1.1，而breed也是192.168.1.1，会冲突，所以要进天翼网关更改网段，我改成了192.168.0. *。改了后，在电脑控制面板更改电脑IP地址为手动获取，然后填入IP：192.168.1.2，掩码：255.255.255.0，网关192.168.1.1。之后访问192.168.1.1即可进入breed控制界面。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;最后用进入浏览器，最好是隐身模式，输入192.168.1.1  进入breed管理界面&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;刷完后可能无法进入原厂系统，进BREED删变量：normal_firmware_md5&lt;/strong&gt;&lt;/p&gt;
&lt;h1&gt;在breed刷入OPENWRT KERNEL 底包&lt;/h1&gt;
&lt;p&gt;跟着文章顶部的教程走，点击 环境变量编辑 点 添加, 字段 输入 xiaomi.r3g.bootfw  值 输入 2 , 点击 保存&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://supes.top/wp-content/uploads/2022/05/Snipaste_2022-05-13_20-42-11.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;然后刷入底包：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://supes.top/wp-content/uploads/2022/05/2-1.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;底包下载地址：&lt;a href=&quot;https://supes.top/?version=22.03&amp;amp;target=ramips/mt7621&amp;amp;id=xiaomi_redmi-router-ac2100&quot;&gt;https://supes.top/?version=22.03&amp;amp;target=ramips%2Fmt7621&amp;amp;id=xiaomi_redmi-router-ac2100&lt;/a&gt; （可能会有变化，可以在网址里找找），第一个是升级包，第二个才是底包。&lt;/p&gt;
&lt;h1&gt;刷入OPENWRT 正式固件&lt;/h1&gt;
&lt;p&gt;路由器会自动重启, 等蓝灯常亮后 浏览器 输入 &lt;a href=&quot;http://10.0.0.1/cgi-bin/luci&quot;&gt;10.0.0.1&lt;/a&gt; 进入OpenWrt底包后台，如果是无线，此时openwrt已经自动有了WiFi，可以断开和另外一台路由器的连接转而连接AC2100了。默认密码为root。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://supes.top/wp-content/uploads/2022/05/8.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;选择好升级包后，选择不保留配置，刷入，等待路由器自动重启完成,蓝灯常亮后,会自动跳转到后台登录界面, 或手动输入后台地址  &lt;a href=&quot;http://10.0.0.1/cgi-bin/luci&quot;&gt;10.0.0.1&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;登录后台, 默认密码 root，进入后会有引导，一般只需要修改WiFi名和密码。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;之后一定要去系统→管理权 中修改root密码！！！！&lt;/strong&gt;&lt;/p&gt;
&lt;h1&gt;后续的一些操作&lt;/h1&gt;
&lt;h2&gt;内网主机不可达问题&lt;/h2&gt;
&lt;p&gt;奇怪的问题：内网主机不可达，ping显示没有路径，我想难道是没有路由表？所以我在网络→路由→静态ipv4路由里加了一条，重启后就恢复正常了！&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./2c6849d0.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;安装证书（可选）&lt;/h2&gt;
&lt;p&gt;因为家里有公网IP，所以我将openwrt面板映射到了外部，安全起见，开启HTTPS证书。&lt;/p&gt;
&lt;p&gt;因为OW使用的uhttpd，我的域名在cloudflare上，所以以此为例。&lt;/p&gt;
&lt;p&gt;进入ssh，在系统→管理权处开启&lt;/p&gt;
&lt;h3&gt;安装acme.sh&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;curl  https://get.acme.sh | sh
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;安装好后，&lt;code&gt;cd /root/.acme.sh&lt;/code&gt;进入目录，&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;export CF_Key=&quot;cloudflare中查看你的APIkey&quot;
export CF_Email=&quot;你的邮箱&quot;
./acme.sh  --issue  --dns dns_cf -d 你的域名

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可能会执行失败，按照提示对邮箱进行注册即可。&lt;/p&gt;
&lt;h3&gt;安装证书并重启uhttpd&lt;/h3&gt;
&lt;p&gt;在申请好证书后，还需要把证书安装到指定的位置：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;./acme.sh --installcert -d 你的域名 --keypath /etc/uhttpd.key  --fullchainpath /etc/uhttpd.crt --reloadcmd &quot;/etc/init.d/uhttpd restart&quot;
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>WSL入坑记</title><link>https://yuzi.dev/posts/tinkering/getting-started-with-wsl/</link><guid isPermaLink="true">https://yuzi.dev/posts/tinkering/getting-started-with-wsl/</guid><description>起因是最终还是受不了Linux生态过于薄弱，又不想用ms-Windows做开发，遂想到了这个，然后开始尝试，还是踩了不少坑，总结如下：</description><pubDate>Sat, 24 Sep 2022 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;起因是最终还是受不了Linux生态过于薄弱，又不想用ms-Windows做开发，遂想到了这个，然后开始尝试，还是踩了不少坑，总结如下：&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;首先，我选择的Arch发行版，因为我之前一直用的manjaro，然鹅manjaro的wsl貌似很久没维护了，所以我就干脆使用Arch了。
仓库地址：&lt;a href=&quot;https://github.com/yuk7/ArchWSL&quot;&gt;https://github.com/yuk7/ArchWSL&lt;/a&gt; ，我使用的是安装方法1。wsl版本是wsl2。&lt;/p&gt;
&lt;h2&gt;关于Error 0x80370102&lt;/h2&gt;
&lt;p&gt;开始安装，首先要安装以下三条：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./v2-d65f6b7c8761b9091c5ea8b596e2447b_r.jpg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;或者也可以在powershell里开启：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Enable-WindowsOptionalFeature -Online -FeatureName Microsoft-Hyper-V -All
Enable-WindowsOptionalFeature -Online -FeatureName VirtualMachinePlatform
Enable-WindowsOptionalFeature -Online -FeatureName Microsoft-Windows-Subsystem-Linux

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然而即使这样做了，启动时仍然报错80370102，搜遍全网，最后在知乎&lt;a href=&quot;https://zhuanlan.zhihu.com/p/147233604&quot;&gt;这里&lt;/a&gt;找到了答案，就是在上述做完后，再键入一条：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;bcdedit /set hypervisorlaunchtype auto
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;就没问题了。&lt;/p&gt;
&lt;h2&gt;代理配置&lt;/h2&gt;
&lt;p&gt;因为在wsl中，随着宿主机的重启，wsl和宿主机的IP全都会变，这就会造成很大的麻烦，还好wsl里有文件能自动更新IP，于是设置代理如下：&lt;/p&gt;
&lt;p&gt;在&lt;code&gt;/etc/profile&lt;/code&gt;[//]: # (这里配置的环境变量全局可见，永久有效)中添加以下语句：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;host_ip=$(cat /etc/resolv.conf |grep &quot;nameserver&quot; |cut -f 2 -d &quot; &quot;)
export ALL_PROXY=&quot;http://$host_ip:20170&quot;
export all_proxy=&quot;http://$host_ip:20170&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;端口根据宿主机配置的来，我Windows为此换成了clash，也为的是它能智能的分流，clash中要把“allow lan”打开。此时全局都可以走代理了，而对于chrome这类不走all_proxy的，可以在其启动参数后增加&lt;code&gt;--proxy-server=$all_proxy&lt;/code&gt;来走代理。&lt;/p&gt;
&lt;h2&gt;GUI&lt;/h2&gt;
&lt;p&gt;微软搞了个wslg，可以很方便地运行GUI程序，甚至还给wsl搞了显卡驱动，但很可惜，我折腾了半天，显卡是没有驱动上的。&lt;/p&gt;
&lt;p&gt;首先将wsl更新到最新版本，然后把显卡驱动也更新到最新。然后安一堆诸如xorg，ffmpeg，libGL等等一堆库后，就神奇的跑起来了，开始菜单也能看到Linux程序了。&lt;/p&gt;
&lt;h3&gt;高分屏缩放&lt;/h3&gt;
&lt;p&gt;不绕弯了，放上最干货的回答：&lt;a href=&quot;https://zhuanlan.zhihu.com/p/424930447&quot;&gt;https://zhuanlan.zhihu.com/p/424930447&lt;/a&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;小声bb：为什么最后找到的解决方案都是知乎上的🥲「悲」&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;在家目录下新建文件：&lt;code&gt;~/.Xresource&lt;/code&gt;，然后输入：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Xft.dpi: 175
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上面填入缩放大小，175%就填175，保存退出。然后在&lt;code&gt;/etc/profile&lt;/code&gt;后追加：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;xrdb -merge ~/.Xresource
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;就大功告成了！这里是相当于直接调整了X11的缩放，是最完全的！&lt;/p&gt;
&lt;h2&gt;指纹sudo&lt;/h2&gt;
&lt;p&gt;在Windows下最大的好处是可以使用指纹来验证sudo，不用每次都输密码了，这绝对是一个巨大优势&lt;/p&gt;
&lt;p&gt;仓库地址：&lt;a href=&quot;https://github.com/nullpo-head/WSL-Hello-sudo&quot;&gt;https://github.com/nullpo-head/WSL-Hello-sudo&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;按照教程一步步来，其实很简单。最后因为我的不是Ubuntu，需要手动添加，需在&lt;code&gt;/etc/pam.d/sudo&lt;/code&gt;首部追加：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;auth sufficient pam_wsl_hello.so
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;即可。&lt;/p&gt;
&lt;h2&gt;网络转发&lt;/h2&gt;
&lt;p&gt;由于wsl2是基于hyperV的，就是一个虚拟机，所以网络肯定是和主机隔开的，我们查看网卡也可以发现多了一块wsl的虚拟网卡，那么问题来了，假如在wsl里serve，外部怎么访问到呢？微软已经贴心的做了转发，本机访问&lt;code&gt;localhost:端口&lt;/code&gt;会自动访问到wsl的serve，但是如果想要局域网访问，那还得费一番功夫，因为每次IP都会变，网上貌似有自动脚本，但是考虑到要向局域网暴露的时候不多（顶多传个文件），直接放在.bat里执行就行，内容如下：（都要以管理员运行）&lt;/p&gt;
&lt;p&gt;打开转发：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;netsh interface portproxy add v4tov4 listenport=要暴露的端口 connectaddress=wsl的IP地址 connectport=wsl的端口 listenaddress=* protocol=tcp
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;另外，需要关闭防火墙，局域网才能访问！&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;关闭转发：（注意，上述打开转发执行后，会一直开着，重启也会继续，所以最好用完就关闭）&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;netsh interface portproxy delete v4tov4 listenport=要关闭的已暴露端口 protocol=tcp
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Systemd与Docker&lt;/h2&gt;
&lt;p&gt;wsl2一直不支持systemd，这导致诸如docker守护进程就无法启动，然而在我安装的前两天，Win11的22H2更新带来了systemd的支持：&lt;a href=&quot;https://github.com/microsoft/WSL/releases/tag/0.67.6&quot;&gt;https://github.com/microsoft/WSL/releases/tag/0.67.6&lt;/a&gt; ，&lt;a href=&quot;https://aka.ms/wslsystemd&quot;&gt;https://aka.ms/wslsystemd&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;打开方法：更新后配置&lt;code&gt;/etc/wsl.conf&lt;/code&gt;，在其后追加：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[boot]
systemd=true
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后&lt;code&gt;wsl.exe --shutdown&lt;/code&gt;一波，差不多应该就可以了。我尝逝了一下，结果docker是好了，wslg炸了。wslg的issue里也有多人反馈，哎……看来还是不要追新。不得已，把上述配置删掉，wslg立刻就恢复了，幸好。评论区以及有人提出，相信过几天就应该能修好（毕竟是pre-release）&lt;/p&gt;
&lt;p&gt;不使用systemd的情况下，要使用docker就只能每次开个终端手动执行&lt;code&gt;sudo dockerd&lt;/code&gt;啰。&lt;/p&gt;
&lt;h1&gt;后记&lt;/h1&gt;
&lt;p&gt;折腾了这么两天，中途都有点怀疑这样做的意义，也一直在比较wsl和纯Linux的异同。最后还是决定入wsl弃实机Linux，首先是Windows装wsl比Linux装Windows虚拟机绝对是要强的多的。其次Windows的软件生态是不可忽视的一环，比如MIUI+之类的。另外，wsl备份挺方便的，一键备份无忧。wsl的两个系统文件互通做得很不错。&lt;/p&gt;
&lt;p&gt;另外今早我进行了Windows的续航测试，不插电轻度使用从早上9点用到中午12点半共3个半小时掉了60%的电，这基本上是两个大节的时间，这样是完全能接受的，所以说也没有什么理由不使用win+wsl了。唯一的不足可能是Windows的minecraft的帧率为何低于Linux呢？可以留着Linux环境玩MC Java版用。&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;后续更新：10月就弃了WSL了，Linux实体机还是性能更高，更适合当开发系统。Windows还是留着当游戏启动器罢！「悲」&lt;/p&gt;
</content:encoded></item><item><title>一个比较好看的firework-js</title><link>https://yuzi.dev/posts/frontend/a-better-looking-firework-js/</link><guid isPermaLink="true">https://yuzi.dev/posts/frontend/a-better-looking-firework-js/</guid><description>在逛别人博客时看到的，本质上都是一个东西，不过看着他的配色更好看更柔和一些，遂扒之。</description><pubDate>Mon, 19 Sep 2022 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;在逛别人博客时看到的，本质上都是一个东西，不过看着他的配色更好看更柔和一些，遂扒之。&lt;/p&gt;
&lt;p&gt;原博客：&lt;a href=&quot;http://lixianglong.cn/&quot;&gt;http://lixianglong.cn/&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;扒下来的代码：&lt;a href=&quot;https://blog.yuzi0201.top/js/firework2.js&quot;&gt;https://blog.yuzi0201.top/js/firework2.js&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;效果如图：&lt;img src=&quot;./d3f3ebbe.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;使用方法与&lt;a href=&quot;https://argvchs.netlify.app/2022/04/17/hexo-blog-3/&quot;&gt;原博教程&lt;/a&gt;类似&lt;/p&gt;
</content:encoded></item><item><title>在ParticleX上使用Waline作为评论区</title><link>https://yuzi.dev/posts/frontend/using-waline-on-particlex/</link><guid isPermaLink="true">https://yuzi.dev/posts/frontend/using-waline-on-particlex/</guid><description>珍爱生命，远离高权限评论插件</description><pubDate>Thu, 15 Sep 2022 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;珍爱生命，远离高权限评论插件&lt;/p&gt;
&lt;h2&gt;目录&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;#%E4%B8%BA%E4%BB%80%E4%B9%88%E5%BC%83%E7%94%A8gitalk&quot;&gt;为什么弃用Gitalk?&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#%E9%83%A8%E7%BD%B2vercel&quot;&gt;部署Vercel&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#%E4%BD%BF%E7%94%A8supabase%E5%81%9A%E6%95%B0%E6%8D%AE%E5%BA%93&quot;&gt;使用SupaBase做数据库&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#%E5%89%8D%E7%AB%AF%E5%BC%95%E5%85%A5&quot;&gt;前端引入&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h1&gt;为什么弃用Gitalk?&lt;/h1&gt;
&lt;p&gt;昨天，我使用ParticleX自带的Gitalk弄好了评论区系统，能用，但是当我邀请舍友来参与评论时，一名舍友发出了质疑：这个评论索要的权限也太高了吧！&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./image_AgNA1mpbUW.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;仔细一看，居然要&lt;strong&gt;读写所有公开仓库&lt;/strong&gt;的权限，看着实在是太吓人了，为什么会这样？&lt;/p&gt;
&lt;p&gt;在我仔细搜索一番后，发现v2ex论坛早已有相关&lt;a href=&quot;https://www.v2ex.com/t/535608&quot;&gt;讨论&lt;/a&gt;，因为他的实现原理是使用一个repo的issue来存储评论区，那要发评论就是操作你的账号往该仓库的issue里发东西，而由于GitHub Oauth的权限粒度的问题，要想实现上述功能，就必须要这么高的权限。而这个权限的token貌似就存在localstorage中，博主只需稍微在网页上写一点Ajax，就能拿到评论者的GitHub所有仓库的读写权限！&lt;/p&gt;
&lt;p&gt;不光如此，使用gitalk，该Oauth的clientID和secret都会被直接暴露在前端！对于博主来说也是很危险的行为：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./5bcc57a8.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;所以，我必须寻找一个合适的评论系统，规避以上问题，经过我的一番搜寻，Waline便成为我选择的对象，它的优点有：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;不会暴露评论者和博主的隐私&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;可选择匿名评论，降低评论门槛（可选关闭）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;使用Vercel部署，多种数据库可供选择，速度快&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;也可使用多种第三方账号登录，登录时只索要个人信息的最基础权限&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;部署Vercel&lt;/h1&gt;
&lt;p&gt;首先，上&lt;a href=&quot;https://github.com/walinejs/waline&quot;&gt;官方仓库地址&lt;/a&gt;和&lt;a href=&quot;https://waline.js.org/&quot;&gt;官方网站&lt;/a&gt;，其实跟着官方网站的教程走，也可以很快搭好。&lt;/p&gt;
&lt;p&gt;打开教程，看到&lt;a href=&quot;https://waline.js.org/guide/get-started.html#vercel-%E9%83%A8%E7%BD%B2-%E6%9C%8D%E5%8A%A1%E7%AB%AF&quot;&gt;部署&lt;/a&gt;部分，可以看到官方给的一键部署链接：&lt;a href=&quot;https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fwalinejs%2Fwaline%2Ftree%2Fmain%2Fexample&quot;&gt;Deploy&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;点进去后，选择GitHub部署，输入一个未被使用的名字，然后点击Create。（注：一定要勾选创建私有仓库！）&lt;/p&gt;
&lt;p&gt;创建好后，就可以配置数据库了。&lt;/p&gt;
&lt;h1&gt;使用SupaBase做数据库&lt;/h1&gt;
&lt;p&gt;这个东西是一个“BaaS”的东西，叫后端即服务，是昨天舍友推荐给我的，没想到今天就用上了，官网也有对应部署&lt;a href=&quot;https://waline.js.org/guide/server/databases.html#postgresql&quot;&gt;教程&lt;/a&gt;。&lt;/p&gt;
&lt;p&gt;注册好后，将官方提供的&lt;a href=&quot;https://github.com/walinejs/waline/blob/main/assets/waline.pgsql&quot;&gt;创建表的SQL&lt;/a&gt;直接扔进SQL Editor中，运行，Over！&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./becf921c.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;创建好后，就把连接数据库的字段放进Vercel的环境变量中，其值在Supabase的“Settings→Database”页面往下划：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./68ec9d93.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;然后就去Vercel的“Settings→Environment Variables”里增加环境变量，对照文档里的添加就行了：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./476aad5d.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;密码是你刚刚创建项目时候输入的，如果忘了可以在下面重置。&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;当然，这个评论系统还有诸多实用功能，如 使用邮箱进行提醒 等，可参照官网文档进行设置&lt;/p&gt;
&lt;h1&gt;前端引入&lt;/h1&gt;
&lt;p&gt;我在稍后会向ParticleX的仓库提PR，向其添加内置Waline的功能，可以直接在主题设置里调：&lt;/p&gt;
&lt;p&gt;关于自定义Vercel地址，详见：“Settings→Domains”&lt;/p&gt;
&lt;p&gt;&lt;code&gt;_config.yml&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;waline:
    # New! Whether enable this plugin
    enable: true
    # Waline server address url, you should set this to your own link
    serverURL: #此处填写Vercel部署的地址
    # Custom locales
    # locale:
    #   placeholder: Welcome to comment # Comment box placeholder

    # If false, comment count will only be displayed in post page, not in home page
    commentCount: true

    # Pageviews count, Note: You should not enable both `waline.pageview` and `leancloud_visitors`.
    pageview: false

    # Custom emoji
    emoji:
        - https://unpkg.com/@waline/emojis@1.0.1/weibo
        - https://unpkg.com/@waline/emojis@1.0.1/alus
        - https://unpkg.com/@waline/emojis@1.0.1/bilibili
        - https://unpkg.com/@waline/emojis@1.0.1/qq
        - https://unpkg.com/@waline/emojis@1.0.1/tieba
        - https://unpkg.com/@waline/emojis@1.0.1/tw-emoji

    # Comment infomation, valid meta are nick, mail and link
    meta:
        - nick
        - mail
        - link

    # Set required meta field, e.g.: [nick] | [nick, mail]
    requiredMeta:
        - nick

    # Language, available values: en-US, zh-CN, zh-TW, pt-BR, ru-RU, jp-JP
    lang: zh-CN

    # Word limit, no limit when setting to 0
    wordLimit: 0

    # Whether enable login, can choose from &apos;enable&apos;, &apos;disable&apos; and &apos;force&apos;
    login: enable

    # comment per page
    pageSize: 10
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;修改的地方如下：&lt;/p&gt;
&lt;p&gt;&lt;code&gt;post.ejs&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    &amp;lt;% if (theme.waline.enable) { %&amp;gt;
        &amp;lt;!-- 样式文件 --&amp;gt;
        &amp;lt;link rel=&quot;stylesheet&quot; href=&quot;https://unpkg.com/@waline/client@v2/dist/waline.css&quot; /&amp;gt;
        &amp;lt;div id=&quot;comment&quot;&amp;gt;
            &amp;lt;div id=&quot;waline-container&quot;&amp;gt;
            &amp;lt;/div&amp;gt;
        &amp;lt;/div&amp;gt;
        &amp;lt;% } %&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;script.ejs&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;% if (theme.waline.enable &amp;amp;&amp;amp; is_post()) { %&amp;gt;
    &amp;lt;script src=&quot;https://unpkg.com/@waline/client@v2/dist/waline.js&quot;&amp;gt;&amp;lt;/script&amp;gt;
    &amp;lt;script&amp;gt;
        Waline.init({
            el: &apos;#waline-container&apos;,
            serverURL: &quot;&amp;lt;%- theme.waline.serverURL %&amp;gt;&quot;,
            commentCount: &quot;&amp;lt;%- theme.waline.commentCount %&amp;gt;&quot;,
            pageview: &quot;&amp;lt;%- theme.waline.pageview %&amp;gt;&quot;,
            emoji: &quot;&amp;lt;%- theme.waline.emoji %&amp;gt;&quot;.split(&apos;,&apos;),
            meta: &quot;&amp;lt;%- theme.waline.meta %&amp;gt;&quot;.split(&apos;,&apos;),
            requiredMeta: &quot;&amp;lt;%- theme.waline.requiredMeta %&amp;gt;&quot;.split(&apos;,&apos;),
            lang: &quot;&amp;lt;%- theme.waline.lang %&amp;gt;&quot;,
            wordLimit: parseInt(&quot;&amp;lt;%- theme.waline.wordLimit %&amp;gt;&quot;),
            pageSize: &quot;&amp;lt;%- theme.waline.pageSize %&amp;gt;&quot;,
            login: &quot;&amp;lt;%- theme.waline.login %&amp;gt;&quot;,
        });
    &amp;lt;/script&amp;gt;
    &amp;lt;% } %&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;祝各位都能用上Waline！有问题评论区见！&lt;/p&gt;
</content:encoded></item><item><title>搭建图床</title><link>https://yuzi.dev/posts/tinkering/building-an-image-host/</link><guid isPermaLink="true">https://yuzi.dev/posts/tinkering/building-an-image-host/</guid><description>给博客搭了个图床~</description><pubDate>Thu, 15 Sep 2022 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;给博客搭了个图床~&lt;/p&gt;
&lt;p&gt;参考链接：&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://sspai.com/post/61624&quot;&gt;https://sspai.com/post/61624&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;不过因为我的域名没有备案（个人备什么案），所以我购买了香港的包，具体步骤&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;首先去阿里云开通OSS对象存储&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;购买资源包，地域选择香港🇭🇰，其他默认即可，半年5块钱40G，还是挺便宜的。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./7a39bcce.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;然后创建bucket，地域选香港，权限选择公共读。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./e7988048.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;创建好后，在“权限管理→防盗链”中开启防盗链，防止流量被滥用&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./e4738ff5.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;在“传输管理→域名管理”中绑定域名，先验证，去域名商处验证阿里云提供的TXT，验证完后，在域名提供商处加入CNAME的DNS记录，指向OSS的域名&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;在首页下载oss-browser图形化管理工具，配置好用户，然后在本地登录运行，这样就能很方便地上传和管理文件了！&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Fin&lt;/p&gt;
&lt;hr /&gt;
</content:encoded></item><item><title>人工智能实验1</title><link>https://yuzi.dev/posts/course-notes/ai-programming-lab-1/</link><guid isPermaLink="true">https://yuzi.dev/posts/course-notes/ai-programming-lab-1/</guid><description>拿到这个实验的时候，我是一脸懵逼的，毕竟理论课只上了一节，还全是在吹水，然后实验课就来了，还是用的Python。实验课是周二上午上的，然而我周三晚上就把整个实验肝完了，期间除了上课和早上学英语单词，其他时间都在这上面，写代码还是挺快乐的，于…</description><pubDate>Wed, 14 Sep 2022 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;拿到这个实验的时候，我是一脸懵逼的，毕竟理论课只上了一节，还全是在吹水，然后实验课就来了，还是用的Python。实验课是周二上午上的，然而我周三晚上就把整个实验肝完了，期间除了上课和早上学英语单词，其他时间都在这上面，写代码还是挺快乐的，于是将（踩坑）过程记录如下：&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;偷个懒，以下是实验报告原文：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;分析估价函数对启发式搜索算法的影响。&lt;/p&gt;
&lt;p&gt;由实验结果可知，估价函数的选取对结果的长度和求出的时间都有印象，不同的估价函数应对不同的初值，性能也有差异。总的来说，选取一个好的估价函数，对于使用启发式搜索算法又快又好地求出结果是至关重要的。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;探究讨论各个搜索算法的特点。&lt;/p&gt;
&lt;p&gt;广度优先搜索：如果有解，总是能找到最优解，但是其效率较低，体现在耗时较长。&lt;/p&gt;
&lt;p&gt;深度优先搜索：一般找到的都不是最优解，性能和深度限制的设置有很大关系。&lt;/p&gt;
&lt;p&gt;A&amp;amp;A*启发式搜索: 在状态空间中的搜索对每一个搜索的位置进行评估，得到最好的位置，再从这个位置进行搜索直到目标。这样可以省略大量无谓的搜索路径，效率极高。在启发式搜索中，对位置的估价是十分重要的。采用了不同的估价可以有不同的效果。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;*扩展选做题：从初始状态到目标状态的变换，符合什么规律才可解。&lt;/p&gt;
&lt;p&gt;由数学知识可知，可计算这两个有序数列的逆序值，如果两者都是偶数或奇数，则可通过变换到达，否则，这两个状态不可达。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;我的思考：&lt;/p&gt;
&lt;p&gt;在编写代码的过程中，我遇到了许多或“哭笑不得”，或“意味深长”的bug。下面列举几例：&lt;/p&gt;
&lt;p&gt;一．预先给的代码在我的电脑上（Linux）无法运行&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./563e3206.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;但是在其他同学的Windows电脑上却能跑起来，我查阅资料，再细看这个报错，感觉这个构造函数里的“拼图”字符串是多此一举，遂去掉，程序便能正常运行了！&lt;/p&gt;
&lt;p&gt;二．Python语言不熟练导致的问题
因为我对Python的了解不太深入，就导致了一些我写的代码和我想要的效果不一致的问题，比如：for i in range(-(COLS-1), COLS-1)中，range是“右开”的，所以右边界是取不到的。再比如有如下问题：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./8b4e2bc8.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;错误写法看似没有毛病，如果是C的话，确实没毛病，但是Python中变量可以不声明就直接赋值，所以这里Python就会把145行当成声明了一个局部变量，即使外面有同名全局变量！第一种解决方法就是，因为该值是一个数组，所以可以直接操作其数组内的值，此时就会取到全局变量，没有歧义；第二种解决方法就是使用global声明该变量是全局变量，再对其进行操作。&lt;/p&gt;
&lt;p&gt;三．i与j和x与y的匹配问题，到底哪个是行，哪个是列，常常搞得人晕头转向，不过使用调试器或者事先仔细思考一下也能解决。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;总的来说，这次实验我确实学习到了很多，也收获到了很多。&lt;/p&gt;
&lt;p&gt;代码地址：&lt;a href=&quot;https://github.com/Yuzi0201/AIProgramming/blob/main/EX1/tmpPuzzleWithAutoPlay.py&quot;&gt;https://github.com/Yuzi0201/AIProgramming/blob/main/EX1/tmpPuzzleWithAutoPlay.py&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;一些参考：&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://zhuanlan.zhihu.com/p/266830722&quot;&gt;https://zhuanlan.zhihu.com/p/266830722&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://blog.csdn.net/weixin_48557496/article/details/121463172&quot;&gt;https://blog.csdn.net/weixin_48557496/article/details/121463172&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;局部变量问题：&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://www.jianshu.com/p/c3587a5f9f68&quot;&gt;https://www.jianshu.com/p/c3587a5f9f68&lt;/a&gt;&lt;/p&gt;
</content:encoded></item><item><title>新的开始</title><link>https://yuzi.dev/posts/notes/new-beginning/</link><guid isPermaLink="true">https://yuzi.dev/posts/notes/new-beginning/</guid><description>万年不更新的博客终于重新开更了！</description><pubDate>Sun, 11 Sep 2022 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;万年不更新的博客终于重新开更了！&lt;/p&gt;
&lt;p&gt;重新使用了新版本的hexo，新的主题：ParticleX。
&lt;a href=&quot;https://github.com/argvchs/hexo-theme-particlex&quot;&gt;主题GitHub&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://argvchs.github.io/tags/Hexo/&quot;&gt;教程地址&lt;/a&gt;（是主题作者写的，非常不错）&lt;/p&gt;
&lt;p&gt;在大一刚刚入门hexo的时候简直是懵懂无知，就这么跌跌撞撞地搭建好了，今日回头再看，hexo也支持了使用&lt;code&gt;npm&lt;/code&gt;，各种操作行云流水，再加上强大的教程，不一会就搭建好了！&lt;/p&gt;
&lt;p&gt;之后可以考虑把一些笔记给放上来。&lt;/p&gt;
</content:encoded></item><item><title>使用node-xlsx来操作表格文件</title><link>https://yuzi.dev/posts/frontend/using-node-xlsx-for-spreadsheets/</link><guid isPermaLink="true">https://yuzi.dev/posts/frontend/using-node-xlsx-for-spreadsheets/</guid><description>闲来无事，就试着玩了一下这个库</description><pubDate>Sun, 11 Sep 2022 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;闲来无事，就试着玩了一下这个库&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import xlsx from &apos;node-xlsx&apos;;
import * as fs from &apos;fs&apos;;
import path from &quot;path&quot;

// const data = [
//   [1, 2, 3],
//   [true, false, null, &apos;sheetjs&apos;],
//   [&apos;foo&apos;, &apos;bar&apos;, new Date(&apos;2014-02-19T14:30Z&apos;), &apos;0.3&apos;],
//   [&apos;baz&apos;, null, &apos;qux&apos;],
// ];
// var buffer = xlsx.build([{ name: &apos;mySheetName&apos;, data: data }]); // Returns a buffer

// fs.writeFileSync(&apos;./test.xlsx&apos;, buffer, { &apos;flag&apos;: &apos;w&apos; });

const options = {
  type: &quot;buffer&quot;,
  cellDates: true,
  cellHTML: false,
  dateNF: &quot;yyyy-mm-dd hh:mm:ss&quot;,
  cellNF: true
};

const workSheetsFromFile = xlsx.parse(`${path.resolve()}/test.xlsx`, options);
console.log(workSheetsFromFile[0].data);
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>链队的应用：连续区间求最小</title><link>https://yuzi.dev/posts/course-notes/monotonic-queue-for-range-minimum/</link><guid isPermaLink="true">https://yuzi.dev/posts/course-notes/monotonic-queue-for-range-minimum/</guid><description>一个数据结构的链队小练习</description><pubDate>Tue, 26 Oct 2021 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;一个数据结构的链队小练习&lt;/p&gt;
&lt;h1&gt;题目&lt;/h1&gt;
&lt;p&gt;例：
2 3 5 6 1 4 7
前4数最小为2，2-5数为1，3-6为1，4-7为1
给出一个数组，给出n，输出每一组n个数的最小值&lt;/p&gt;
&lt;h1&gt;代码&lt;/h1&gt;
&lt;h2&gt;创建链队&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;typedef struct QNode
{
    int data;
    struct QNode *next;
} QNode, *QueuePtr;
typedef struct
{
    QueuePtr front;
    QueuePtr rear;
} Link;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;以很合理的方式创建了一个链队。&lt;/p&gt;
&lt;h2&gt;创建一个类&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;class Quene
{
private:
    Link Q;
    int n;
    int a[100];
    int length;
public:
    Quene(int a[], int length, int n);
    Quene();
    ~Quene();
    void makequeue()
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;用一个类封装了一下所有的数据及函数。
构造函数采用了重载，分为预设数据和手动输入数据。&lt;/p&gt;
&lt;h2&gt;构造函数[从键盘输入数据]&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;Quene::Quene()
{
    length = 0;
    cout &amp;lt;&amp;lt; &quot;\nPlease enter the array:&quot; &amp;lt;&amp;lt; endl;
    while (cin &amp;gt;&amp;gt; a[length])//从键盘获取数组，同时得到数组长度
    {
        length++;
        if (cin.get() == &apos;\n&apos;)
            break;
    }
    cout &amp;lt;&amp;lt; &quot;Please enter n: &quot;;
    cin &amp;gt;&amp;gt; n;
    Q.front = Q.rear = new QNode;
    Q.front-&amp;gt;next = NULL;//初始化链队
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;构造函数[使用默认数据]&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;Quene::Quene(int a[], int length, int n)
{
    Q.front = Q.rear = new QNode;//初始化链队
    Q.front-&amp;gt;next = NULL;
    this-&amp;gt;length = length;
    this-&amp;gt;n = n;
    for (size_t i = 0; i &amp;lt; length; i++)
    {
        this-&amp;gt;a[i] = a[i];//从传入的参数获取数组，长度，n
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;进行比较的函数&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;void makequeue()
    {
        int min = 0;
        for (int i = 0; i &amp;lt; n; i++)//将前n个数据放入链队
        {
            QueuePtr p = new QNode; //入队元素开辟空间
            p-&amp;gt;data = a[i];         //置数1
            p-&amp;gt;next = NULL;
            Q.rear-&amp;gt;next = p; //新节点插入队尾
            Q.rear = p;       //修改新节点指针
            if (!i)
            {
                min = p-&amp;gt;data;
            }
            else if (p-&amp;gt;data &amp;lt; min)//比较出最小的那一个数并输出
            {
                min = p-&amp;gt;data;
            }
        }
        cout &amp;lt;&amp;lt; min &amp;lt;&amp;lt; &quot; &quot;;
        for (size_t i = n; i &amp;lt; length; i++)//对于剩下的数，每一次循环，从队尾新加一个数，将队头的数移出，然后进行比较大小
        {
            /*——————入队——————*/
            QueuePtr p = new QNode; //入队元素开辟空间
            p-&amp;gt;data = a[i];         //置数1
            p-&amp;gt;next = NULL;
            Q.rear-&amp;gt;next = p; //新节点插入队尾
            Q.rear = p;       //修改新节点指针
            /*——————出队——————*/
            p = Q.front-&amp;gt;next;
            Q.front-&amp;gt;next = p-&amp;gt;next; //更新头指针
            p = Q.front-&amp;gt;next;       //为下面的比较大小而重置p
            min = p-&amp;gt;data;           //重置最小值
            while (p-&amp;gt;next)
            {
                if (p-&amp;gt;next-&amp;gt;data &amp;lt; min)
                {
                    min = p-&amp;gt;next-&amp;gt;data;
                }
                p = p-&amp;gt;next;
            }
            cout &amp;lt;&amp;lt; min &amp;lt;&amp;lt; &quot; &quot;;//比较大小，然后再输出最小值，重置参数min，然后进入下一次循环
            min = 0x7fffffff; //最小值置最大；
        }
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;main函数&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;int main()
{
    int a[100] = {2, 3, 4, 5, 6, 114514, 9, 4, 5, 7, 1919810, 655, 66, 4500, 37, 19, 23, 298, 13, 2};
    int length = 20;
    int n = 4;
    Quene q(a, length, n);
    q.makequeue();
    /*————手动输入数据————*/
    Quene manual;
    manual.makequeue();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;总结&lt;/h1&gt;
&lt;p&gt;当元素出队时，可以最小值进行比较，若大于最小值，则下一次循环比较大小时无需将前三项数据进行比较，而是直接用原先的min与入队数据相比较，可以减少比较的次数。&lt;/p&gt;
&lt;p&gt;本篇代码：https://github.com/Yuzi0201/Data-structure/blob/master/2-queue.cpp&lt;/p&gt;
</content:encoded></item><item><title>程序设计课程设计</title><link>https://yuzi.dev/posts/course-notes/programming-course-project/</link><guid isPermaLink="true">https://yuzi.dev/posts/course-notes/programming-course-project/</guid><description>程序设计课程设计 在火车上，经过满是隧道的四川地区，无网络，遂想找点事做，想起因为考试而一直鸽着的程序设计blog，就心血来潮打开笔记本开始记录起来。</description><pubDate>Tue, 20 Jul 2021 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;程序设计课程设计&lt;/h1&gt;
&lt;p&gt;在火车上，经过满是隧道的四川地区，无网络，遂想找点事做，想起因为考试而一直鸽着的程序设计blog，就心血来潮打开笔记本开始记录起来。&lt;/p&gt;
&lt;h2&gt;背景&lt;/h2&gt;
&lt;p&gt;因为我在学期中就大概了解了一下这个课程设计是个什么东西，所以也就提前准备了一些，当然这一切都是在司徒的鼓励【鼓舞？鼓动？】下进行的。司徒给我介绍了QT编程，我大致的了解了一下过后觉得挺合适的，首先它的语言是Cpp，是我目前唯一掌握了的语言，其次它还有个好处就是可以一个代码可以编译到不同的平台上执行。再加上MFC的各种被诟病，我就放弃了这个方向，转而向Qt进发。&lt;/p&gt;
&lt;h2&gt;题目&lt;/h2&gt;
&lt;p&gt;（1）、设计一个学生类Student,包括数据成员：姓名、学号、二门课程(面向对象程序设计、高等数学)的成绩。&lt;/p&gt;
&lt;p&gt;（2）、创建一个管理学生的类Management，包括实现学生的数据的增加、删除、修改、按课程成绩排序、保存学生数据到文件及加载文件中的数据等功能。&lt;/p&gt;
&lt;p&gt;（3）、创建一个基于对话框的MFC应用程序，程序窗口的标题上有你姓名、学号和应用程序名称。使用（1）和（2）中的类，实现对学生信息和成绩的输入和管理。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./a9ac3433.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;（4）、创建一个单文档的MFC应用程序，读取（3）中保存的文件中的学生成绩，分别用直方图和折线方式显示所有学生某课程的成绩分布图。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./bf6c3b9c.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;——————————————————————————————&lt;/p&gt;
&lt;p&gt;以上便是题目，很显然，题目是要求用MFC实现各种功能，我便要用Qt来实现&lt;/p&gt;
&lt;h2&gt;资料查找&lt;/h2&gt;
&lt;p&gt;众所周知，我们这是面向~~百度，谷歌，~~对象编程。而我对GUI编程可谓是一无所知，更别说Qt编程了，所以说，找到一篇别人已经完成的项目，将其读懂并“制作”成自己的项目就成为了我的课设解决方案。【滑稽】&lt;/p&gt;
&lt;p&gt;在我没有查找到想要的项目时，我是十分崩溃的，因为我知道整个思路和实现的方法，但是我却无法将其转化为编程语言将它写出来。既然都说到这了，那就把我的思路写下来吧。&lt;/p&gt;
&lt;h3&gt;实现思路&lt;/h3&gt;
&lt;p&gt;首先我要解决怎么样存储和读取的问题，要存成什么形式？存在哪里？等等等等……经过一番查找，各种项目的方法都是用MySQL或者SQLlite以数据库的形式来存储，这个想法直接被我否决了，其一，这个项目储存的相当于只是一个二维数组，用数据库简直是杀鸡焉用牛刀，大材小用了；其二，数据库我下学期才学，我现在根本不会啊，一周的时间我也没法学啊。&lt;/p&gt;
&lt;p&gt;我的思路是将数据储存在一个txt文件里，就想一个CSV文件一样的排列，遵循着这样的思想，我成功地找到了一个大佬的项目，项目的地址在下方：&lt;/p&gt;
&lt;p&gt;https://github.com/ChangWenhan/StudentManagementSystem-Qt&lt;/p&gt;
&lt;p&gt;我的这个项目就是在他的项目基础上魔改而来。&lt;/p&gt;
&lt;h2&gt;代码详解&lt;/h2&gt;
&lt;p&gt;我的项目地址在下方：&lt;/p&gt;
&lt;p&gt;https://github.com/Yuzi19/GZHU_Program_design_2021_1&lt;/p&gt;
&lt;h3&gt;mainwindow&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;./9503182e.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;main.cpp的作用无须多言，就是启动mainwindow主窗口，其中的东西都是默认的，没有修改。&lt;/p&gt;
&lt;p&gt;mainwindow是整个程序的主窗口，程序的主要功能就在这里执行，下面我详细讲讲每个部件：&lt;/p&gt;
&lt;h4&gt;按钮&lt;/h4&gt;
&lt;p&gt;第一个按钮是刷新按钮。因为程序本身设计的原因，表格中的数据不会在改变后立即变化，所以我设计了这么一个按钮来重载一遍数据，实际上要设计成实时刷新，可以把这个按钮对应的槽函数加到添加和修改删除窗口的析构函数里，应该可以得到这样的效果，如果你感兴趣，可以尝试一下。&lt;/p&gt;
&lt;p&gt;下拉框有四个选项，分别是数学和cpp成绩的正序和倒序，点击排序按钮后，就可以依照下拉框选择的排序方式对表格进行排序。&lt;/p&gt;
&lt;p&gt;直方图按钮点按后，会弹出一个窗口，并根据存储的数据进行直方图绘制。&lt;/p&gt;
&lt;p&gt;增加按钮按下后，会弹出增加数据的窗口&lt;/p&gt;
&lt;p&gt;增加按钮旁边的按钮是一个空按钮，按下并没有什么用，这是因为我把编辑和删除的功能设计成双击表格中的某一项了（下面会讲到）。&lt;/p&gt;
&lt;p&gt;右边的确定和取消按钮，是用来确认是否保存更改的。要讲起它们的作用，就要从构造函数和析构函数讲起了。&lt;/p&gt;
&lt;p&gt;mainwindow的构造函数的一部分：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;QFile::copy(&quot;student.txt&quot;,&quot;student_old.txt&quot;);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这段话的作用是，在程序启动时，复制student.txt到student_old.txt中。&lt;/p&gt;
&lt;p&gt;析构函数的一部分：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;QFile::remove(&quot;student_old.txt&quot;);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这段话是程序退出时，删去备份student_old.txt。&lt;/p&gt;
&lt;p&gt;确定按钮对应的槽函数：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void MainWindow::on_buttonBox_accepted()
{
    this-&amp;gt;close();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;直接关闭窗口，此时的student.txt就是修改后的文件，修改前的文件就随着析构函数的执行而被丢弃。&lt;/p&gt;
&lt;p&gt;取消按钮对应的槽函数：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void MainWindow::on_buttonBox_rejected()
{
    int ret=QMessageBox::question(this,&quot;请确认&quot;,&quot;确定要不保存数据而关闭吗？&quot;,&quot;确认&quot;,&quot;取消&quot;);
    if(ret==0)
    {
        QFile::remove(&quot;student.txt&quot;);
        QFile::rename(&quot;student_old.txt&quot;,&quot;student.txt&quot;);
        this-&amp;gt;close();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;首先会弹出一个确认窗口：&quot;确定要不保存数据而关闭吗？&quot;如果选择取消，下方的if不会被执行，程序不会退出，回到主界面；如果选择确定，修改后的文件将会被废弃，修改前的文件被命名为student.txt，程序关闭，起到了不修改文件的作用。&lt;/p&gt;
&lt;h4&gt;表格&lt;/h4&gt;
&lt;p&gt;位于主窗口正中央的是一个表格，准确来说，是一个QTableView类的部件，差不多就是一个表格，我对它也没有搞得多清楚。&lt;/p&gt;
&lt;p&gt;下面是析构函数的一部分：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;this-&amp;gt;model=new QStandardItemModel;

    this-&amp;gt;model-&amp;gt;setHorizontalHeaderItem(0,new QStandardItem(&quot;学号&quot;));
    this-&amp;gt;model-&amp;gt;setHorizontalHeaderItem(1,new QStandardItem(&quot;姓名&quot;));
    this-&amp;gt;model-&amp;gt;setHorizontalHeaderItem(2,new QStandardItem(&quot;性别&quot;));
    this-&amp;gt;model-&amp;gt;setHorizontalHeaderItem(3,new QStandardItem(&quot;年龄&quot;));
    this-&amp;gt;model-&amp;gt;setHorizontalHeaderItem(4,new QStandardItem(&quot;高数&quot;));
    this-&amp;gt;model-&amp;gt;setHorizontalHeaderItem(5,new QStandardItem(&quot;C++&quot;));

    this-&amp;gt;ui-&amp;gt;tableView-&amp;gt;setModel(model);

    this-&amp;gt;ui-&amp;gt;tableView-&amp;gt;setColumnWidth(0,140);
    this-&amp;gt;ui-&amp;gt;tableView-&amp;gt;setColumnWidth(1,130);
    this-&amp;gt;ui-&amp;gt;tableView-&amp;gt;setColumnWidth(2,100);
    this-&amp;gt;ui-&amp;gt;tableView-&amp;gt;setColumnWidth(3,100);
    this-&amp;gt;ui-&amp;gt;tableView-&amp;gt;setColumnWidth(4,100);
    this-&amp;gt;ui-&amp;gt;tableView-&amp;gt;setColumnWidth(5,105);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;大概可以读出来，他整了个标准的模型，然后给这模型水平第一行分别填入了学号姓名等等。然后他将这个model给了mainwindow的tableView，并设置了每一列的宽度。这些都是在析构函数中的，所以在程序一启动的时候就可以看到第一行。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./435778ff.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h4&gt;主要的函数&lt;/h4&gt;
&lt;h5&gt;readstudentfile()&lt;/h5&gt;
&lt;p&gt;这个函数是将student.txt中的内容读取到由QString组成的QList中，这个Qlist定义在mainwindow的头文件中，是其私有成员，mainwindow.h部分代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;private:
    Ui::MainWindow *ui;
    QList&amp;lt;QString&amp;gt; score_line;
    QStandardItemModel *model;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;readstudentfile()代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int MainWindow::readstudentfile()
{
    score_line.clear();
    QFile file(&quot;student.txt&quot;);
    if(!file.open(QIODevice::ReadOnly|QIODevice::Text))
    {
        return -1;
    }
    QTextStream in(&amp;amp;file);
    while (!in.atEnd())
    {
        QString line=in.readLine();
        score_line.append(line);
    }
    file.close();
    return 0;
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;3行：清空score_line的内容；4行：载入文件；5-8行，如果文件不存在或者无法打开则退出；9行将文字处理函数载入file；12行：定义一个字符串存储file中读出来的一行；13行：并将这一行添加到score_line的结尾；10行：直到读到file末尾为止。&lt;/p&gt;
&lt;h5&gt;display(int row, QStringList score_line)&lt;/h5&gt;
&lt;p&gt;此函数是在read函数执行完后，将score_line读取到的内容显示到tableView中。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void MainWindow::display(int row, QStringList score_line)
{
    int i=0;
    for (i=0;i&amp;lt;score_line.length();i++)
    {
        this-&amp;gt;model-&amp;gt;setItem(row,i,new QStandardItem(score_line.at(i)));
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;要看它的效果，得把按钮的槽函数拿出来一起看&lt;/p&gt;
&lt;h5&gt;刷新按钮的槽函数&lt;/h5&gt;
&lt;pre&gt;&lt;code&gt;void MainWindow::on_pushButton_clicked()
{
    this-&amp;gt;model-&amp;gt;clear();
    reset();
    readstudentfile();
            int i=0,row=0;
            for (i=0;i&amp;lt;score_line.length();i++)
            {
                QString line=score_line.at(i);
                line=line.trimmed();
                QStringList linesplit=line.split(&quot; &quot;);
                        display(row++, linesplit);
            }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;点击刷新后，首先将表格进行了清空，然后对表格进行了初始化【reset函数内容与构造函数的初始化表格代码类似，下面会提到】，然后读取了文件进了score_line，然后将内容通过display显示到表格上。&lt;/p&gt;
&lt;h5&gt;reset()&lt;/h5&gt;
&lt;p&gt;上文提到，reset函数内容与构造函数的初始化表格代码类似。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void MainWindow::reset()
{
    this-&amp;gt;model-&amp;gt;setHorizontalHeaderItem(0,new QStandardItem(&quot;学号&quot;));
    this-&amp;gt;model-&amp;gt;setHorizontalHeaderItem(1,new QStandardItem(&quot;姓名&quot;));
    this-&amp;gt;model-&amp;gt;setHorizontalHeaderItem(2,new QStandardItem(&quot;性别&quot;));
    this-&amp;gt;model-&amp;gt;setHorizontalHeaderItem(3,new QStandardItem(&quot;年龄&quot;));
    this-&amp;gt;model-&amp;gt;setHorizontalHeaderItem(4,new QStandardItem(&quot;高数&quot;));
    this-&amp;gt;model-&amp;gt;setHorizontalHeaderItem(5,new QStandardItem(&quot;C++&quot;));
    this-&amp;gt;ui-&amp;gt;tableView-&amp;gt;setColumnWidth(0,140);
    this-&amp;gt;ui-&amp;gt;tableView-&amp;gt;setColumnWidth(1,115);
    this-&amp;gt;ui-&amp;gt;tableView-&amp;gt;setColumnWidth(2,100);
    this-&amp;gt;ui-&amp;gt;tableView-&amp;gt;setColumnWidth(3,100);
    this-&amp;gt;ui-&amp;gt;tableView-&amp;gt;setColumnWidth(4,100);
    this-&amp;gt;ui-&amp;gt;tableView-&amp;gt;setColumnWidth(5,100);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h5&gt;排序&lt;/h5&gt;
&lt;pre&gt;&lt;code&gt;void MainWindow::on_sort_clicked()
{
    on_pushButton_clicked();
    int flag=this-&amp;gt;ui-&amp;gt;sortway-&amp;gt;currentIndex();
    switch (flag) {
    case 0:
        model-&amp;gt;sort(4,Qt::DescendingOrder);
        break;
    case 1:
        model-&amp;gt;sort(4,Qt::AscendingOrder);
        break;
    case 2:
        model-&amp;gt;sort(5,Qt::DescendingOrder);
        break;
    case 3:
        model-&amp;gt;sort(5,Qt::AscendingOrder);
        break;
    default:
        break;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;首先刷新一下表格，定义flag为当前下拉框所选择的数据，使用switch对应每一种情况。&lt;/p&gt;
&lt;h3&gt;增加学生（addstu）&lt;/h3&gt;
&lt;p&gt;在mainwindow中，有一个按钮可以增加数据，它的槽函数就是启动这个窗口：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void MainWindow::on_add_student_clicked()
{
    addstu *add=new addstu;
    add-&amp;gt;show();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;界面如下图：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./1921334a.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h5&gt;函数&lt;/h5&gt;
&lt;p&gt;取消按钮按下后就是直接关闭窗口，这里不细说了&lt;/p&gt;
&lt;p&gt;确定按钮的槽函数：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void addstu::on_buttonBox_accepted()
{
    QString name=this-&amp;gt;ui-&amp;gt;add_stu_name-&amp;gt;text();
    QString age=this-&amp;gt;ui-&amp;gt;add_stu_age-&amp;gt;text();
    QString number=this-&amp;gt;ui-&amp;gt;add_stu_num-&amp;gt;text();
    QString gender=this-&amp;gt;ui-&amp;gt;add_stu_gender-&amp;gt;text();
    QString math=this-&amp;gt;ui-&amp;gt;add_stu_math-&amp;gt;text();
    QString cpp=this-&amp;gt;ui-&amp;gt;add_stu_cpp-&amp;gt;text();

    QString info=number+&quot; &quot;+name+&quot; &quot;+gender+&quot; &quot;+age+&quot; &quot;+math+&quot; &quot;+cpp;

    bool charge=name.length()&amp;lt;1||number.length()&amp;lt;1||gender.length()&amp;lt;1||
                age.length()&amp;lt;1||math.length()&amp;lt;1||cpp.length()&amp;lt;1;

    if(charge==1)
    {
        QMessageBox::critical(this,&quot;错误&quot;,&quot;信息填写不完整，请检查&quot;,&quot;确定&quot;);
    }
    else
    {
        QFile mFile(&quot;student.txt&quot;);
        if(!mFile.open(QIODevice::Append|QIODevice::Text))
        {
            QMessageBox::critical(this,&quot;错误&quot;,&quot;文件打开失败，信息没有写入&quot;,&quot;确认&quot;);
            return;
        }
        QTextStream out(&amp;amp;mFile);
        out&amp;lt;&amp;lt;info&amp;lt;&amp;lt;&quot;\n&quot;;
        mFile.flush();
        mFile.close();
        return;
    }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;首先将框中的所有数据分别赋值到对应的字符串上，再将这些字符以空格隔开，组成一个新的字符串“info”，然后在检查数据无误，文件无误后，将其写入文件的新的一行。&lt;/p&gt;
&lt;h3&gt;编辑和删除数据&lt;/h3&gt;
&lt;h4&gt;触发方法&lt;/h4&gt;
&lt;p&gt;上文提到，进入删除与编辑框是双击表格中的项目触发的，在mainwindow中的槽函数是：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void MainWindow::on_tableView_doubleClicked(const QModelIndex &amp;amp;index)
{
    int row=this-&amp;gt;ui-&amp;gt;tableView-&amp;gt;currentIndex().row();
    num1=model-&amp;gt;data(model-&amp;gt;index(row,0)).toString();
    name1=model-&amp;gt;data(model-&amp;gt;index(row,1)).toString();
    gender1=model-&amp;gt;data(model-&amp;gt;index(row,2)).toString();
    age1=model-&amp;gt;data(model-&amp;gt;index(row,3)).toString();
    math1=model-&amp;gt;data(model-&amp;gt;index(row,4)).toString();
    cpp1=model-&amp;gt;data(model-&amp;gt;index(row,5)).toString();
    
    change_and_del a;
	a.exec();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;首先row指代当前选中的行，然后将这一行中的数据全部赋值给变量，然后启动并显示编辑删除窗口&lt;/p&gt;
&lt;p&gt;在mainwindow.h中有如下定义：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;extern QString name1;
extern QString age1;
extern QString num1;
extern QString gender1;
extern QString math1;
extern QString cpp1;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这使得这些变量在编辑删除窗口类中也可以被使用。&lt;/p&gt;
&lt;h4&gt;函数&lt;/h4&gt;
&lt;h5&gt;构造函数&lt;/h5&gt;
&lt;p&gt;部分构造函数如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;	this-&amp;gt;ui-&amp;gt;edit_stu_num-&amp;gt;setText(num1);
    this-&amp;gt;ui-&amp;gt;edit_stu_name-&amp;gt;setText(name1);
    this-&amp;gt;ui-&amp;gt;edit_stu_gender-&amp;gt;setText(gender1);
    this-&amp;gt;ui-&amp;gt;edit_stu_age-&amp;gt;setText(age1);
    this-&amp;gt;ui-&amp;gt;edit_stu_math-&amp;gt;setText(math1);
    this-&amp;gt;ui-&amp;gt;edit_stu_cpp-&amp;gt;setText(cpp1);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这将双击选中的学生的数据都写入了编辑框中&lt;/p&gt;
&lt;h5&gt;确认按钮槽函数（重要！！）&lt;/h5&gt;
&lt;p&gt;21-7-18的时候，我在写这一段文字，距离我写代码快过去了20天，以至于我都搞不明白这段代码的意思，在图书馆研读了半个小时我才搞清楚，代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void change_and_del::on_buttonBox_accepted()
{
    QString name=this-&amp;gt;ui-&amp;gt;edit_stu_name-&amp;gt;text();
    QString age=this-&amp;gt;ui-&amp;gt;edit_stu_age-&amp;gt;text();
    QString number=this-&amp;gt;ui-&amp;gt;edit_stu_num-&amp;gt;text();
    QString gender=this-&amp;gt;ui-&amp;gt;edit_stu_gender-&amp;gt;text();
    QString math=this-&amp;gt;ui-&amp;gt;edit_stu_math-&amp;gt;text();
    QString cpp=this-&amp;gt;ui-&amp;gt;edit_stu_cpp-&amp;gt;text();

    QString info=number+&quot; &quot;+name+&quot; &quot;+gender+&quot; &quot;+age+&quot; &quot;+math+&quot; &quot;+cpp;

    bool charge=name.length()&amp;lt;1||number.length()&amp;lt;1||gender.length()&amp;lt;1||
                age.length()&amp;lt;1||math.length()&amp;lt;1||cpp.length()&amp;lt;1;

    if(charge==1)
    {
        QMessageBox::critical(this,&quot;错误&quot;,&quot;信息填写不完整，请检查&quot;,&quot;确定&quot;);
    }
    else
    {
        if(readstudentfile()==-1)
        {
             this-&amp;gt;close();
             QMessageBox::critical(this,&quot;错误&quot;,&quot;文件读取失败，信息没有删除&quot;,&quot;确认&quot;);
        }
        else
        {
           int i=0;
           for (i=0;i&amp;lt;score_line.length();i++)
           {
               QString line=score_line.at(i);
               line=line.trimmed();
               QStringList linesplit=line.split(&quot; &quot;);
               if(num1!=linesplit.at(0))
               {
                    QFile file(&quot;student_temp.txt&quot;);
                    if(!file.open(QIODevice::Append|QIODevice::Text))
                    {
                        QMessageBox::critical(this,&quot;错误&quot;,&quot;文件打开失败，信息没有修改&quot;,&quot;确认&quot;);
                        return;
                    }
                    QTextStream out(&amp;amp;file);
                    out&amp;lt;&amp;lt;line+&quot;\n&quot;;
                    file.close();
                }
               else
               {
                   QFile file(&quot;student_temp.txt&quot;);
                   if(!file.open(QIODevice::Append|QIODevice::Text))
                   {
                       QMessageBox::critical(this,&quot;错误&quot;,&quot;文件打开失败，信息没有修改&quot;,&quot;确认&quot;);
                       return;
                   }
                   QTextStream out(&amp;amp;file);
                   out&amp;lt;&amp;lt;info+&quot;\n&quot;;
                   file.flush();
                   file.close();
               }
             }
         QFile file_old(&quot;student.txt&quot;);
         QFile file_new(&quot;student_temp.txt&quot;);
         if (file_old.exists())
         {
           file_old.remove();
           file_new.rename(&quot;student.txt&quot;);
         }
         else
         {
           QMessageBox::critical(this,&quot;错误&quot;,&quot;未有信息保存为文件，无法修改&quot;,&quot;确认&quot;);
         }
         QMessageBox::information(this,&quot;通知&quot;,&quot;修改成功！&quot;,&quot;确认&quot;);
         this-&amp;gt;close();
         }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;首先，3-10行，将数据从框中读取并赋值给变量。然后是常规的确认文件，无须多言，下面是重点：&lt;/p&gt;
&lt;p&gt;29行的for循环，循环次数是文件的行数，也就是学生的个数。&lt;/p&gt;
&lt;p&gt;然后将当前行【第i行】的内容写入line中，然后创建了一个temp的空文件，34行的if条件是学号是否对应，34-57行的内容是，若不是本次修改的行，就照抄原来的那一行（也就是将源文件复制过来），如果是本次修改的行，则把上面已经准备好的info写进去。&lt;/p&gt;
&lt;p&gt;60-66行将temp转正同时删去旧文件。&lt;/p&gt;
&lt;p&gt;我这样修改后的写法，就可以使修改后的学生保持在原位，原来的写法是将旧的文件除了修改的行复制进temp后，在将修改后的行加上。这样的话，一旦修改了数据，那一行就会被置于文件的末尾。&lt;/p&gt;
&lt;h5&gt;删除按钮槽函数&lt;/h5&gt;
&lt;pre&gt;&lt;code&gt;void change_and_del::on_del_Button_clicked()
{
    readstudentfile();
    int i=0;
    int ret=QMessageBox::question(this,&quot;请确认&quot;,&quot;确定要删除吗？&quot;,&quot;确认&quot;,&quot;取消&quot;);
    if(ret==1)
    {
    }
    else
        {
            for (i=0;i&amp;lt;score_line.length();i++)
            {
                QString line=score_line.at(i);
                line=line.trimmed();
                QStringList linesplit=line.split(&quot; &quot;);
                if(num1!=linesplit.at(0))
                {
                    QFile file(&quot;student_temp.txt&quot;);
                    if(!file.open(QIODevice::Append|QIODevice::Text))
                    {
                       QMessageBox::critical(this,&quot;错误&quot;,&quot;文件打开失败，信息没有写入&quot;,&quot;确认&quot;);
                       return;
                    }
                QTextStream out(&amp;amp;file);
                out&amp;lt;&amp;lt;line+&quot;\n&quot;;
                file.close();
                }
            }
            QFile file_old(&quot;student.txt&quot;);
            QFile file_new(&quot;student_temp.txt&quot;);
            if (file_old.exists())
            {
               file_old.remove();
               file_new.rename(&quot;student.txt&quot;);
            }
            else
            {
               QMessageBox::critical(this,&quot;错误&quot;,&quot;未有信息保存为文件，无法删除&quot;,&quot;确认&quot;);
            }
            QMessageBox::information(this,&quot;通知&quot;,&quot;删除成功！&quot;,&quot;确认&quot;);
            this-&amp;gt;close();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;有了上面编辑的经验，这个就很简单了，其实上面编辑功能已经实现了删除功能了，只是加上了else写入了新的数据而已。删除的函数就没有这样，从16行的if就可以知道原理：将原文件除了选中行都复制过来，最终效果就是那一行消失啦！&lt;/p&gt;
&lt;p&gt;最后附上一张截图：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./01efb8db.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;直方图&lt;/h3&gt;
&lt;h4&gt;定义&lt;/h4&gt;
&lt;p&gt;在类私有区域定义了如下数据&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int P1A[5]={0},P1B[5]={0};
QList&amp;lt;QString&amp;gt; score_line;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;第一个数组是存放数学0-59，60-69，70-79，80-89，90-100的人数的，第二个数组是存放cpp的人数的。&lt;/p&gt;
&lt;h4&gt;计算人数&lt;/h4&gt;
&lt;p&gt;函数如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void histogram::calc()
{
    readstudentfile();
    for (int i=0;i&amp;lt;score_line.length();i++)
    {
        QString line=score_line.at(i);
        line=line.trimmed();
        QStringList linesplit=line.split(&quot; &quot;);
        int a=linesplit.at(4).toInt(); //提取数学成绩并转为int
        if(a&amp;lt;60)
            P1A[0]++;
        else if(a&amp;lt;70)
            P1A[1]++;
        else if(a&amp;lt;80)
            P1A[2]++;
        else if(a&amp;lt;90)
            P1A[3]++;
        else
            P1A[4]++;
    }
    for (int i=0;i&amp;lt;score_line.length();i++)
    {
        QString line=score_line.at(i);
        line=line.trimmed();
        QStringList linesplit=line.split(&quot; &quot;);
        int a=linesplit.at(5).toInt(); //提取cpp成绩转为int
        if(a&amp;lt;60)
            P1B[0]++;
        else if(a&amp;lt;70)
            P1B[1]++;
        else if(a&amp;lt;80)
            P1B[2]++;
        else if(a&amp;lt;90)
            P1B[3]++;
        else
            P1B[4]++;
    }

}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;首先读取数据，逐行循环，读取每个人的数学/cpp成绩，并进行判断，并将其加入数组的相应位置中。注意9行和26行的赋值时，使用了QString的转为整数功能toInt()，使之可以赋给int类型的变量。&lt;/p&gt;
&lt;p&gt;**可以优化的地方：**上下两个for循环完全可以合并为一个&lt;/p&gt;
&lt;h4&gt;绘图&lt;/h4&gt;
&lt;p&gt;函数如下，具体可以看注释：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void histogram::paintEvent(QPaintEvent *event)
{
    Q_UNUSED(event);

    QPainter painter(this);
    painter.setPen(QColor(0,0,0));

    //绘制直方图 远点50，400，单位高度50 单位宽度20
    painter.drawLine(50,400,750,400);     //x轴 单位长10，30个单位，总长700
    painter.drawLine(50,400,50,50);      //y轴 单位50，5个单位，总长350
    painter.drawLine(50,50,45,55);      //上箭头
    painter.drawLine(50,50,55,55);      //上箭头
    painter.drawLine(745,395,750,400);    //右箭头
    painter.drawLine(745,405,750,400);    //右箭头
    int xi = 40;            //单位长度x
    int yi = 50;            //单位长度y
    int u = 3;              //刻度的长度
    //画y轴的刻度
    for(int i=0;i&amp;lt;=6;i++)
    {
        painter.drawLine(50,400-yi*i,50+u,400-yi*i);        //画刻度线
        painter.drawText(QPoint(40,403-yi*i),QString::number(i));   //画刻度数字
    }
    //画x轴的刻度
    for(int i=1;i&amp;lt;=5;i++)
    {
        painter.drawLine(40+xi*3*i,400,40+xi*3*i,403);    //画刻度线
    }
    painter.drawText(QPoint(30+xi*3*1,420),&quot;0-59&quot;); //画刻度1数字
    painter.drawText(QPoint(30+xi*3*2,420),&quot;60-69&quot;); //画刻度2数字
    painter.drawText(QPoint(30+xi*3*3,420),&quot;70-79&quot;); //画刻度3数字
    painter.drawText(QPoint(30+xi*3*4,420),&quot;80-89&quot;); //画刻度4数字
    painter.drawText(QPoint(30+xi*3*5,420),&quot;90-100&quot;); //画刻度5数字

    //数学成绩直方图
    painter.setBrush(QColor(62,147,192));
    for(int i=0;i&amp;lt;5;i++)
    {
        painter.drawRect(40+xi*3*(i+1)-40,400-P1A[i]*yi,40,P1A[i]*yi);
    }
    //cpp成绩直方图
    painter.setBrush(QColor(62,102,149));
    for(int i=0;i&amp;lt;5;i++)
    {
        painter.drawRect(40+xi*3*(i+1),400-P1B[i]*yi,40,P1B[i]*yi);
    }



    //给出说明
    painter.drawText(QPoint(360,520),&quot;说明：&quot;);
    painter.drawText(QPoint(450,520),&quot;数学&quot;);
    painter.drawText(QPoint(450,570),&quot;C++&quot;);
    painter.setBrush(QColor(62,147,192));
    painter.drawRect(400,500,30,30);
    painter.setBrush(QColor(62,102,149));
    painter.drawRect(400,550,30,30);


 }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;简而言之，就是使用QPainter进行绘图，39行和45行是画柱子的函数，这个是函数的功能是画一个填充了颜色的矩形，后面的四个参数是左上和右下两个点的坐标，公式看起来很复杂，但是如果你稍加思考理清计算统计的思路便可知道是怎么画出来的。&lt;/p&gt;
&lt;p&gt;最后附上一张截图。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./5dae74a2.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;需要改进的地方&lt;/h2&gt;
&lt;p&gt;首先呢，这是一次课设，它没有十分具体的要求，所以有些限制措施我就没有写，这就有可能导致一些问题&lt;/p&gt;
&lt;p&gt;1.没有限制输入的内容：首先，程序的运行是基于空格来判断数据之间的间隔，如果输入的东西里面包含空格的话，就会产生意想不到的结果，比如：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./03e9380c.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;这里我在年龄那里打了个空格，然后呈现在表中就是这样：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./c0ad1395.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;多出来了一个并不存在的第七行，至于为什么会这样，相信读到这里的你一定知道是什么原因了吧。&lt;/p&gt;
&lt;p&gt;其次，学号位数啊什么什么的都么有加以限制，只要都写了东西就通过。也没对分数区间加以限制，1000分都可以。&lt;/p&gt;
&lt;p&gt;2.未对学号单一性进行限制：在修改的时候都是根据学号来定位要修改的行，如果输入了两个学号一样的学生就会修改错误【比如改下面的变成了上面的】（PS：现实生活中也没有一个学校里面学号相同的学生吧）&lt;/p&gt;
&lt;p&gt;3.没有写源文件丢失的措施：这个加上其实很简单，但是要加上去很繁琐我就没有搞，如果程序运行时student.txt没了程序会报错无法运行，但程序不会自己解决，这个可以在构造函数以及在每个判断文件是否存在的地方加上一些东西，具体的由读者来探索吧。&lt;/p&gt;
&lt;p&gt;差不多就这些了，有疑问欢迎来GitHub发私信或者邮箱交流，O(∩_∩)O谢谢！&lt;/p&gt;
&lt;p&gt;项目地址：https://github.com/Yuzi19/GZHU_Program_design_2021_1&lt;/p&gt;
</content:encoded></item><item><title>运算符重载</title><link>https://yuzi.dev/posts/course-notes/operator-overloading/</link><guid isPermaLink="true">https://yuzi.dev/posts/course-notes/operator-overloading/</guid><description>好久没写博客了，趁今天无聊，学习了一下怎样用github与vs连接，用git来管理代码，然后翻出来上周的面向对象的运算符重载的code，遂想着写一下博客</description><pubDate>Sun, 28 Mar 2021 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;好久没写博客了，趁今天无聊，学习了一下怎样用github与vs连接，用git来管理代码，然后翻出来上周的面向对象的运算符重载的code，遂想着写一下博客&lt;/p&gt;
&lt;h1&gt;题目&lt;/h1&gt;
&lt;p&gt;有两个均为m行n列的矩阵A，B，求其和，赋值给C，重载“+”、“&amp;gt;&amp;gt;”、“=”、和“&amp;lt;&amp;lt;”&lt;/p&gt;
&lt;p&gt;&lt;s&gt;幸好是加法，如果是乘法直接裂开&lt;/s&gt;&lt;/p&gt;
&lt;h1&gt;初版&lt;/h1&gt;
&lt;p&gt;见：https://github.com/Yuzi19/Object-oriented-programming/blob/master/matrix_reload/old/0.1.cpp&lt;/p&gt;
&lt;p&gt;此处代码运行了就会出大问题（实际上各种小问题也出过，但是在经过我的各种努力过后也能解决了）&lt;/p&gt;
&lt;h2&gt;各个重载函数&lt;/h2&gt;
&lt;h3&gt;+&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;matrix operator+(const matrix &amp;amp;A, const matrix &amp;amp;B)
{
	//matrix C(A.m, A.n);
	double **T;
	T = new double*[A.m];
	for (int i = 0; i &amp;lt; A.m; i++)
	{
		T[i] = new double[A.n];
	}
	for (int i = 0; i &amp;lt; A.m; i++)
	{
		for (int k = 0; k &amp;lt; A.n; k++)
		{
			//C.a[i][k] = A.a[i][k] + B.a[i][k];
			T[i][k] = A.a[i][k] + B.a[i][k];
		}
	}
	//return C; 不能像注释部分这样写，因为一旦出了函数就会调用析构函数，就会导致赋值时出错！！！（找不到指针）
	return matrix(T, A.m, A.n); //此处重载了构造函数
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;不需要看注释，注释的原因写错了，这两种写法都可以，错误不在于析构函数，而是在于没有写赋值构造函数！！（这一点下面要讲）&lt;/p&gt;
&lt;p&gt;原理就是把两项的每一项相加&lt;/p&gt;
&lt;h3&gt;=&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;matrix&amp;amp; matrix::operator=(const matrix &amp;amp; A)
{
	m = A.m; n = A.n;
	for (int i = 0; i &amp;lt; A.m; i++)
	{
		for (int k = 0; k &amp;lt; A.n; k++)
		{
			a[i][k] = A.a[i][k];
		}
	}
	return *this;
	// TODO: 在此处插入 return 语句
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其中的m，n都是被赋值的类的东西，最后返回的也是被赋值的类的地址&lt;/p&gt;
&lt;h3&gt;&amp;lt;&amp;lt;&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;ostream&amp;amp; operator&amp;lt;&amp;lt;(ostream&amp;amp; output, const matrix&amp;amp; A)
{
	for (int i = 0; i &amp;lt; A.m; i++)
	{
		for (int k = 0; k &amp;lt; A.n; k++)
		{
			cout &amp;lt;&amp;lt; A.a[i][k] &amp;lt;&amp;lt; &quot;  &quot;;
		}
		cout &amp;lt;&amp;lt; endl;
	}
	return output;
	// TODO: 在此处插入 return 语句
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;&amp;gt;&amp;gt;&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;istream&amp;amp; operator&amp;gt;&amp;gt;(istream&amp;amp; input, matrix&amp;amp; A)
{

	for (int i = 0; i &amp;lt; A.m; i++)
	{
		for (int k = 0; k &amp;lt; A.n; k++)
		{
			cin &amp;gt;&amp;gt; A.a[i][k];
		}
	}
	return input;
	// TODO: 在此处插入 return 语句
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;十分的普通和正常，就是把输入输出改成了依次输出矩阵&lt;/p&gt;
&lt;h2&gt;构造函数和析构函数&lt;/h2&gt;
&lt;p&gt;这里的矩阵就是用二维数组来存储的，所以构造和析构要参照二级数组的方法【顺便还可以弄成动态数组】&lt;/p&gt;
&lt;h3&gt;构造&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;matrix::matrix(int m, int n)
{
	this-&amp;gt;m = m; this-&amp;gt;n = n;
	a = new double*[m];
	for (int i = 0; i &amp;lt; m; i++)
	{
		a[i] = new double[n];
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;a是一个有m个元素的数组，每一个元素都是一个double类型的指针，然后开始for，让每个元素的指针都指向一个double类型的数组，这样，一个二维数组就完成了&lt;/p&gt;
&lt;h3&gt;析构&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;matrix::~matrix()
{
	for (int i = 0; i &amp;lt; m; i++)
	{
		if (a[i] != NULL)
		{
			delete[]a[i];
			a[i] = NULL;
		}
	}
	delete[]a;
	a = NULL;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;没啥好说的，注意一下格式&lt;/p&gt;
&lt;h1&gt;老师的改版&lt;/h1&gt;
&lt;p&gt;那么，问题出在哪里呢？？&lt;/p&gt;
&lt;p&gt;原来是————没有复制构造函数！！！！&lt;/p&gt;
&lt;p&gt;因为这里用到了数组，有数组就有指针，有指针就不能用系统自带的浅复制！！！因为浅复制把地址也给过去了，就会报错！！&lt;/p&gt;
&lt;h2&gt;复制构造函数&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;matrix::matrix(const matrix&amp;amp; M) {
	this-&amp;gt;m = M.m; this-&amp;gt;n = M.n;
	this-&amp;gt;a = new double*[m];
	for (int i = 0; i &amp;lt; m; i++)
	{
		this-&amp;gt;a[i] = new double[n];
	}
	for (int i = 0; i &amp;lt; m; i++) {
		for (int j = 0; j &amp;lt; n; j++)
			this-&amp;gt;a[i][j] = M.a[i][j];
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;复制构造函数实际上就是重载了一个构造函数，参数是一个类，这里就是重新创了内存空间，然后把值一个个地赋给新空间里，就不会有浅复制的错误了！！&lt;/p&gt;
&lt;p&gt;代码详见：https://github.com/Yuzi19/Object-oriented-programming/blob/master/matrix_reload/matrix_reload.cpp&lt;/p&gt;
&lt;h1&gt;最后&lt;/h1&gt;
&lt;p&gt;跑一下程序，答案正确！！下一次不要忘记了复制构造函数哦！&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://imgtu.com/i/cpzaxe&quot;&gt;&lt;img src=&quot;./737bb3aa.png&quot; alt=&quot;cpzaxe.png&quot; /&gt;&lt;/a&gt;&lt;/p&gt;
</content:encoded></item><item><title>指针调用函数实例</title><link>https://yuzi.dev/posts/course-notes/function-pointer-call-examples/</link><guid isPermaLink="true">https://yuzi.dev/posts/course-notes/function-pointer-call-examples/</guid><description>思修课无聊搞出来的东西</description><pubDate>Thu, 10 Dec 2020 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;思修课无聊搞出来的东西&lt;/p&gt;
&lt;p&gt;题目:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./db7dd539.jpg&quot; alt=&quot;rFsWfs.jpg&quot; /&gt;&lt;/p&gt;
&lt;p&gt;手撸代码&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;iostream&amp;gt;
#include &amp;lt;cmath&amp;gt;
using namespace std;
double Tn(double a, double b, double n,int sumtype);
double f1(double x);
double f2(double x);
double f3(double x);
const int PI = 3.1415926;

int main()
{
	double n, a, b;int sumtype;
	cout &amp;lt;&amp;lt; &quot;Please enter a b n,sum 1 or 2 or 3&quot; &amp;lt;&amp;lt; endl;
	cin &amp;gt;&amp;gt; b &amp;gt;&amp;gt; a &amp;gt;&amp;gt; n &amp;gt;&amp;gt; sumtype;
	double result = Tn(a, b, n,sumtype);
	cout &amp;lt;&amp;lt; &quot;result is &quot; &amp;lt;&amp;lt; result &amp;lt;&amp;lt; endl;
}

double Tn(double a, double b,double n, int sumtype)
{
	double h = (b - a) / n;
	double (*f)(double);
	switch (sumtype)
	{
	case 1: f = f1; break;
	case 2: f = f2; break;
	case 3: f = f3; break;
	default: abort();
		break;
	}
	double fa = f(a); double fb = f(b); double sum = 0;
	for (int i=1; i &amp;lt; n; i++)
	{
		sum += f(a + i*h);
	}
	return h * (f(a) + f(b)) / 2 + h * sum;
}

double f1(double x)
{
	return 4 / (1 + x * x);
}

double f2(double x)
{
	return sqrt(1 + x * x);
}

double f3(double x)
{
	return sin(x);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注意，22行定义指针时，要带上形参(double)，且要用(*p)，因为优先级的原因，否则会报错!!!&lt;/p&gt;
&lt;p&gt;然后就可以愉快地求积分啦!再也不用担心数学作业不会啦!(逃——)&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./31cf5eb6.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;这个是第三个函数的结果，可以发现，在π精度很高，和切片n很多的时候，得到了正确的积分::one:&lt;/p&gt;
</content:encoded></item><item><title>动态数组-杨辉三角</title><link>https://yuzi.dev/posts/course-notes/dynamic-array-yanghui-triangle/</link><guid isPermaLink="true">https://yuzi.dev/posts/course-notes/dynamic-array-yanghui-triangle/</guid><description>浅谈一下这个有点复杂的杨辉三角</description><pubDate>Tue, 08 Dec 2020 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;浅谈一下这个有点复杂的杨辉三角&lt;/p&gt;
&lt;h2&gt;引入&lt;/h2&gt;
&lt;p&gt;二项式(a+b)^n^展开式系数由n决定，有n+1个系数&lt;/p&gt;
&lt;p&gt;1&lt;/p&gt;
&lt;p&gt;1 1&lt;/p&gt;
&lt;p&gt;1 2 1&lt;/p&gt;
&lt;p&gt;1 3 3 1&lt;/p&gt;
&lt;p&gt;1 4 6 4 1&lt;/p&gt;
&lt;p&gt;1 5 10 10 5 1&lt;/p&gt;
&lt;p&gt;…………&lt;/p&gt;
&lt;p&gt;由于每行元素都是在上一行的基础上计算出来的，因此可以用一维数组进行迭代。数组长度是根据二项式的幂次决定的，所以在程序中使用动态数组。以下程序输出二项展开式 n 次幂的系数表。&lt;/p&gt;
&lt;h2&gt;书上程序&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;iostream&amp;gt;
using namespace std;
void yhtriangle(int * const, int);

int main()
{
	int n, *yh;
	do
	{
		cout &amp;lt;&amp;lt; &quot;input n&quot; &amp;lt;&amp;lt; endl;
		cin &amp;gt;&amp;gt; n;
	} while (n&amp;lt;0||n&amp;gt;20);  //此段保证n处于1-19之间
	yh = new int[n + 1]; //创建n+1的数的动态数组
	yhtriangle(yh, n);
	delete[]yh;
	yh = NULL; //释放
}

void yhtriangle(int * const py, int pn)
{
	int i, j, k;
	py[0] = 1;//第一项的值为1
	cout &amp;lt;&amp;lt; py[0] &amp;lt;&amp;lt; endl;//0次幂的系数
	for (i = 1; i &amp;lt; pn + 1; i++)//依次输出n=1,2,3……到n的结果
	{
		py[i] = 1; //最后一项是1
		for (j = i - 1; j &amp;gt; 0; j--)
			py[j] = py[j - 1] + py[j]; //难理解句！！注意：等式右边是上一行的！左边是本行的！！
		for (k = 0; k &amp;lt;= i; k++)
			cout &amp;lt;&amp;lt; py[k] &amp;lt;&amp;lt; &quot; &quot;; //输出结果
		cout &amp;lt;&amp;lt; endl;
	}
}

&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;解释&lt;/h2&gt;
&lt;p&gt;最难以理解的是第28行，也就是程序的核心，那我们先从算法来看每一项是怎么来的&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./fa127f84.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;这就是一个杨辉三角，跟着程序走一下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void yhtriangle(int * const py, int pn)
{
	int i, j, k;
	py[0] = 1;//第一项的值为1
	cout &amp;lt;&amp;lt; py[0] &amp;lt;&amp;lt; endl;//0次幂的系数

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;此时，数组第0项为1，第一行已经输出了&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;
	for (i = 1; i &amp;lt; pn + 1; i++)//依次输出n=1,2,3……到n的结果
	{
		py[i] = 1; //最后一项是1
		for (j = i - 1; j &amp;gt; 0; j--)
			py[j] = py[j - 1] + py[j]; //难理解句！！注意：等式右边是上一行的！左边是本行的！！
		for (k = 0; k &amp;lt;= i; k++)
			cout &amp;lt;&amp;lt; py[k] &amp;lt;&amp;lt; &quot; &quot;; //输出结果
		cout &amp;lt;&amp;lt; endl;
	}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;进入循环，i=1，开始第二行的操作&lt;/p&gt;
&lt;p&gt;首先将第二个数赋值1，进入循环，因为j=0，不执行循环&lt;/p&gt;
&lt;p&gt;第二行输出，结束。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;注意，结束后数组值未清空，所以我们可以利用上一行数据来算下一行！！&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;第3行，i=2，&lt;/p&gt;
&lt;p&gt;第三项为1,&lt;/p&gt;
&lt;p&gt;第二项，由第二行的第二项与第一项相加而来！！&lt;/p&gt;
&lt;p&gt;第一项不变&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./1b6eb516.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;第四行亦是如此&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./8ecb04e5.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
&lt;p&gt;那么问题就解决啦&lt;/p&gt;
&lt;p&gt;&lt;a&gt;&lt;img src=&quot;./4c2819fc.png&quot; alt=&quot;rSlBeH.png&quot; /&gt;&lt;/a&gt;&lt;/p&gt;
</content:encoded></item><item><title>一个选择排序函数</title><link>https://yuzi.dev/posts/course-notes/selection-sort-function/</link><guid isPermaLink="true">https://yuzi.dev/posts/course-notes/selection-sort-function/</guid><description>Mark一下</description><pubDate>Tue, 01 Dec 2020 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Mark一下&lt;/p&gt;
&lt;h2&gt;原理&lt;/h2&gt;
&lt;p&gt;每一趟从待排序的&lt;a href=&quot;https://baike.baidu.com/item/%E6%95%B0%E6%8D%AE%E5%85%83%E7%B4%A0&quot;&gt;数据元素&lt;/a&gt;中选出最小（或最大）的一个元素，顺序放在已排好序的数列的最后，直到全部待排序的数据元素排完。&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://baike.baidu.com/item/%E9%80%89%E6%8B%A9%E6%8E%92%E5%BA%8F&quot;&gt;选择排序&lt;/a&gt;是不稳定的排序方法(很多教科书都说选择排序是不稳定的，但是，完全可以将其实现成稳定的排序方法)。&lt;/p&gt;
&lt;p&gt;n个记录的文件的&lt;a href=&quot;https://baike.baidu.com/item/%E7%9B%B4%E6%8E%A5%E9%80%89%E6%8B%A9%E6%8E%92%E5%BA%8F&quot;&gt;直接选择排序&lt;/a&gt;可经过n-1趟直接选择排序得到有序结果：&lt;/p&gt;
&lt;p&gt;①初始状态：无序区为R[1..n]，有序区为空。&lt;/p&gt;
&lt;p&gt;②第1趟排序&lt;/p&gt;
&lt;p&gt;在无序区R[1..n]中选出关键字最小的记录R[k]，将它与无序区的第1个记录R[1]交换，使R[1..1]和R[2..n]分别变为记录个数增加1个的新有序区和记录个数减少1个的新无序区。&lt;/p&gt;
&lt;p&gt;……&lt;/p&gt;
&lt;p&gt;③第i趟排序&lt;/p&gt;
&lt;p&gt;第i趟排序开始时，当前有序区和无序区分别为R[1..i-1]和R(1≤i≤n-1)。该趟排序从当前无序区中选出关键字最小的记录 R[k]，将它与无序区的第1个记录R交换，使R[1..i]和R分别变为记录个数增加1个的新有序区和记录个数减少1个的新无序区。&lt;/p&gt;
&lt;p&gt;这样，n个记录的文件的&lt;a href=&quot;https://baike.baidu.com/item/%E7%9B%B4%E6%8E%A5%E9%80%89%E6%8B%A9%E6%8E%92%E5%BA%8F&quot;&gt;直接选择排序&lt;/a&gt;可经过n-1趟直接选择排序得到有序结果。&lt;/p&gt;
&lt;h3&gt;优劣&lt;/h3&gt;
&lt;p&gt;优点：移动数据的次数已知（n-1次）。&lt;/p&gt;
&lt;p&gt;缺点：比较次数多，不稳定。&lt;/p&gt;
&lt;h2&gt;函数&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;void compositor(int w,int x, int y,int z)
{
	int a[4], i, j, temp, b;
	a[0] = w; a[1] = x; a[2] = y; a[3] = z;
	for (i = 0; i &amp;lt; 4 - 1; i++)
	{
		temp = i;
		for (j = i + 1; j &amp;lt; 4; j++)
		{
			if (a[temp] &amp;gt; a[j])
				temp = j;
		}
		if (i != temp)
		{
			b = a[temp];
			a[temp] = a[i];
			a[i] = b;
		}
	}
	for (i = 0; i &amp;lt; 4; i++)
		cout &amp;lt;&amp;lt; a[i]&amp;lt;&amp;lt;&quot; &quot;;
	cout &amp;lt;&amp;lt; endl;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;本函数是给4个数排序后输出，该函数可根据实际情况更改&lt;/p&gt;
</content:encoded></item><item><title>关于汉诺塔问题的一些思考</title><link>https://yuzi.dev/posts/course-notes/some-thoughts-on-tower-of-hanoi/</link><guid isPermaLink="true">https://yuzi.dev/posts/course-notes/some-thoughts-on-tower-of-hanoi/</guid><description>最开始我以为这是个很简单的问题，结果后面我发现我看错题了………………</description><pubDate>Tue, 17 Nov 2020 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;最开始我以为这是个很简单的问题，结果后面我发现我看错题了………………&lt;/p&gt;
&lt;h2&gt;题目&lt;/h2&gt;
&lt;p&gt;汉诺塔(Tower of Hanoi)源于印度传说中，大梵天创造世界时造了三根金钢石柱子，其中一根柱子自底向上叠着64片黄金圆盘。大梵天命令婆罗门把圆盘从下面开始按大小顺序重新摆放在另一根柱子上。并且规定，在小圆盘上不能放大圆盘，在三根柱子之间&lt;strong&gt;一次只能移动一个圆盘。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;&lt;strong&gt;注意！一次只能移动一个圆盘！！&lt;/strong&gt;&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;初探&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;./32693f88.jpg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;题目中，A为起始柱子，B为中间柱子，C为终到柱子&lt;/p&gt;
&lt;h2&gt;化繁为简&lt;/h2&gt;
&lt;h3&gt;1阶 move(1,A,B,C)&lt;/h3&gt;
&lt;p&gt;如果只有一个片片，会怎么样？&lt;/p&gt;
&lt;p&gt;答案很简单，A--&amp;gt;C，不需要以B为中转&lt;/p&gt;
&lt;h3&gt;2阶 move(2,A,B,C)&lt;/h3&gt;
&lt;p&gt;现在A有2个片了，因为大的必须要在下面，所以首要任务是把A底下的移到C去，但是小的那一个必须先动，才能移动大的。移动小的，就成了一阶的问题了，只不过，C要被大的占用，所以小片片的目标柱子成了B&lt;/p&gt;
&lt;p&gt;所以步骤为：&lt;/p&gt;
&lt;p&gt;1.A--&amp;gt;B（以A为起始，C为中间，B为目标）&lt;/p&gt;
&lt;p&gt;move(1,A,C,B)&lt;/p&gt;
&lt;p&gt;&lt;img alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;2.A--&amp;gt;C（与1阶相同）&lt;/p&gt;
&lt;p&gt;move(1,A,B,C)&lt;/p&gt;
&lt;p&gt;&lt;img alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;3.B--&amp;gt;C&lt;/p&gt;
&lt;p&gt;move(1,B,A,C)&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./2eee091b.jpg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;到此时就已经完成了&lt;/p&gt;
&lt;h3&gt;3阶&lt;/h3&gt;
&lt;p&gt;好了，现在A上有三个片片了，现在把它分开，我要把上面2个移到B上，把最大的移到C上。&lt;/p&gt;
&lt;p&gt;应该调用什么函数？&lt;/p&gt;
&lt;p&gt;1.move(2,A,C,B)&lt;/p&gt;
&lt;p&gt;依照这个，让上面两个到了B，接下来，要让A剩下的最大的那个到C&lt;/p&gt;
&lt;p&gt;2.move(1,A,B,C)&lt;/p&gt;
&lt;p&gt;好了，最大的已经到C了，这个时候我们只需要把B上的两个到C上&lt;/p&gt;
&lt;p&gt;3.move(2,B,A,C)&lt;/p&gt;
&lt;p&gt;好了，结束&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;发现什么奇怪的东西没有？？&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;3阶与2阶，调用的函数，除了第一步和第二步加了个1之外，都是一样的！！！&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;因为这时&lt;s&gt;按照传统功夫&lt;/s&gt; 我们已经将问题化成了一个基本的递归思想！&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;将除了最大的那个其他的搬到B上&lt;/li&gt;
&lt;li&gt;将最大的搬到C上&lt;/li&gt;
&lt;li&gt;将B上的搬动到C上&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;而1与3的复杂的操作可以交给递归解决&lt;/p&gt;
&lt;p&gt;那么函数代码可以出来了&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void mover(int n, char a, char b, char c)
{
	if (n == 1)
	{
		cout &amp;lt;&amp;lt; a &amp;lt;&amp;lt; &quot;--&amp;gt;&quot; &amp;lt;&amp;lt; c &amp;lt;&amp;lt; endl;
	}
	else
	{
		mover(n - 1, a, c, b);
		cout &amp;lt;&amp;lt; a &amp;lt;&amp;lt; &quot;--&amp;gt;&quot; &amp;lt;&amp;lt; c &amp;lt;&amp;lt; endl; //这句话也可以改为“mover (1,a,b,c);”效果一样
		mover(n - 1, b, a, c);
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可能走得有点快了，第一次我也有点疑惑这三步为什么就可以成了？毕竟递归真的有点难以理解&lt;/p&gt;
&lt;h2&gt;递归的详解&lt;/h2&gt;
&lt;p&gt;把三阶展开来（这里之间贴知乎大佬的答案）&lt;/p&gt;
&lt;p&gt;链接：https://www.zhihu.com/question/24385418/answer/252603808&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;step1. 把除了最大的盘子之外的盘子从A移到B（注意对于这个步骤来说此时A为开始柱，C为中转柱，B为目标柱，这样才能完成把最上面的2个盘子从A---&amp;gt;B的任务）&lt;/p&gt;
&lt;p&gt;A---&amp;gt;C  (开始柱---&amp;gt;中转柱)   【相当于调用  move（1，A，B，C）】&lt;/p&gt;
&lt;p&gt;A---&amp;gt;B  (开始柱---&amp;gt;目标柱)   【相当于调用  move（1，A，C，B）】&lt;/p&gt;
&lt;p&gt;C---&amp;gt;B  (中转柱---&amp;gt;目标柱)   【相当于调用  move（1，C，A，B）】&lt;/p&gt;
&lt;p&gt;step2.  把最大的盘子从A移到C（对于这个步骤来说此时A为开始柱，B为中转柱，C为目标柱，这样才能把最大的盘子从A---&amp;gt;C）&lt;/p&gt;
&lt;p&gt;A---&amp;gt;C  (开始柱---&amp;gt;目标柱)   【相当于调用  move（1，A，B，C），即直接执行 print（&apos;A---&amp;gt;C&apos;）】&lt;/p&gt;
&lt;p&gt;step3.  把除了最大的盘子之外的盘子从B移到C（注意对于这个步骤来说此时B为开始柱，A为中转柱，C为目标柱，这样才能完成把处于step2中的中转柱的2个盘子从B---&amp;gt;C的任务）&lt;/p&gt;
&lt;p&gt;B ---&amp;gt; A  (开始柱---&amp;gt;中转柱)   【相当于调用  move（1，B，C，A）】&lt;/p&gt;
&lt;p&gt;B ---&amp;gt; C  (开始柱---&amp;gt;目标柱)   【相当于调用  move（1，B，A，C）】&lt;/p&gt;
&lt;p&gt;A ---&amp;gt; C  (中转柱---&amp;gt;目标柱)   【相当于调用  move（1，A，B，C）】&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;**注意到了吧，move(2,A,C,B)等价于调用  move（1，A，B，C）； move（1，A，C，B）； move（1，C，A，B）【注意输入的字母顺序不一样，要做相应替换！！！！】&lt;/p&gt;
&lt;p&gt;所以函数就可以继续递归调用下去，然后完成计算。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;链接：https://www.zhihu.com/question/24385418/answer/252603808&lt;/p&gt;
&lt;p&gt;这三个步骤就是move（2，A，B，C）所做的事情，是可以详细列出每步移动的动作的。&lt;/p&gt;
&lt;p&gt;既然最上面的2个盘子都调用move（2，A，B，C）移开了，那么第3个盘子自然也可以从A---&amp;gt;B了，之后再把放在C上面的2个盘子从C移动到B上就完成了移动上面3个盘子的任务了。前面的move（2，A，B，C）函数既然可以将2个盘子从A借助B移动到C并列出详细的移动动作，那么move（2，C，A，B）也就能将放在C上的2个盘子从C借助A移动到B并列出详细的移动动作，如此说来，移动3个盘子的步骤就可以总结如下了：&lt;/p&gt;
&lt;p&gt;move（2，A，B，C）&lt;/p&gt;
&lt;p&gt;A---&amp;gt;B&lt;/p&gt;
&lt;p&gt;move（2，C，A，B）&lt;/p&gt;
&lt;p&gt;这三个步骤就是move（3，A，C，B）所做的事情，因为我们已经证明move（2，A，B，C）和move（2，C，A，B）是可以详细列出移动2个盘子时每步移动的动作的，而中间的A---&amp;gt;B是一步显而易见的移动动作，所以可以确定move（3，A，C，B）是能列出每步的移动动作的。&lt;/p&gt;
&lt;p&gt;然后根据同样的分析，最上面的3个盘子都移开了，接着只要将第4个盘子从A---&amp;gt;C，然后将放在B上的3个盘子移动到C上就完成全部任务了。前面我们已经证明move（3，A，C，B）能将3个盘子从A借助C移动到B并且列出详细的移动动作，那么move（3，B，A，C）也能将3个盘子从B借助A移动到C并列出每步的移动动作，这样，移动4个盘子的步骤就出来了：&lt;/p&gt;
&lt;p&gt;move（3，A，C，B）&lt;/p&gt;
&lt;p&gt;A---&amp;gt;C&lt;/p&gt;
&lt;p&gt;move（3，B，A，C）&lt;/p&gt;
&lt;p&gt;这三个步骤就是最开始move（4，A，B，C）函数所做的事情，因为我们已经证明move（3，A，C，B）和move（3，B，A，C）是可以详细列出移动3个盘子时每步移动的动作的，而中间的A---&amp;gt;C是一步显而易见的移动动作，所以可以确定move（4，A，B，C）是能列出每步的移动动作的。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;需要说明的是，从xx借助xx移动到xx这样的说法在上面出现了很多次，“从”后面代表的就是开始柱，“借助”后面代表的是中转柱，“移动到”后面代表的是目标柱。你也能注意到到 开始柱，中转柱，目标柱 在不同层级下是不一样的。&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;代码完成&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;iostream&amp;gt;
using namespace std;
void mover(int n, char a, char b, char c);

int main()
{
	int m;
	cout &amp;lt;&amp;lt; &quot;Input the number of disks:&quot; &amp;lt;&amp;lt; endl;
	cin &amp;gt;&amp;gt; m;
	mover(m, &apos;A&apos;, &apos;B&apos;, &apos;C&apos;);
}

void mover(int n, char a, char b, char c)
{
	if (n == 1)
	{
		cout &amp;lt;&amp;lt; a &amp;lt;&amp;lt; &quot;--&amp;gt;&quot; &amp;lt;&amp;lt; c &amp;lt;&amp;lt; endl;
	}
	else
	{
		mover(n - 1, a, c, b);
		cout &amp;lt;&amp;lt; a &amp;lt;&amp;lt; &quot;--&amp;gt;&quot; &amp;lt;&amp;lt; c &amp;lt;&amp;lt; endl;
		mover(n - 1, b, a, c);
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出结果（以4阶为例）&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./2e65bd14.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;参考链接：&lt;/p&gt;
&lt;p&gt;https://www.zhihu.com/question/24385418/answer/252603808&lt;/p&gt;
&lt;p&gt;https://blog.csdn.net/qq_37873310/article/details/80461767&lt;/p&gt;
&lt;p&gt;关于递归算法，这中解决问题的方法也只有计算机才喜欢，我们虽然看着代码很简单，但真要深入理解也是很费脑细胞的，不过递归确实有中数学上的简洁美和逻辑美。&lt;/p&gt;
&lt;p&gt;以上，Fin.&lt;/p&gt;
</content:encoded></item></channel></rss>