当我们谈论 AI 对程序员的冲击时，问题往往被简化成：程序员会不会被取代。但真正被取代的，也许并不是“程序员”这个职业，而是十几年来我们所熟悉的岗位划分与组织形式。或许 AI 会将整个计算机软件开发行业，带回到应属于它的样子，“全能开发”与“领域专家”将会这个行业仅需的两种人才。而那些被岗位划分保护着的程序员们将会被淘汰。过去的软件开发行业是什么样的？为什么同样是编程工作却会被细化为如此之多的岗位？未来的软件开发行业又会变成什么样？希望通过这篇文章与读者们一同分享，作为从业者的我，在 AI 冲击下的一些思考。过去: A programmer上个世纪 60 年代，大型计算机在西方的企业、大学和政府的普及，使得程序员这一职业进入了主流视野。这个时期程序员们的工作与现在有着极大的不同：程序员们根据业务部门的需求，对照着如同字典一般厚度的手册，在纸带上打孔，或者是在电传打字机上打字，将程序和数据一同送入计算机中。计算机会按照程序处理数据，然后将结果打印出来，再由程序员们“翻译”后转交给业务部门。随着集成电路技术的发展，70年代末，以 Apple II 而为代表的个人电脑走入普罗大众的家中，编程这一活动不再仅限于企业雇员，而是一项所有人都可以参与的活动。这一时期有一群热爱编程与计算机的人，例如 Guido van Rossum1（吉多·范罗苏姆), Linus Torvalds2（林纳斯·托瓦兹）, Bill Gates（比尔·盖茨）, Steve Jobs（史蒂夫·乔布斯）等等，他们塑造了我们如今所熟悉的计算机与编程开发工作，也塑造了计算机软件开发领域的文化——开放与共享。而到了90年代到新世纪的第一个10年，无论是为企业信息化提供软件方案，亦或是给 Windows 图形化桌面系统开发个人软件，也或许是开发一个互联网的网站服务。这一时期程序员的需求达到了一个极高点，而程序员的高薪资待遇也吸引了许多人前赴后继投入这个行业之中。过去那种因为热爱才去做的时代，不复存在。在这一时期，程序员之间的工作差异并没有那么明显，一个程序员通常会承担许许多多的工作，包括但不限于程序的文档资料、程序的界面开发、核心业务逻辑、服务器的运维、客服工作等等，甚至可能包含给老板修打印机。在这一时代，由于技术选项并不算多并且业务相对简单，因此以当下的眼光来看，人人都是全栈人才。除了一些特殊行业（例如：游戏开发），程序员之间的分化并没有那么严重。现在: 无形壁垒2008 年 iPhone 3G 发布，其首次搭载了 3G 网络，并允许用户通过 App Store 安装第三方软件。这一款手机的发布，标志着信息时代进入了第二个时期：移动互联网时代。新的互联网入口，极大的扩展了互联网服务的呈现方式，加之移动客户端、桌面客户端、网站的开发流程完全不同，因此岗位出现了分化——前端（客户端）开发、后端（服务端）开发。2008 年，我国的手机上网用户仅为 7305 万人3。到了 2013 年，已经达到了 4.64 亿人，占网民群体的 78.5%.互联网用户群体的暴增，带来了许多新的挑战：一个小小的程序错误，可能会导致成千上万人受到影响。为了提高系统的可靠性，在技术发展越发复杂，在管理上为了权责的划分，也将后端开发的岗位进行了进一步的细分：业务程序员、架构师、数据库管理员、运维人员、产品经理、专职测试 QA 人员。至此，我们所熟悉的行业组织形式已然出现：岗位不再承担具体的角色，而是为了达成某一目的的零件，个人所持有的技能被岗位的权责所限制。并且这些岗位并非技术上不可合并，而是管理上的“风险管控”。团队中的绝大多数人，可能根本不了解系统是如何运作的：前端开发人员不知道用户输入的数据在系统内是如何被处理和存储的，后端开发人员也不知道用户的交互是为何而设计且如何实现的。每个人只要负责足够小的一个模块，每个人都可以非常轻易地被另一个人所替代。塑造这一切的，既有客观原因：用户量的增加、软件组织复杂度的提高、安全与合规性的挑战等等。但不可忽视的，一些人为因素进一步加剧了整个行业的变化，从人才培养到职业晋升都变得如此畸形：公司经营的风险管控、HR 的评价体系简化、短期绩效考核带来的奖惩机制等。这样的工作组织形式，与工厂流水线无异，每个人只需要专心并负责自己手上的工作即可。无论是工业时代，还是信息时代都足以证明，这样组织形式，在生产力没有得到突破之前，一定是最优解。其成就便是这十几年来高度信息化的社会，与极为丰富的信息服务体验。未来: 全能技术与领域专家2022 年末，ChatGPT 问世，无比自然的交流与回答，颠覆了人们对于人工智能在过去二三十年的传统认知。但在信息技术领域，程序员们依然对其嗤之以鼻，认为其只是一个可笑的玩具。三年之后的 2025 年，氛围编程 (Vibe Coding) 这一编程范式被提出，并且被一些人所实践。其核心方法非常简单：将你的需求告诉大语言模型 AI ，全盘接受 AI 给你的代码，百分之百信任其结果，并不做进一步测试与检查。完全依靠氛围编程打造一个足以支撑千万用户级别的项目并不现实（因为编程的难点并非是写代码），但使用 AI 完成一些枯燥的工作内容还是绰绰有余。这些工作内容的特点即没有任何复杂度，却需要输入大量的代码才能完成，例如：CRUD4、表单、测试用例等等。解放程序员工作中的体力与心智负担，仅仅只是 AI 的表象。AI 对于程序员而言，真正本质更像是一个放大器：它不会凭空创造认知，却会成倍放大程序员已有的知识、经验与判断力。一个对系统缺乏理解的人，即便借助 AI 写出大量代码，也难以判断其正确性与边界；而真正理解业务与技术的人，则可以借助 AI，将原本需要数周的工作压缩到极短时间内完成。这也意味着，AI 不会抹平程序员之间的差距，反而会迅速放大这种差距。过去依靠岗位分割来掩盖能力差异的组织方式，将难以为继。AI 迫使每一个从业者直面一个问题：你真正为系统提供的价值是什么？在这样的背景下，未来的程序员岗位，很可能会收敛为两类角色。第一类，全能技术开发者（并不是全栈！更不是全干！）他们并不追求在单一技术点上的极致深度，但必须对系统整体有清晰的、完整的认知与理解：业务边界如何划分、技术方案如何取舍、风险在哪里、成本在哪里。他们的主要工作，不是亲自完成所有编码，而是持续地拆解问题、组织实现，并引导 AI 完成大量执行层面的工作。全能技术开发者并不是“什么都做的人”，而是当问题尚未被正确定义时，依然需要站出来，对系统结果承担责任的人。第二类，领域专家他们会向更深的专业领域持续下沉。他们专注于某一类关键技术或复杂业务，对其约束、极限与演化路径有着长期积累的理解。在 AI 的辅助下，他们可以更高效地进行方案推演与设计验证，但最终的判断依然依赖于经验与认知本身。越是关键、越是不可逆的决策，越难被完全自动化。他们的价值，也并不体现在给出更多答案，而体现在更清楚地知道：哪些方案在现实世界中是不可接受的。当这两类角色逐渐成为主流，那些高度依赖流程，只负责系统中极小一部分，却缺乏整体认知的岗位，将不可避免地失去存在的意义。这并非 AI 的残酷，而是生产力跃迁后的自然选择。当 AI 可以替人完成越来越多的执行性工作时，程序员之间真正的分野，将不再体现在“会不会写代码”，而体现在是否有人愿意，且也有能力，为问题的定义、边界与结果承担责任。AI 并没有终结程序员这个职业，而是迫使它回归本质——程序员不再是“写代码的人”，而是“理解问题并构建解决方案的人”。结语在七、八年前，我曾在网上向他人请教过一些关于职业规划的问题，虽然没有给出明确的答复，但那位大佬给我的一句话，至今让我受益匪浅。在 AI 冲击行业的当下，这句话更是进一步验证了它的价值。“我们与其他人的不同，并不是因为会写代码，而是更懂计算机。” Guido van Rossum: 软件编程语言 Python 的创造者。Python 在如今人工智能的领域是一门不可或缺的语言，从研发到上线或许都会用到它。 ↩ Linus Torvalds: 电脑操作系统 Linux 系统的创造者。Linux 系统是一种主要用于服务器与数据中心的电脑操作系统，世界上绝大多数的服务器都使用 Linux 系统。 ↩数据来自于中国互联网络信息中心 CNNIC 的《中国互联网络发展状况统计报告（2008年7月）》 ↩CRUD: Create, Read, Update, Delete 的缩写，表示数据的增删改查。 ↩
如果不是因为在社交媒体上看见了一位网友分享了自己的年度文章，我大概会在下周才开始思考如何撰写，然后赶在今年结束之前草草写一篇“年度报告”，回顾一下所见所闻所想，然后设定几个在新的一年里可能完成率连一半都达不到的目标，就像前两年那样。不过这并非什么坏事，至少看见他人的文章之后想要立即动身去写，说明我还没有厌倦这个事情。对我来说，每年一篇的年度总结文章有着一些特殊的意义，在五年前，我曾希望通过工具或者习惯，达成对于“时间”与“金钱”的管理。五年后的今天，记账已经成为了我生活中的一部分，哪怕间隔长时间都没有记账，在年底之前用程序导出账本，将所有交易一笔笔过目，反映我真实的财务状况。但对于“时间”的管理，我却始终未能找到足够好的工具，也未能养成良好的习惯。因此，每年的这篇年度总结，算是我所能做的，对于过去时间的总结与未来时间的规划，最简单和最容易坚持的办法了。今年决定对年度总结内容做一些改变，一是过去两三年的行文风格以及内容，开始变得有些公式化了，变得抽象了，缺少了指导性的作用。另一个原因是今年开始，我在微信公众号上也运营了自己的博客，本文也会发在自己的微信个人号上，不过年终总结多是长文，完读率乃至打开率应该都不会太高。自我与生活今年的生活与去年相比没有太多变化，工作依然是那一份工作，身边的长辈、朋友也都安好，这一切平稳既让我感到安心，也让我在当下有一些焦虑。生活没有太大变化，也就意味着客观世界在一定层面上是静止的，这对于自我认知、对于世界的认知都会产生一定的固化，使自己变成一个“思想保守”的人。事实上当我在编写这一段内容时，回忆今年自己是否有过哪些颠覆性的思考和认知时，似乎一片空白。得益于每周都会听一听播客节目，听一些与人生、哲理、认知、情感、旅行有关的话题，听节目中主持人与嘉宾们的讨论。周末与朋友聊天，聊一些生活与工作上的烦恼。至少还没有让脑子完全锈掉，对于一些不同的观点和意见，还是会进行分析与消化，并进一步巩固自己的认知理论。对于自己性格上的一些缺陷——情绪控制问题，今年感知得非常明显，并自己有意地在进行一些控制，避免与人发生冲突以及对自身的一些伤害。但是从今年模糊的统计结果来说，只能给自己的纠正行为打一个 3/5 分，因为在工作和生活上还是与人发生了较为激烈的言语上的冲突，尽管之后还是迅速认识到了自己的错误而变得极度沮丧，但犯错本身就不应该。也有好的方面，今年第二季度的时候，有一次因为工作上的事情，不好的情绪处于溃堤边缘了，但我也通过转移注意力，与变化环境的方式让自己强制冷静了下来。至少还是有一些改变。工作公司今年搬了新办公室，通勤时间缩短了十分钟，同时上午的工作时间后移了半个小时，这使得我早起之后的时间非常充裕。本来打算用这段时间来进行锻炼，结果没想到反而变成每天可以多睡半小时。工作上今年最大的成就，毫无疑问是将去年使用 Go 语言编写的设备控制程序，使用 Java 完全推翻重写。此前使用 Go 语言编写的程序，为了实现某些可能根本不会存在的需求——类似于在设备上用电机唱歌——使用了一套略微抽象的系统架构，并且极其难以维护：代码量又多，还存在 JavaScript 执行器；对于设备的控制是通过发送一个 JavaScript 脚本过去，然后由设备上的程序进行执行。这样的设计天然存在一些问题，即设备无法自主执行某些操作，例如读取设备温湿度然后上报给服务器。虽然真要做也不是不行，但是这样对于任务的管理又会变得极其复杂。之后在与新设备厂商沟通时，了解到了 MQTT 这个东西，发现其天生就是为了物联网服务的，我甚至还写了篇文章介绍了一下。在征得领导的同意之后，逐开始着手重写，耗时20天时间完成了整个系统的开发，并且至今为止我负责的代码只出现了很少的问题。那段时间反倒是柜子硬件接二连三的出现卡货问题，让我无比头疼和焦虑，甚至导致我目前听到微信通知提示音，心理上都多少有一些应激反应。旅行今年算是有三次旅行，一次自己独自一人的一日往返的山野徒步，这种走在山林之间的感觉非常舒服。另有两次则与往年不同，是与他人结伴的，尽管我非常享受独自旅行时，自由与孤独所带来的快乐，但结伴同行时与亲朋好友即时分享所见所想，我也并不讨厌。春天即将结束的时候，在小红书上找到了一条徒步路线，阴差阳错地在徒步软件上却找到另一条错误的环线路线，但也因祸得福，开启了一场独自一人的日归徒步之旅。全程16公里，累计爬升740米，耗时5个小时，途中因为准备不充分致使补给不足，导致后半程又累又渴。如果不是因为钢铁般的意志，以及前不着村后不着店的情况（但是手机依然有信号，所以其实很安全），感觉可能都会选择放弃了。总归还是非常有意思的，下次一定要带足补给再去征服一次，也想试试在下满雪的山上吃热腾腾的豪华泡面是什么感觉。夏天，国外的朋友回国游玩，和他一起去了三星堆博物馆，但是因为是旺季加之暑假，游客人数多到难以接受……展品前面总是围了好几层的人，以及被小朋友的手摸得脏脏油油的玻璃。之后由于工作原因，耽误了半天时间，放弃了去九寨沟游玩，转而去了峨眉山游玩，结果因为攻略没有做足，导致兜兜转转差点错过上山，并且也非常遗憾没能带朋友在山上看到日出以及云海。一位难得回国的朋友，却没有给到最好的体验，多少还是有一些愧疚。当然这也确实说明了我不太会做旅行攻略，毕竟大多数时候独自旅游，本质上更像是一种探险，去享受陌生的过程。但做旅行攻略本身又是一门技能，所以我也并不排斥。秋天，去了一次网红景点：理小路，这条线路被称作川西的独库公路，虽然这个名头有些浮夸，但是在游历一遍之后我觉得并不为过。从高山森林一路穿越，到远观雪山，感觉与之齐高的景色，再一路下至山脚感受人类的渺小。一路的彩林与溪流，蜿蜒的盘山公路，这一切都是那么美好，但也是那么短暂。理小路的核心景点非常的短，如果一路不停的行驶，大概两三小时就能跑完，但正是线路短且景色集中，或许正因如此它才如此出名吧。正是因为短小，一些问题也足够显著。最明显的应该是卫生与垃圾问题，整条线路上，提供卫生服务的地方只有两处，且一处采用的自然降解的方式，环境极差无比。另一处要稍微好一些，印象里是有自来水冲洗的，尽管水流不大。而垃圾问题则是彻底灾难，一处景点允许游客下到溪流边游玩，没走多远就能看见漂浮在水中的塑料垃圾。虽然我并不喜欢封闭式管理的景区，但这些卫生与环境问题，也少有人愿意做义务劳动吧。这次旅行的线路则是我规划的，还算有所成绩，选择了一条与主流的线路相反的方向，从而获得了不错的体验。在回程时，看见堵了上千辆车的长龙，不禁庆幸所做决定真是正确无比。REVIEW O1KR1: 体重降低至 BMI 健康标准KR2: 体脂率降低至22%以下（小米体脂秤标准）KR3: 夏天不再一动就出汗KR4: 每周至少运动3次一个都没有做到 🌚再接再厉吧！知识与分享可能是 AI 发展得太好用了，也有可能是年龄大了对于新鲜知识的欲望没有那么强烈了。对于系统性的知识的学习反而变得更少了，通过 AI 去学习一些零星的知识点反而成为了我的常态。一有不懂就去问 AI ，它总是能够给出我能看懂的回答。因此对于成体系的知识反而没有那么渴求了，并且如果多追问几次，也不知道它给出来的回答到底是它瞎掰的还是正确的。这种做法其实还挺糟糕的，容易形成知其然不知其所以然的情况，并且一年下来，其实根本没有记住多少知识，如果不是回头去看了一下和 AI 的聊天记录，甚至我都不记得自己还询问过有关于放弃著作权的知识。总而言之，我其实觉得这反而可能是进入 AI 时代之后的一种疾病，因为获取知识的途径更加轻松，且单一知识的认知难度也有所降低，因此人们对困难知识的学习会逐渐变得排斥，最终会退化成不愿意学习。当然，以上那些“借口”或者理由并不是最主要的原因，仔细想来其实在工作之后，或者说长期以来就缺少了一个钻研的方向，这大概是我一直缺乏持续学习系统性知识的主要原因。之前一直都想着专精某一个工作需求比较高的技术，然后找一份不错的工作。结果到头来发现一切并没有那么简单，一些容易找到工作的技术，天花板也并不算高，并且如今经济下行其薪资也没那么高。而那些不容易找到工作的技术，反而更有难度，综合能力要求也更高，薪资也相对稳定。可能还是得靠兴趣驱动，找一个感兴趣的领域的技术钻研一番。至于工作，就目前看来做常规的信息产品，特别是无实体附加的信息产品，被 AI 整体取代只不过是一个时间问题了。摄影忘记是在哪儿看到的了，大致是说摄影有三个阶段：记录、发现、创作。“记录”即日常的拍摄，早期我们使用胶片相机和DV记录生活，如今我们使用手机来进行拍摄，无论是当日就餐的美食，还是分享到社交媒体的自拍照，甚至是家庭合照，这些都是单纯的记录。“发现”则是指通过特定的视角，用器材去记录光影和时间，例如风景照，事物的特写等等。“创作”则更多的是在光影和时间之外，附加上构图和故事性，创作是通过摄影自我表达的一种手段。我自认为目前还停留在“发现”这个阶段，并且对于发现美的这一点能力来说，也还有待加强。一来是对于城市摄影作品的阅览太少了一些，在街拍时，仍比较害怕将镜头对准他人。再者即对于作品的后期，还是没有形成一个自己的风格，对于不同内容所想要表达的具体感觉，还是很难去把控。以及自己至今没有一个使用得比较顺手的图片后期软件，像是 RawTherapee, ART, DarkTable 都有在使用，但三者的功能和操作逻辑差异都比较大，于是在每一次修图时，都会尝试三个程序，非常浪费时间也有一些打击热情。今年我最喜欢的一张照片，拍得其实非常糟糕，构图并不算好看，甚至右上角还漏了一点蓝天。但整个画面的颜色让我感到十分舒适与协调。写作虽然没有达成去年设定的目标，还是写了不少文章。也许是技术并没有太多进步，也许是对于过于具体的技术有些厌烦了，今年的文章主要内容都在讨论一些“比较大”、“不那么具体”的话题——心智负担、AI、MQTT等等。尽管当我写下今年最后一篇文章《使用 atlas 进行数据库迁移管理的实践》时，我觉得今年写的内容都没有太多意义。但在当下来看，自己今年一系列的文章似乎可以串出一条线：AI 时代的程序员技术指南。也许新年可以以此为目标创作一系列文章。今年的所见所闻图书《重来》〔美〕贾森·弗里德〔丹〕戴维·海涅迈尔·汉森一本讲述小型企业的另类经营之道的书籍，有一些不太符合中国国情，但许多内容还是有一些道理。作者之一是明星程序员和RoR框架的创造人 DHH，他们的公司虽然小，但是创始人都实现了财富自由了，并且似乎也没有上市融资。《事实》汉斯·罗斯林、欧拉·罗斯林、安娜·罗斯林·罗朗德网上冲浪时无意中发现了某一年比尔·盖茨推荐书单，其中就有这本书。这本书用数据的角度阐述了很多与认知相悖的事实，并且创新性的采用了四分法划分财富水平，也是如今联合国对于经济发展水平的描述方法。我觉得是一本非常有意思的经济学和社会学读物。《注意力危机：你为什么会失去专注力？如何重回深度思考？》约翰·海利没读完。有一段时间认识到，当下社会不仅信息爆炸，且信息极度碎片化。我一直尽可能保证自己不被这些事情影响，但专注力比起从前也还是许多降低，于是找了这本书来看。《架构整洁之道》罗伯特·C·马丁没读完。有一段时间意识到“写”代码的能力没有太多作用了，于是想要找一本将程序架构的书来看。这本书内容很好，指导性很强，并且有可以用来评估项目代码健康度的方法。不过后面因为意识到一些其他事情，所以暂停了，并且这本书本身读起来一方面还是有一些晦涩。播客因为听过的播客节目太多了，这里只列出几个我今年印象比较深刻，比较喜欢的几期节目。《错误：如何认识失败以及如何塑造自我》虚实之间我今年最喜欢的一期播客节目，其中有一个观点我很喜欢。所谓错误实际上是“没有达到预期效果”，通过这种理解方式实现客体分离，一件错误为何未能达成预期才是关键，而非将错误归咎于某人某事。《从科比到尼采，从曼巴精神到超人哲学》虚实之间反复听了许多次，喜爱程度如同去年的“工作的意义”那一期节目。这期节目算是对于个人内心意志的讨论，其中一个观点我很喜欢：真正的偶像应该能够使你找到人生意义，而非赋予、给予你意义。《劳模雷军：假如生命明天就会终止，你今天还会创业吗？》保持偏见长达3个小时的播客节目，我大概花了一个星期才听完。这篇博客算是雷军的个人传记，从大学读书一直讲到小米创业以及后面造车。在讲述小米创业的那部分内容时，一开始的屡屡碰壁还挺感触的。《影视飓风TIM✖️罗永浩！用影像打开世界的梦想家》罗永浩的十字路口影视飓风的创始人——潘天鸿（Tim），算是与我同龄的人里面我比较崇拜与佩服的人。这个访谈节目在罗永浩犀利的提问之下，他也还能沉着应付。尽管他争议比较大，但不妨碍把他视作我的目标。“苹果简史系列” 半拿铁 | 商业沉浮录没有听完。总共五期节目，每一期都在一两个小时左右，本来是出差路上听的，后面没有出差了于是也没有听了。虽然我也看过《乔布斯传》，但时间太久远了剧情也记不清了。这个存在半个世纪的民用电脑公司的故事，一直都非常吸引我。电影《捕风追影》一部前半部分我能给出满分好评，但后半部分只能打2.5分（满分为5）的作品。前半部分的动作戏以及警匪之间的博弈都非常有意思，后半部分能看出来编剧引出了好几条线，但是在回收时做得并不算好，且对于反派的形象以及主角的成长还是有一些刻意了。《疯狂动物城2》等待近10年终于迎来的续作，虽然许多人对其评价褒贬不一，但成年人又何尝不能有自己的童话故事呢？至少我看得非常开心。动画/漫画这里仅列出几个我愿意分享的作品。《迷宫饭》今年看过的最喜欢的动画，剧情紧凑没有拖沓，对于世界观的刻画也非常深刻，人物形象的刻画也非常灵动，不同种族不同个体之间也有不同性格，作为观众/读者也不会对某个种族产生所谓的刻板映像。《天国大魔境》本来这个部分里面我还有写《葬送的芙莉莲》，但是仔细想想，作为公路片来说，芙莉莲其实相当糟糕，特别是在魔法考试篇的时候，整个故事又退回到了少年向的感觉。反之《天国大魔境》，主线剧情一直推动主角在路上前进，且在看完之后去看解析视频，两条相互关联相互影响的时间线描述，让人不得不佩服作者的想象力和创造力。REVIEW O2从结果上来看，完成度只有一半。但已然不错了，相比较去年已经有了很大进步，并且这也确实说明了定下目标是有完全达成的可能性的。KR1: 读四本书只读完两本KR2: 每月至少产出一篇文章加上年终也才只有6篇KR3: 每半年至少出一份多媒体内容产出一条视频和若干张图片技术与产品今年付费订阅了 Cursor ，一个 AI 代码编辑器。体验了大半年快一年时间，如果从使用率来说的话，其实我未能物尽其用。使用率低的主要原因是因为工作中的代码实在过于糟糕，所以真正能够放心让 AI 自己去干的事情实在太少，很多时候为了避免代码的修改不会影响已有的功能，只能自己亲力亲为去做。还有一个原因是我从很早就开始使用 AI 辅助编写代码，因由于一些过往经历，我对 AI 始终不那么信任，所以不太敢放心让他完全去做。尽管如此，明年我依然会付费尝试 AI 编码工具，只是不会考虑一次性按年付费了，因为这个原因导致我没机会（或者说预算）去尝试其他的 AI 编码辅助工具了。虽然目前使用率低，但我尝试过的几次体验都非常好，像是一个得力的助手为你完成编码工作，只要你的指令足够详细，告诉他具体应该如何去做，他总能给你很好的完成，沟通成本比真人还低。他也像是一个智能的搜索工具，面对公司那糟糕无比的屎山代码，他依然能够根据我的要求，找到我所需要的正确的代码。这让我思考了一些事情，一直以来我一直都将程序员看作是古代的手工艺人，事实上也确实如此。但是如今有了 AI 工具之后，如果依然保持“传统”采用手作代码，那么其实反而更像是流水线上的初级工人，因为初级工人无法“驯服”更为高级的工具，以至于被取代将成为常态。程序员只有在能够更好的使用 AI 工具之后，才能维持住自己手工艺人的身份，才不会被 AI 所取代。我在去年就思考过这个问题：信息行业里面，什么技术岗位不会被 AI 取代。我去年给出的答案是运维（或者 DevOps），这个岗位如同 AI 时代的会计一样，不会被取代的原因是始终需要人来背锅。但实际上，AI 的替代作用至少短期来看还没有那么强，尽管 AI 会写代码，并且写得比许多人都要好，但当下的 AI 始终还是相对而言比较愚笨的，他并不知道什么样的代码是优质什么样的是劣质的，仍然需要有足够多经验的人给他足够多的提示，他才能够写出让人满意的代码。对于当下的 AI 来说，他不会狡辩只会默默干活，其上限依旧取决于人的上限，但其却保证了人的下限。因此对于许多事情来说，只要 AI 能够完成的事情，做与不做，则完全取决于个人意愿了。对于当下这个时代的程序员掌握哪些技能才不会被 AI 取代，这个话题还是留着明年写一篇文章好了。产品的未来今年对于产品的开发有过很多尝试，但是没有一个做出来，放弃的原因有非常多：实现难度太高、商业转化太难、开发时间太长热情褪去……我一直在思考适合自己的产品或者说独立项目是什么。本来一开始这一部分内容写了很多，对于自己做过的项目进行了一些分析，对于网上一些关于“独立开发者”的话题进行讨论。但一想到这篇文章是属于我的年终总结，写这些内容多少有一些“超纲”，并且谈那么多却拿不出产品来证实，多少有些空谈了。Talk is cheap, show me the production.希望明年这个地方可以谈谈我对于自己的产品的一些看法和理解。REVIEW O3KR1: 开发一款独立应用KR2: 用 Ansible 管理自己的服务器KR3: 基于 Gitea 打造一套集成 CI/CD 的开发环境一个都没做到 🌚该调整 Key Result 了！财务 此内容仅文章作者可见REVIEW O4KR1: 工资以外收入1万元达成率约 1.3%KR2: 证券投资有所回报确实有所回报，但不多（上面那一个 KR 的 Review 就是全部了 😂）感觉自己在财务方面还有待提升，但是今年至少算是迈出了第一步，新的一年希望通过量化的方式对抗恐惧，然后通过多方面的手段增加更多的收入吧。展望去年开始，我就使用 OKR 方法替换掉了罗列一大堆不切实际目标的方法，通过下少数几个目标 （Objective）指明一个前进的方向，然后通过指定一些可以量化的关键结果（Key Result），评价自己的努力的结果。考虑到今年很多 KR 都没有达成，因此明年多数是继续延续，但对于一些不再具备指导性的 KR 进行调整。O1: 身体健康KR1: 体重降低至 BMI 健康标准KR2: 体脂率降低至22%以下（小米体脂秤标准）KR3: 夏天不再一动就出汗KR4: 每周至少运动3次O2: 知识与分享KR1: 读四本书KR2: 每月至少产出一篇文章KR3: 做一本自己的电子摄影集O3: 技术与产品KR1: 完成一个自己的产品KR2: 推广自己的产品使其拥有超过100个用户KR3: 基于 GitHub Action 打造一套集成 CI/CD 开发环境KR4: 打造一个 AI 解放手工写代码的开发环境O4: 财务增长KR1: 工资以外收入1万元KR2: 通过量化交易方法收入500元
工作经历虽然不多，且都是开发基建不那么完善的小公司，因此在工作中经常会有各种不爽，其中一家连源代码管理都没有，那更是痛苦至极。现在这家公司在基建方面虽然不太完善，开发管理也不是那么完美，但总归来说还是比较不错的，至少有使用 Gitea 这种比较成熟的代码托管平台。我负责的项目在去年一直都没有正式上线，但也是给其中一个意向客户部署了测试版，在这期间项目也在快速迭代，在没有 CICD 的场合下，每一次在开发测试环境（是的，我们开发和测试是同一个环境）完成某一个需求后，在向客户推送更新时，迁移数据库表结构是一件非常痛苦的事情。在这件事情上，我的同事们使用了一些不那么合规的方法，通过图形化的数据库管理软件进行结构迁移。这对我来说有一些难以接受，另一方面我确实不习惯使用图形软件管理表结构。尽管我在公司被称为项目经理，但公司同事和团队成员普遍习惯使用图形化工具。因此引入语言绑定的迁移工具并强制统一使用并不现实，只能寻找外部独立工具作为折中方案。对于这个工具，我有一下几点要求：需要可以导出现有数据库的 DDL可以比对两个数据库生成 Migration 的 SQL可以像常规的迁移工具一样，通过 Migration 管理数据库版本最好是命令行工具，且尽可能不需要有额外的运行时要求实际上，这个需求的 1、3、4 都并不算复杂，市面上也有许多，例如 dbmate 和 goose 都能做到。但 2 这个需求就很难符合的产品，因为“根据对比生成 Migration”和“通过 Migration 管理数据库版本”本身是冲突的，绝大多数产品并不会同时支持两个方法管理。在我苦苦寻找之后，我找到了同时满足以上四点的产品 —— atlas迁移工具的不同形式市面上的迁移工具有两种形式，分别是：“描述、执行”和“声明、对比、应用”。类型代表软件描述、应用- phinx (主要用于 PHP 项目)- goose (通用，可与 golang 项目集成)- flyway (通用，可与 Java 项目集成)声明、对比、应用- alembic (与 ORM SQLAlchemy 集成，用于 Python 项目)- EntityFramework Core (本身就是 ORM，用于 .NET 项目)- atlas (通用，面向 CICD 设计)简单来说，“描述、执行”方式的迁移工具，符合 DBA （数据库管理员）的直觉，它的流程通常是：新建一个迁移文件，描述本次迁移执行的指令，然后迁移工具执行迁移文件，维护迁移的执行历史。但这对于开发人员来说比较繁琐，因为我们在设计或者调整表结构时，更多的是去关注整个结构是否满足需求，而不会去在意甚至是记录增删改了哪些字段。而“声明、对比、应用”更符合开发人员的直觉，它的流程是这样的：声明是指定义出期望的各个表（在 ORM 为 Entity/实体 或 Model/模型 ）的结构是什么样的；对比是指计算出当前数据库中的各个表（Entity）与期望的表（Entity）的差异；应用是指迁移工具执行差异并维护迁移执行历史而之所以这种方法能够与 ORM 高度集成，也是因为当开发人员定义 Entity（或模型）时，实际上就已经完成了绝大多数的表结构定义的工作了。这个时候再手动去维护表结构，就显得有一些多余了。但也正如前言所述，这类方法很多时候也都是与 ORM 高度集成的，对于一些编程语言而言，特别是一些弱类型动态类型语言而言，社区里面没有提供这样的 ORM，也就很难享受到这种面向开发者的迁移流程。不过好在，现在我们有了 atlas 。工作流atlas 提供了两种工作流，分别是 Declarative workflow 和 Versioned workflow 。他们分别对应了：数据库优先和代码优先，这两种工作流程。其中所谓数据库优先则是指对数据库进行相应操作，然后使用 atlas 将数据库直接迁移直接应用到目标数据库中， atlas 将比较两个数据库之间的差异，并生成、执行对应的 SQL/DDL 语句。而代码优先，则是通过定义数据库结构，通过生成迁移文件来实现可以管理和追踪的数据库版本。在本篇，我们将使用 PostgresSQL 进行演示，尽管 atlas 提供了 HCL 这样一个 DSL （领域特定语言）来完成相关工作，但仅为了数据库管理就引入一个 DSL 收益或许并不算太高。说明式的工作流/数据库优先 (Declarative workflow)迁移假设我们获得了一个名为 "original-database" 的数据库，它的 DDL 是这样的：CREATE TABLE book ( id SERIAL, title VARCHAR(1000) NOT NULL, author VARCHAR(1000) NOT NULL, isbn VARCHAR(20) NOT NULL, PRIMARY KEY ("id"), UNIQUE ("isbn") );现在需要将其数据库结构迁移到 "target-database" 数据库中，我们可以直接执行下面这个命令：> atlas schema apply --url "postgres://postgres:PASSWORD@localhost:5432/target-database?sslmode=disable" --to "postgres://postgres:PASSWORD@localhost:5432/original-database?sslmode=disable" Planning migration statements (1 in total): -- create "book" table: -> CREATE TABLE "public"."book" ( "id" serial NOT NULL, "title" character varying(1000) NOT NULL, "author" character varying(1000) NOT NULL, "isbn" character varying(20) NOT NULL, PRIMARY KEY ("id"), CONSTRAINT "book_isbn_key" UNIQUE ("isbn") ); ------------------------------------------- Applying approved migration (1 statement in total): -- create "book" table -> CREATE TABLE "public"."book" ( "id" serial NOT NULL, "title" character varying(1000) NOT NULL, "author" character varying(1000) NOT NULL, "isbn" character varying(20) NOT NULL, PRIMARY KEY ("id"), CONSTRAINT "book_isbn_key" UNIQUE ("isbn") ); -- ok (3.7036ms) ------------------------- -- 3.7036ms -- 1 migration -- 1 sql statement我们可以看见，atlas 向我们展示了计划执行迁移的 SQL 语句，以及执行的语句数量，以及执行的时间。这个时候我们查看数据库，会发现迁移已经成功了：更新在未来，"original-database" 发生了变化：(1) "book" 表中新增了 "isbn2" 字段用于表示其第二个 ISBN；(2) "book" 表中的 "author" 字段变更为 "author_id" 字段；(3) 新增了 "author" 数据表，用来存储作者信息。其 DDL 变更为如下形式：CREATE TABLE IF NOT EXISTS "author" ( "id" SERIAL, "name" VARCHAR(1000) NOT NULL, PRIMARY KEY ("id") ); CREATE TABLE IF NOT EXISTS "book" ( "id" SERIAL, "title" VARCHAR(1000) NOT NULL, "author_id" INTEGER NOT NULL, "isbn" VARCHAR(20) NOT NULL, "isbn2" VARCHAR(20) NOT NULL, PRIMARY KEY ("id"), UNIQUE ("isbn"), FOREIGN KEY ("author_id") REFERENCES "author" ("id") );现在当我们执行一下刚才的指令，会发现 atlas 已经将变更为我们处理好了：> atlas schema apply --url "postgres://postgres:PASSWORD@localhost:5432/target-database?sslmode=disable" --to "postgres://postgres:PASSWORD@localhost:5432/original-database?sslmode=disable" Planning migration statements (2 in total): -- create "author" table: -> CREATE TABLE "public"."author" ( "id" serial NOT NULL, "name" character varying(1000) NOT NULL, PRIMARY KEY ("id") ); -- modify "book" table: -> ALTER TABLE "public"."book" DROP COLUMN "author", ADD COLUMN "author_id" integer NOT NULL, ADD COLUMN "isbn2" character varying(20) NOT NULL, ADD CONSTRAINT "book_author_id_fkey" FOREIGN KEY ("author_id") REFERENCES "public"."author" ("id") ON UPDATE NO ACTION ON DELETE NO ACTION; ------------------------------------------- Applying approved migration (2 statements in total): -- create "author" table -> CREATE TABLE "public"."author" ( "id" serial NOT NULL, "name" character varying(1000) NOT NULL, PRIMARY KEY ("id") ); -- ok (2.4938ms) -- modify "book" table -> ALTER TABLE "public"."book" DROP COLUMN "author", ADD COLUMN "author_id" integer NOT NULL, ADD COLUMN "isbn2" character varying(20) NOT NULL, ADD CONSTRAINT "book_author_id_fkey" FOREIGN KEY ("author_id") REFERENCES "public"."author" ("id") ON UPDATE NO ACTION ON DELETE NO ACTION; -- ok (3.5375ms) ------------------------- -- 6.0313ms -- 1 migration -- 2 sql statements这个时候，我们 Apply approved migration，则会自动将变更应用到 "target-database" 中。不过有时候，我们需要进行更多更复杂的操作，例如在上面的例子中，我们需要执行诸如重新设置 "author_id" 的操作时，我们可能就需要使用其他方法来实现这一点了。如果仅仅只是想要查看两个数据库之间的差异而不执行迁移，可以使用 schema diff 命令。基于版本的工作流/代码优先 (Versioned workflow)起步让我们新建一个文件，并将其命名为 schema.sql，并在其中写上我们的表结构：--- schema.sql CREATE TABLE IF NOT EXISTS "user" ( "id" SERIAL PRIMARY KEY, "username" VARCHAR(255) NOT NULL, "password" VARCHAR(500) NOT NULL, UNIQUE ("username") ); COMMENT ON COLUMN "user"."username" IS '用户唯一用户名';然后让我们生成第一个迁移文件：> atlas migrate diff initial --to file://schema.sql --dev-url "docker://postgres/17/dev?search_path=public" --format '{{ sql . " " }}'在上面这个命令中，我们执行了 migrate 命令中的 diff 子命令，并生成一个名字为 "initial" 的迁移文件。其中 --to 参数表示期望的数据库结构，在这里选择的是 "schema.sql" 文件；--dev-url 表示用于处理迁移的数据库连接，在这里使用 docker 中来进行处理，atlas 将新建一个容器并在 dev 数据库中完成迁移内容的生成工作，如果不方便使用容器，也可以使用 DSN 指定一个数据库进行相关操作；而最后的 --format 表示使用何种格式输出，这里我们使用 SQL 格式，并缩进一个空格。执行完成这个命令后，atlas 将生成一个文件夹，并在其中新增一个迁移文件：. ├── migrations │ ├── 20251110124133_initial.sql │ └── atlas.sum └── schema.sql因为这是我们生成的第一个迁移文件，因此其中的内容与我们的 "schema.sql" 文件一致，这里就不再展示了。同一个目录下还有一个 "atlas.sum" 文件，里面有每个迁移文件的哈希值。接下来让我们应用这个迁移文件：> atlas migrate apply --url postgres://postgres:PASSWORD@localhost:5432/example?sslmode=disable Migrating to version 20251110124133 (1 migrations in total): -- migrating version 20251110124133 -> CREATE TABLE "user" ( "id" serial NOT NULL, "username" character varying(255) NOT NULL, "password" character varying(500) NOT NULL, PRIMARY KEY ("id"), CONSTRAINT "user_username_key" UNIQUE ("username") ); -> COMMENT ON COLUMN "user"."username" IS '用户唯一用户名'; -- ok (5.2507ms) ------------------------- -- 34.5275ms -- 1 migration -- 2 sql statements可以看到，迁移文件被正确执行了，并且 atlas 的友好反馈会展示哪些语句被执行了，以及相应的耗时。接下来查看数据库，我们能够看见表也被正确地创建出来了。变更于起步之后接下来，由于业务变更，我们需要调整 "user" 数据表：(1) 追加一个 "display" 的字段用户存储用户的显示名称；(2) 将 "username" 字段调整为 500 个字符的长度；(3) 追加 "username" 的索引。之后我们再追加一个表格 "user_favorite_book" 存储用户收藏的图书的 ISBN，并且这里需要追加外键约束和联合唯一键约束。要实现这些变更，我们只需要修改我们的 "schema.sql" 文件即可：--- schema.sql CREATE TABLE IF NOT EXISTS "user" ( "id" SERIAL PRIMARY KEY, "username" VARCHAR(500) NOT NULL, "password" VARCHAR(500) NOT NULL, "display_name" VARCHAR(500) NOT NULL DEFAULT '', UNIQUE ("username") ); CREATE INDEX IF NOT EXISTS "user_username_idx" ON "user"("username"); COMMENT ON COLUMN "user"."username" IS '用户唯一用户名'; CREATE TABLE IF NOT EXISTS "user_favorite_book" ( "id" SERIAL PRIMARY KEY, "user_id" INTEGER NOT NULL, "book_isbn" VARCHAR(13) NOT NULL, UNIQUE ("user_id", "book_isbn"), FOREIGN KEY ("user_id") REFERENCES "user"("id") );接下来让我们执行下面这个命令：> atlas migrate diff modify_user_and_create_user_favorite_book --to "file://schema.sql" --dev-url "docker://postgres/17/dev?search_path=public" --format '{{ sql . " " }}'我们会发现 "migrations" 文件夹下出现了一个新的文件：. ├── migrations │ ├── 20251110124133_initial.sql │ ├── 20251110132553_modify_user_and_create_user_favorite_book.sql │ └── atlas.sum └── schema.sql其内容就是本次变更的 SQL 内容：--- migrations/20251110132553_modify_user_and_create_user_favorite_book.sql -- Modify "user" table ALTER TABLE "user" ALTER COLUMN "username" TYPE character varying(500), ADD COLUMN "display_name" character varying(500) NOT NULL DEFAULT ''; -- Create index "user_username_idx" to table: "user" CREATE INDEX "user_username_idx" ON "user" ("username"); -- Create "user_favorite_book" table CREATE TABLE "user_favorite_book" ( "id" serial NOT NULL, "user_id" integer NOT NULL, "book_isbn" character varying(13) NOT NULL, PRIMARY KEY ("id"), CONSTRAINT "user_favorite_book_user_id_book_isbn_key" UNIQUE ("user_id", "book_isbn"), CONSTRAINT "user_favorite_book_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "user" ("id") ON UPDATE NO ACTION ON DELETE NO ACTION );可以注意到我们在 "schema.sql" 中的所有变更，都被 atlas 的 migrate diff 命令得到了正确的 SQL 语句。接下来，我们执行相应的命令，便可以将这次的修改应用到数据库中：> atlas migrate apply --url postgres://postgres:PASSWORD@localhost:5432/example?sslmode=disable Migrating to version 20251110132553 from 20251110124133 (1 migrations in total): -- migrating version 20251110132553 -> ALTER TABLE "user" ALTER COLUMN "username" TYPE character varying(500), ADD COLUMN "display_name" character varying(500) NOT NULL DEFAULT ''; -> CREATE INDEX "user_username_idx" ON "user" ("username"); -> CREATE TABLE "user_favorite_book" ( "id" serial NOT NULL, "user_id" integer NOT NULL, "book_isbn" character varying(13) NOT NULL, PRIMARY KEY ("id"), CONSTRAINT "user_favorite_book_user_id_book_isbn_key" UNIQUE ("user_id", "book_isbn"), CONSTRAINT "user_favorite_book_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "user" ("id") ON UPDATE NO ACTION ON DELETE NO ACTION ); -- ok (9.4ms) ------------------------- -- 19.9508ms -- 1 migration -- 3 sql statements实践与问题atlas 提供的两种工作流可以满足绝大多数的场景，但在我们公司和团队中，使用的是 MySQL 数据库和数据库优先开发流程。因而存在一些问题：MySQL AUTOINCREMENT在 atlas 的免费版中，在 Postgres 数据库中的 Sequence 并不会被迁移，因此这些自动递增的值并不会随着结构迁移而迁移。但是因为我们公司使用的是 MySQL ，MySQL 的自动递增值是数据表上一个属性，因福得祸，使用 schema diff 或者 schema apply 执行时，AUTOINCREMENT 也会随之迁移。为了解决这个问题，我将迁移时的工作分成了两步：(1) 生成当前开发/测试环境的数据库的 schema 。使用 schema inspect 获得当前数据库的 schema 文件。然后使用 sed 将其中的 AUTOINCREMENT 的设置去掉。 (2) 使用 schema diff 将期望的数据库和这个移除了 AUTOINCREMENT 的 schema 文件做对比，得到用于迁移的 SQL。尽管操作变得更多了，但是这样做有两个好处：一是每一步的 SQL 语句都可以被审计；二是生成出来的数据库 schema 可以被 AI 读取，AI 可以获取到数据库中的表结构，从而更好的生成代码。无法迁移视图 (View)atlas 的免费版虽然功能已经足够了，但是依然缺少一些功能，例如免费版无法迁移 SQL Server 和 Clickhouse 这类数据库。更麻烦的是无法迁移视图和 Postgres 的扩展，尽管这些东西不太经常变更，但是缺少这个功能，在迁移时还是会增加一些工作量。
数字化簿记系统设计（上）：会计学的古往今来
随着时代发展，我们身边越来越多的事物都被纳入了数字化管理的领域之中，对于公司和组织来说更是如此。无论是公司的财产，还是公司所拥有的生产物资，甚至是公司所拥有的劳动力，一套可以进行审计、分析甚至可以进行预测的信息系统，对于一家具有一定规模的公司而言都是极具价值的。然而开发这样一套系统并非易事，要设计一个合理且稳健的资源管理系统充满了挑战。好在进入数字化时代之前，我们社会中就已经有许多工作在与大量的信息与数据打交道了，在这其中会计学以及一系列会计的工具和方法，可谓是资源管理的“集大成者”。在人类社会发展过程中，许多可以被量化的资源，都可以通过会计工具和方法进行管理。会计簿记通过最简单的方法和规则，构建了一套审计过去，分析当下和预测未来的系统。通过了解并掌握簿记的规则，有助于我们设计一套适用于绝大多数场景的资源管理系统。尽管现实世界通常更加复杂，通用的设计并不能完美满足各个企业的具体工作流程，但我们仍然能从中学习到设计一个可审计的系统的必要元素。本文将分为上下两个篇章，上篇将介绍会计学的历史，引导读者建立基本的会计意识，并介绍一些现代复式簿记的基础规则。下篇将会使用信息技术进行实战。希望对各位读者有所启发。会计的历史记账，是人类社会发展至一个阶段的必然需求。无论是记录食物的收获，还是记录人力的开销，亦或是金银的流通，当人类中某一事物数量的增减，复杂到无法再用双手去计算变化时，记账的需求会自然而生。在文明早期，纸张发明之前，记录文字的成本通常比较高，例如苏美尔文明使用石板作为媒介。因此人们不得不使用其他更易获取的事物来完成记账工作：在一小块粘土上刻上图案烧制成陶块，用以表达一个数目，通过若干个陶块就可以完成计数的工作。随着科技发展，各式各样纸张的诞生，让记录成为了一种稀疏平常的事情。但在很长的时间里，由于宗教、文化等多种因素影响，会计并不是一个专门的职业，它通常和一些文书类的职务有关，因为在这段时期里财产体现多是粮食等作物，并且很长一段时间里面财产的流动都是极为缓慢的。事情的转变发生在欧洲文艺复兴时期，由于宗教至上权威性的瓦解，加之亚平宁半岛（意大利半岛）各种政权林立，束缚人们思想的得以解放，政治格局充满机会，使得这一时期地中海附近的商业发展变得尤为繁盛，这其中以商业共和国威尼斯最为典型。在文艺复兴早期（公元13世纪前后），随着威尼斯的海上贸易的日渐繁盛，复杂的商业行为自然而生，这其中就包括“集资”和“分红”。在那个时期海上贸易是风险极大的商业行为，地中海的飓风、海盗以及目的地动乱的局势等等都可能会让一次出行成为泡影，轻则空载而回，重则一去不复返。但是如果能够进行一次顺利的贸易活动，那么收益也是相当客观的，高风险加之高回报，集资便成为了分担风险的手段。人类自古以来所使用的“流水账”，由于只关心单一账户下的数目变动，因而很难分清谁投资了多少金额的数目，之后又该为其分红多少。于是，记录账户之间数目变动的工具便诞生了。复式簿记法（复式记账法）的发展上图是最早的关于复式簿记的例子，他们出自于 1494 年由意大利数学家卢卡·帕西奥里(Luca Pacioli)出版的一本数学教材《概述：算术、几何、比例和比率》（ Summa de arithmetica, geometria, proportioni et proportionalità )。左侧是原文，右侧是来自于一本 1914 年出版的书籍《古代复式簿记》（ Ancient double-entry )1。尽管其排版与形制与如今的复式簿记有所不同，但其已经具备复式簿记法中最重要的一些要素如：日期、账户（人名）、金额、交易性质以及借贷平衡。下面的内容是由 AI 辅助得到一部分内容的翻译，其中的 L, S, D 是那个时期意大利地区所使用的货币的缩写：关于借方账簿的制作方法1493年。洛多维科，皮埃罗·福雷斯塔尼之子，应在1493年11月14日给出L 44, S 1, D 8，用于现金借贷，已记现金应在第2页有：L 44, S 1, D 8...关于贷方账簿的制作方法...西蒙内之子阿莱西奥·邦贝尼手中的现金，应在1493年11月14日拥有L 44, S 1, D 8，来自洛多维科·皮埃特罗·福雷斯塔尼，L 44, S 1, D 8；以及在11月22日，L 18, S 11, D 6，给马蒂诺，皮埃罗·福拉博斯基之子，第2页：L 18, S 11, D 6...随着文艺复兴影响的扩散，以及欧洲各国的贸易越来越频繁。上述这种用于私人记账或者少数几个人之间核对账户的记账方法，其局限性也愈发体现出来：每个人或者组织的记账格式都不一致；加之市场扩大，组织的形式从少数几个人的集资活动，演变成了股份制公司；同时国家也对财务管理有了切实的需求。其最典型的体现则是法国在 1673 年颁布的《商业条例》（Ordonnance du Commerce)，这部法典在其序言中曾写明，希望借由规章制度规范化商业行为，以减少商人之间的诈骗行为（最典型的就是欺诈性破产，至今依然有这样的犯罪行为）：鉴于这些措施取得了我们所期望的成效，我们认为有义务通过相关规章来保障其长久运行，从而在商人之间确保诚信、防范欺诈，并预防那些因诉讼时间冗长而使他们偏离正业、以及因诉讼费用而消耗其已得之流动资产的各种障碍。—— 1673年法国《商业条例》序言，节选2现代复式簿记19世纪，工业革命在英国爆发，工厂的诞生标志着重资本产业的出现。一些欧洲小国过往百年的资产，在进入工业时代之后，或许也不过是一家大型工厂以及公司的一年的成本而已。19世纪中叶，英国陆续推出了相关法律如《股份公司法》（Joint Stock Companies Act, 1844）、《有限责任法》（Limited Liability Act, 1855)，《公司法》(Companies Act, 1862)等，这些法律在各个层面对于会计簿记和报表进行了规范，也规定了许多与审计相关的要求。时至今日，经过了百余年的发展，会计学已经成为了一门极为重要的管理学科，除了通常所熟知的财务会计外，还有像是管理会计、审计、税务（如一些专门的税务顾问）等众多分野。但万变不离其宗，会计学的一切都是建立在复式簿记这一基石之上，如果没有复式簿记，那么现在我们所熟知的一些金融规则乃至于资产这一概念都将不复存在。现如今的复式簿记是高度标准化的，抛开语言以及税务法律的差异，全世界绝大多数国家的会计流程与规范，甚至所使用的账簿的排版格式都是大致相同的。因而了解现代簿记系统中一些最为基础的规范和要求，这对于我们进行簿记系统的设计具有非常重要的意义。借贷对等与借贷平衡之所以叫做复式簿记，其最典型的特征就是每一笔资金变动都会至少涉及两个账户，分别表示借方 (Debit) 与贷方 (Credit) ，其表达了复式簿记中的核心概念之一——借贷对等。举个简单的例子：我们使用现金从超市购买了一些物资，那么物资账户是借方，现金账户是贷方，记录下来则是如此。摘要账户借方金额贷方金额采购物资物资账户100.00 现金账户 100.00通过上表，我们可以很清晰的看见金额的流动方向，这也是复式簿记法与单式簿记法最大的区别：每一笔交易都是记录了金钱在不同账户之间流转，而非关注于某一个账户的资金来去。千万被借贷二字所迷惑了，所谓借方即增加金额（入账）的一方，而贷方是减少金额（出账）的一方；并且任何账户都可以成为借方或者贷方。例如我们向税务局缴税，那么此时税款账户则是借方，而我们的现金账户是贷方；同理税务局如果退税，那么税款账户是贷方，而我们的现金账户是借方。不过现实中一笔交易通常不会那么简单，其中可能还会涉及到税费等额外的开支，借方和贷方的账户也不是有且仅有一个，在一些复杂的记录中可能会有多个账户参与，例如我们扩展一下上面的例子：摘要账户借方金额贷方金额采购物资物资账户90.00 税费5.00 运输费5.00 现金账户 50.00 应收款项 50.00尽管表格变得更加复杂了，但是我们依然可以清晰地注意到金额在各个账户之间的流动，但无论借贷的账户数量如何变化，在复式簿记中，有一个必须遵守的规则：借方总额等于贷方总额，即借贷平衡。试想一下在上表中，如果借贷不平衡，例如借方总额只有90元，但是贷方总额却是100元，这也就意味着我们花了100元购买物资，却有10元钱去向不明。凭证、日记账、试算平衡表在上面一节中，我们使用表格来描述了一笔资金变动的记录，在会计术语中其被称为凭证 (Voucher) 。凭证是复式簿记系统中的基础记录单位，通过凭证我们能够知晓某一笔资金变动，金额在账户之间的流转关系，以及相关的证据（票据等）。而以某个账户作为主视角，按照日期的先后顺序，根据凭证进行登记，则得到了某一个账户的日记账，也就是所谓的流水账。与凭证不同，日记账的关注点在于对于某个账户而言，其资金的来源与去向。尽管日记账是单式账簿，但是由于其本身的创建依据是复式记账的凭证，因此比起普通的流水账而言，财务簿记的日记账更加严谨。当我们完成这些日常财务记录时之后，在某一财务周期结束时，我们通常需要一些工具去验证我们的财务记录是否正确，而承担这一任务的就“试算平衡表”。简单来说，试算平衡表的用法就是将某一周期内的所有账户的借贷金额罗列出来，并计算其总额是否相同，如果借贷总额不满足借贷平衡这一原则，那么就表示记账数据存在问题。以上这些知识对于我们设计一个稳健的、可审计、可回滚的簿记系统已经足够了。实际上在会计账簿中，还有许多的知识点在本文中没有提及，例如：科目、总账、资产负债表以及会计等式等等。但鉴于篇幅以及本文的主题，在此不再展开讲述。审计、分析、预测当企业按照规范使用复式簿记完成了财务记录之后，这些记录天然地形成了可以进行审计、分析甚至是预测的数据。通过对凭证、平衡表的审计，可以得知企业资金是否有异常流向。通过使用日记账可以快速检索某一账户在某一时期的资金来往，分析企业在某一方面是否投入了超量的资源而未取得足够的回报。更为重要的是，通过长周期的簿记记录，我们可以利用统计方法、人工智能结合市场现状对未来进行科学预测。当然，更重要的是，复式簿记方法并不仅限于财务这一领域，在企业生产管理活动中，一切可以被量化的资源，无论是人力、物资还是财产，只要存在流动，那么理论上都可以通过复式簿记方法进行管理。例如仓库管理系统中，对于入库、出库甚至是内部转移，我们都可以将其视作一个个“流转凭证”，而仓位、来源、去处都可以视为是一个个“账户”。在下篇中，我们将会通过使用关系型数据库、KV数据库设计一个通用场景、高性能、可扩展的簿记系统，并以分销-结算系统为例，介绍这一系统设计其在实战中的应用效果。并通过仓库管理系统的应用描述这一设计在财务领域之外的适用性。 https://archive.org/details/ancientdoubleent00geij/page/n3/mode/2up ↩ https://fr.wikipedia.org/wiki/Code_de_commerce_(France)#Code_Savary ↩
人工智能（下或简称 AI）的发展速度着实是快得超乎想象，2022年底时，和 AI 对话的次数如果太多，它们就会开始胡言乱语、前言不搭后语地产生“幻觉”。两年过去了， AI 不仅不再会出现非常严重的幻觉问题，他们也还具备了思考和看图说话的能力，甚至还有编曲、画画以及制作视频等强大的功能。尽管在当下这个时间节点来看，人工智能对于人类社会的影响似乎还有限，但结合社会发展，让人工智能取代当下社会中部分人的工作，似乎并不是一件坏事。淘汰枯燥与乏味的工作从当下的发展来说，一切只以“结果”为最终评价标准的工作都会被人工智能淘汰掉，根本原因在于 AI 可以比绝大多数人类做得更快更好。以写文章来举例，本文 1000 多个字，我花了近四个小时才写完，但如果把这个任务交给 AI，它可能同样的时间能写上百篇不重样的命题作文。对于一些纪实需求的工作者而言，将一些事实的照片或者视频，加之一些提示性文字告诉 AI，它可能在眨眼间就写出了一篇质量还算不错的稿件；若把这个工作交给人类，即使是专家或许也需要几分钟时间，并且可能还有一些错别字等问题。但对于 AI 来说病句和错别字的问题，出错的可能相较于人类会小很多。而在实体领域，AI 加持下的机器人或许将会取代众多的制造业的岗位。例如自动化机床，其本身就已经实现了自动化进行作业，而人类对于它来说唯一的功能可能就只是上下料、控制程序以及监督而已。而之所以许许多多的制造业工作依然需要人力，除了一些机械设计的固有限制外，沿用至今的机器设计从根本上就没有考虑“人机交互”这个需求会不存在这种情况。但我相信，随着时代的发展，无人工厂、黑灯工厂将会是常态。毕竟生产活动中，抛开效率不谈，需要注意安全的，只有人类而已。再无知识的鸿沟现代教育体系的发展自工业时代以来也不过两百多年时间，而进化到如今系统性、结构性的教育也不过百年不到。在传统教育中，知识是“传承”的，以师徒制为原型，老师将自己所学的知识主观地传递给学生，而学生的知识来源绝大部分都是来自于老师的教授，这一点在现今的科研领域依然是如此。但在进入信息化时代之后，知识的获取途径变得更加多样化，基础知识（小学、中学）在互联网上唾手可得，教师这一角色在这其中的作用，如果单从知识传递的意义上来讲，似乎变得可有可无了。在人工智能时代的今天，任何专业领域的知识的获取都不再是一件难事。上至天文下至地理，从农学到工学，人类从古至今的绝大多数知识都已经被纳入了 AI 训练数据中。你可以通过 AI 学习从幼稚园到大学的任何一门学科的任何知识，而从它那里获取知识的难度，也变得无比简单，只要你想学，它会不顾一切、不惜口舌地将你所想要只晓的一切倾注于你，如果你不能理解，他也会不知疲惫地不断用你能够理解的方式向你解释。而在当下，使用当下最先进的 AI 的成本也不过每个月 20 美元（折换成人民币 140 元）。知识的鸿沟——无论是经济上的、制度上的亦或是人性上的，在这个时代里注定会消失。如今基础教育中教师的角色依然存在，在我看来主要有以下两点原因：(1)在现行教育评价制度下，教师利用其信息优势，教授学生应对考试的技巧；(2)对于义务教育阶段的青少年而言，在学校这一社会环境中，教师承担着看护的职责。而这两点原因正是因为社会发展导致的，对于“教书育人”——这一教师的职责的异化。每个人的自由发展尽管人工智能的发展，在市场经济下自然地会将“人工服务”费用给不断提高。但从长远的角度上来考虑，不会被人工智能取代的服务或者职业，其必然存在着人性的价值，具备着体验导向的意义，而为此付出高昂的费用，在那个时代看来或许不是一个错误的事情。但从另一个视角来看。当我们绝大多数人的生活的主要部分——学习和工作，都不再需要为之付出巨量的时间的时候。每个人都可以选择自由地发展，选择去从事那些“体验性”的事物（就像我还在自己写文章一样），选择成为一个自己想要成为的人，不再需要为了所谓的种种去放弃的理想。那时候上述的那些所谓“费用”或许已然也不会存在了吧。而在当下，尽管迈向我们所期望的社会还存在一定的难度。但尊重每个人的自由发展，必然是迈向那个社会的必经之路。
物联网的标准通信协议：MQTT的介绍和快速上手
最近在给公司的物联网设备（自动售卖机）的控制体系架构进行改版。起因是由于新合作的设备厂商采用了完全不同的通讯协议（MQTT），为兼容新旧设备并尽可能降低对现有业务的影响，我决定重构现有的控制体系。之前厂家直接给的是串口通讯的协议，我这边还需要自己去写串口通讯的代码，然后在这个基础上去构造整个物联网的控制方案。新的厂家好像提供的解决方案更全面一些，只提供了 MQTT 协议的技术文档，没有涉及到串口通讯的内容，因此之前的方案可以说是完全不兼容了。虽然可以为新的设备重新开发一套，但是既然已经有在运营的设备了，对于业务影响最小的解决方案，反而是重构现有的设备控制体系，然后去兼容业务方面的接口。所以才决定重构，否则这么大的工作量，完全没有必要做……当然本文主要还是我在了解和学习 MQTT 之后的一篇（部分）应用费曼学习法的产出文章，如果对读者有帮助，那就再好不过了。什么是 MQTTMQTT 是一款由 IBM 的工作人员内部研发并使用近10年的协议，其最开始的定位是用于计算资源较少以及网络环境受限的场景。而之所以被叫做 MQTT 是因为 MQ 是当时 IBM 一系列的产品的名称的前缀，TT 在最开始是 Telemetry Transport（遥测传输）的缩写， MQTT 最初诞生的应用场景就是人造卫星检测石油管道连接时降低电池损耗以及带宽使用（... developed the MQTT protocol to enable minimal battery loss and bandwidth usage when connecting with oil pipelines via satellite.）2010 年 IBM 公司以免授权费的形式开放了协议（Opened with royalty free protocol）；2013 年全球非营利标准化组织 OASIS 接管 MQTT 协议，并于次年发布了 MQTT 3.1.1 版本。2016 年 MQTT 协议成为 ISO 标准。由于后续的发展与 IBM 再没关系，所以 MQTT 就从 MQ Telemetry Transport 的缩写，成为了正式的名称，即其正式名称就是由四个字母组成的 MQTT。在其后续的发展中，特别是随着 2010 年开始消费者相关的物联网设备极速发展， MQTT 最初设计的特性得到了很好体现，以至于在一些场景和需求下，MQTT 被称作是物联网设备通讯的标准协议。为什么选择 MQTT根据 ISO 标准的介绍，MQTT 的特性大概包含这些：标准轻量化、简单易于实现，并且可以基于其他网络协议实现使用“发布-订阅”方式提供一对多的消息传递数据载荷与无关的消息传递（协议不假设数据载荷类型，由订阅和发布两方自行约定）多种级别的消息传递 QoS (Quality of Service) 保证（服务质量保证）原始版本的 MQTT 是基于 TCP/IP 协议来实现的，实际上也可以基于其他协议如 WebSocket, UDP 甚至是蓝牙。而 MQTT 采用的消息传递是基于发布/订阅的，因此可以实现类似于同一个传感器数据可以同时发送给另一台服务器，或者另一个应用程序。当然 MQTT 最令人兴奋的还是消息传递的 QoS 保证（下简称 QoS），实际做过工程的都知道，消息传递失败是一个非常痛苦并且处理起来也非常麻烦的事情，而 MQTT 在协议层面就提供了三种 QoS: 最多一次、至少一次、精准一次 (Exactly once) 。而这三种 QoS 实际实现起来也非常简单，这部分内容我会在后面的章节中进行简述。核心工作原理以及配置MQTT 的核心工作模式是“发布-订阅”模式，一般采用该模式的架构都会有一个类似于消息总线、消息队列或者消息代理的组件存在，而发送消息和订阅消息的部分都被统称为客户端。 MQTT 也不例外， MQTT 的中间组件被称为 MQTT Broker ，下面这张图可以很好的阐述 MQTT 的整个工作架构。（图来自 EMQX 的文章《MQTT 协议快速入门 2025：基础知识和实用教程》）温度传感器作为客户端连接到 MQTT Broker，并通过发布操作将温度数据发布到一个特定主题（例如 Temperature）。MQTT Broker 接收到该消息后会负责将其转发给订阅了相应主题（Temperature）的订阅者客户端。实际上可以看出来，只要一个设备或者程序实现了 MQTT 客户端的功能，那么它就可以被称为是一个 MQTT 客户端。这个客户端可以是一个库，也可以是一个单独的程序，甚至是一个嵌入式设备。发布-订阅模式MQTT 采用发布-订阅模式也就意味着客户端之间是对等的，且不同于客户端-服务端模式，双方没有直接关联。客户端之间发布消息和订阅消息都要基于一套额外的约定，就好比是 BP 机时代大家都约定某个数字代表不同意涵一样。而这个约定在 MQTT 中被称为 Topic （主题）。主题MQTT 是一个非常强大的工具，利用它可以实现不同颗粒度内容的订阅。MQTT 的主题是采用 / 分割的字符串，类似于 URL 路径，每一个 / 符号分割出来的代表不同的层级。chat/room/1 sensor/10/temperature在订阅 MQTT 消息时，可以使用通配符实现订阅多个主题的内容， MQTT 支持两种通配符：+：表示单层通配符；例如订阅 user/+/chat，就可以匹配诸如 user/alice/chat 或者 user/bob/chat 的主题。#：表示多层通配符；例如订阅 device/report/#，就可以匹配诸如 device/report/sensor/10/temperature 或者 device/report/computer/2/status 的主题。需要注意的是，# 通配符只能用于订阅主题的最后一个层级，诸如 device/#/report 这样的订阅是无效的。还有一类 $ 开头的主题，不过这类主题是系统主题，根据实现不同而有所不同，这里不再赘述。QoS (Quality of Service)MQTT 提供了三种消息传递的 QoS，分别是：QoS 0: 最多一次QoS 1: 至少一次QoS 2: 精确一次当 QoS 0 时，MQTT 保证消息最多只会被传递一次，这是 MQTT 中的传递效率最高的方式。当然它的问题是不保证信息交付。QoS 1 则刚好相反，它保证消息至少有一次被成功交付，但是 QoS 1 存在一个限制，即订阅方可能会收到多次发送方的消息，这是由于 QoS 1 的实现限制导致的。但比起 QoS 2 而言， QoS 1 的开销更小一些，适用于某些允许重复传递的情况。而 QoS 2 则是对 QoS 1 的一种改良，它保证消息仅会被成功投递一次，但 QoS 2 的实现也更加复杂。关于 QoS 1 和 2 的实现将会放到文章末尾简述一下，更详细的资料可以查看这篇文章《MQTT QoS 0、1、2 解析：快速入门指南》如何选择 QoS简单来说，QoS 的选择与数据的重要程度成正比。如果一个数据对于核心业务在一定时间周期内需求不那么高，那么可以选择 QoS 0 ，即使遗漏了在短周期内也没有任何关系。例如花卉植物的温湿度传感器，在一定时间内就算确实问题也不大（除非植物本身对于温湿度高度敏感）。QoS 1 的应用更为广泛一些，因为 QoS 1 保证信息传递成功，而对于消息会重复的情况，实际上对于订阅端来说内部也可以进行去重。因此 QoS 1 比较适合绝大多数的关键操作，只要在实际应用层把去重做好即可。如果不愿意自己去实现去重的话，那么 QoS 2 也是一个更好的选择。QoS 2 本身就实现了保证消息送达，并且确保不会重复，虽然传输成本较高，但是可靠性也更强。对于一些需要非常严谨的场合，例如金融、航空、交通等领域 QoS 2 的使用可能会更多一些。QoS 降级MQTT 中，订阅和发送都可以设置 QoS 等级，在大部分情况下，订阅和发送双方约定一个 QoS 等级时，不会有任何问题。但是当 QoS 订阅和发送方不一致时，Broker 会优先使用订阅者的 QoS 转发发送者的消息，特别是在订阅者的 QoS 等级低于消息的 QoS 等级时。例如对于主题 a ，订阅者订阅时采用 QoS 1 进行订阅，而发送者发送消息时采用的是 QoS 2。此时 Broker 会将消息降级为 QoS 1 发送给订阅者，从而可能会出现多次重复的情况。MQTT 中的 QoS 降级遵循的“木桶效应”，即确保最小的 QoS 可用。并且由于 QoS 从 0 到 2 是递增的实现，因此 MQTT 仅保证更小的那个可以实现就可以。并且 QoS 也不存在升级的可能，即不可能从发送方 QoS 0 升级到订阅方的 QoS 2，因为发送方的在发送时就通过 QoS 声明了其不能处理 QoS 1 和 2 的其他响应报文。遗嘱消息 (Will Message)在有了 QoS 之后，采用了 MQTT 系统的质量和稳定性将会得到巨大的保证。但是 MQTT 客户端可能运行在一些比较脆弱或者低级的设备上，他们硬件本身的可靠性可能难以保证。因此为了保证整个系统的可靠性， MQTT 有一特性叫做遗嘱消息。MQTT 的遗嘱消息是在与 MQTT Broker 建立连接由客户端发送给 Broker 的，遗嘱信息包含主题、保留消息标识、属性、 QoS 以及数据载荷。当 MQTT 客户端在非正常情况下与 Broker 断开连接时，Broker 会向所有订阅该主题的客户端发送消息。在技术上，非常断开连接包含以下情况：Broker 监测到 IO 错误或者网络故障客户端在 Keep Alive 时间内没有通讯客户端在没有发送 Reason Code 为 0x00 的 DISCONNECT 报文的情况下关闭网络连接Broker 收到了客户端发送的不符合协议要求，而关闭与该客户端的连接利用遗嘱消息，我们可以和方便的实现客户端状态的监控。关于这部分，在文章《遗嘱消息（Will Message）介绍与示例 | MQTT 5.0 特性详解》有非常好的最佳实践，并且在 MQTT 5.0 中，新增的延迟遗嘱解决了在旧版本中遗嘱消息发布过于即时，而在重连后又立即发送消息的情况。实战 —— 基于 MQTT 的湿度控制系统在本章节中，将会使用 Python 作为演示用的编程语言，阐述一下基于 MQTT 应用程序的实际开发流程是怎么样的。当然，受限于篇幅，本文中的代码仅会包含重要部分的内容。1. 运行 MQTT Broker要进行 MQTT 的开发，首先需要运行一个 MQTT Broker，MQTT Broker 有非常多的实现，在这里我们使用 Docker 运行一个由国内团队主导开发的 EMQX。docker run -d --name emqx -p 1883:1883 -p 8083:8083 -p 8084:8084 -p 8883:8883 -p 18083:18083 emqx/emqx:5.8.62. 编写传感器和加湿器的代码在这里我们忽略掉一些代码细节，并假设已经对湿度传感器和加湿器的串口通讯代码进行了封装。2.1 安装 MQTT 库**pip install paho-mqtt这里有一个小知识，包名中的 paho 其实标记着这个 MQTT client 的实现是来自于 Eclipse Paho 计划的，这个计划最初是由 IBM 参与的帮助各个语言实现 MQTT 客户端的一个项目，时至今日许多项目已经实质上交给了开源社区进行更新维护。2.2 连接到 MQTT Brokerimport random import json from paho.mqtt import client as mqtt_client from device import sensor, aircond BROKER_URL = os.getenv('BROKER_URL') BROKER_PORT = os.getenv('BROKER_PORT') # 所有 MQTT 客户端都需要有一个 Client ID 标识其唯一性 client_id = f'device-{random.randint(0,100)}' # 实现连接 MQTT Broker 的方法 def connect_mqtt(): def on_connect(client, userdata, flags, rc): if rc == 0: # 连接成功发送设备上线的消息 client.publish(f'device/online/{client_id}') else: print('failed to connect, return code %d\n', rc) client = mqtt_client.Client(client_id) client.on_connect = on_connect client.connect(BROKER_URL, BROKER_PORT) return client2.3 编写读取湿度信息的定时任务我们实现一个每 30 秒从传感器读取一次湿度，然后发送消息的方法。需要注意的是， paho.mqtt 这个库提供了方法，可以方便地让我们的 MQTT 客户端在另一个线程中处理订阅消息以及重连等任务，所以这个任务我们可以将其放在主线程中进行处理。# 包含之前的代码 TOPIC_HUMIDITY_REPORT = f'device/report/{client_id}/humidity' def publish_humidity(): while True: time.sleep(30) humi = sensor.get_humidity() payload = json.dumps({ 'humidity': humi }) result = client.publish(TOPIC_HUMIDITY_REPORT, payload) status = result[0] if status == 0: print(f'send humidity to topic') else: print(f'failed to send humidity to topic')2.4 编写订阅消息启动加湿器的任务# 包含之前的代码 TOPIC_AC_CONTROL = f'device/control/{client_id}/ac' def on_ac_control(payload): datum = json.loads(payload.decode('utf-8')) aircond.set_humidity(datum['humidity']) def subscribe_topics(client): def on_message(client, userdata, msg): match msg.topic: case TOPIC_AC_CONTROL: on_ac_control(msg.payload) case _: print(f'unknown topic received. {msg.topic}') client.subscribe(TOPIC_AC_CONTROL) client.on_message = on_message2.5 编写运行函数# 包含之前的代码 def run(): client = connect_mqtt() # 订阅主题 subscribe_topics(client) # 启动 MQTT 客户端线程 client.loop_start() # 在主线程读取传感器的湿度 publish_humidity(client) # 结束 MQTT 客户端线程 # 但是这里先不处理强制关闭的情况，因为嵌入式设备保证长时间运行 client.loop_stop() if __name__ == '__main__': run()至此，运行在一个“有限设备”上的湿度读取和空调控制的代码就完成了。3. 编写管理程序的代码接下来我们要写个控制程序，这个程序会记录所有设备的湿度信息，并在高于某个值时启动空调调整湿度。这里我们忽略掉重复的创建客户端的部分，而专注于 MQTT 的消息订阅和发布的内容。3.1 订阅湿度传感器报告主题TOPIC_DEVICE_HUMIDITY_REPORT = 'device/report/+/humidity' def on_device_humidity_report(client, msg): client_id = msg.topic().split('/')[2] msg_data = json.loads(msg.payload.decode()) humidity = msg_data['humidity'] # 这里省略插入数据库的操作 if humidity < 50: data = json.dumps({ 'humidity': humidity }) client.publish(f'device/control/{client_id}/ac', data) def subscribe_topics(client): def on_message(client, userdata, msg): match msg.topic: case TOPIC_DEVICE_HUMIDITY_REPORT: on_device_humidity_report(client, msg) case _: print(f'unknown topic received: {msg.topic}') client.subscribe(TOPIC_DEVICE_HUMIDITY_REPORT) client.on_message = on_message3.2 编写运行函数因为我们这个程序只需要在之前确保数据库连接，然后之后不再需要创建，之后只需要监控湿度。因此我们可以使用另一种方式来启动 MQTT 客户端。def run(): # 这里省略创建数据库连接的代码 client = connect_mqtt() subscribe_topics(client) client.loop_forever() if __name__ == '__main__': run()使用 loop_forever() 方法， MQTT 客户端将会在主线程执行，而不同于 loop_start 方法在另一个进程启动。这个方法会占用主线程，因此一些耗时的初始化方法需要在该方法之前调用。应用拓展 I ：动态订阅通过上面的实战可以发现， MQTT 客户端订阅消息其实是发生在连接建立之后的，这就给了开发人员很多场景可以使用，例如我们可以根据程序的需要动态去订阅某些主题，更甚一些可以通过其他客户端发送的消息去订阅新的主题。举个简单的例子：客户端 A 向主题 target/topic/register 发送了一个载荷为 {"topic": "client/name/report", "handler": "ClientReportHandler"} 的消息，收到这条消息的客户端 B 新订阅一个主题，然后通过内部的字典去设置对应的 handler 。应用拓展 II : 物联网以外MQTT 算是一个非常好的协议，它足够轻量化，甚至连动手实现一个支持 QoS 1 的 Broker 也不是一件很难的事情。所以 MQTT 可以用在非物联网的场合下，利用其低消耗和弱网络的宽容度和可靠性，可以实现非常多有意思的内容。网络游戏中部分数据可以通过 MQTT 来传输，特别是对于一些可靠要求较高但数据量不大的内容。另外一些即时通讯软件，可以用来更新和通知用户消息。不过 MQTT 还是有一些限制，那就是单次消息的数据载荷数据大小需要根据具体 Broker 和客户端实现来决定，但通常都不会太大。对于一些大量数据传输的情况，一般可以使用切片的方式，或者直接选择专为较大数据业务的研发的消息队列更为合适。深入阅读：QoS 1 和 2 的实现实际上 MQTT 最吸引人的地方，除了较弱的网络宽容度外，另一点则是提供了三种不同层次的 QoS 可选，这对于复杂的业务开发来说是非常实用的，避免了手工实现 QoS 可能会出现的问题。但仅仅使用不了解本质，对于网络传输相关的工作来说是非常不好的，于是在本文的最后一章，打算简单地谈一谈 MQTT QoS1 和 QoS 2 的实现。本章的大多数内容都来自于《MQTT QoS 0、1、2 解析：快速入门指南》这篇文章，如有看不太明白的地方也建议直接参考原文。QoS 1 的实现如果说 QoS 0 只需要递送，那么 QoS 1 则是发送方需要收到接收方的应答才算是完成。从技术层面来讲，当发送方发送消息时，如果设置了 QoS = 1 ，那么 PUBLISH 报文中会设置一个标记本次消息的唯一值 Packet ID。而接收方收到消息后，需要回传一个 PUBACK 的报文，并附上相同的 Packet ID。而发送方在收到回传的报文后，会释放 Packet ID 使其重新可用于新的消息传输。通过上图就可以得知，QoS 1 会出现消息重复的原因，很多时候其实由于 PUBACK 报文没有传给发送方导致的，发送方则认为消息没有交付成功，则会重新发送进行重试，直至收到 PUBACK 为止。为了解决这个问题，于是便有了 QoS 2。QoS 2 的实现QoS2 的实现复杂，但其本质是通过存储待发报文、状态共享来实现的。相比较 QoS 1 仅仅新增了一个回传报文，QoS 2 完成整个消息传递需要来回传递共计 4 个报文。简单来说 QoS 2 的通讯工作比起 QoS 1 来说新增了许多别的操作：发送方发送报文前会存储一份待发送的报文，用于之后的重传；接收方在收到消息后会存储一个 Packet ID 用于同步发送方的状态；收到消息后，接收方使用 PUBREC 回复发送方，告知已收到消息；此时发送方移除暂存的发送报文，进入 Packet ID 释放流程，暂存 PUBREL 报文用于之后的重发；接收方收到报文 PUBREL 报文后，释放 Packet ID 然后回复 PUBCOMP 报文告知发送方释放；发送方收到 PUBCOMP 报文后，释放 Packet ID。而接收方需要存储 Packet ID 实际上就是 QoS 2 不会重复的要点，实际上在真实场景中接收方回复 PUBREC 也依旧可能会失败，但是由于 QoS 2 的实现里，接收方需要暂存 Packet ID ，这样即使发送方重新发送相同 Packet ID 的报文，接收方并不会进行处理，而是直接重试发送 PUBREC。之后的 Packet ID 释放流程，则是为了保证双方的 Packet ID 都能被正常释放。不过在实际场景中，这个释放流程依旧有可能遭遇网络问题。例如发送方没有收到接收方回复的 PUBCOMP 报文，而重发 PUBREL 报文。因此接收方实现 QoS 2 时，需要确保释放 Packet ID 这个操作是幂等的（即连续执行多次，预期响应和服务器状态不会发生变化）。后记在文章最开始我提到过正在对现有的架构进行改版，实际上让我迫不及待想要更替原因，除了现有架构由于一开始的选型导致有非常严重的缺陷，实际上还有一件非常不齿的事情，那就是我用 WebSocket 实现了在功能上能够覆盖 MQTT 75% 的一个 Agent （主要是它需要根据情况拼装具体给设备的指令脚本，所以不能算是简单的 Broker）。对于不要重复造轮子这件原则而言，我可以说是彻底打破了这一点。现在的这个架构采用消息总线的形式分发消息，并且支持设备断连之后的自动重连等待以及心跳机制，甚至还支持设备断连后向上游反馈（遗言）。但是我始终没有解决消息投递保证 (QoS) 的问题，没有解决的原因也很简单：一是我确实不知道怎么样设计安全的数据重传；二是传输成本太高了（单次设备控制的指令需要传输近 40KB 的数据，这还是简单压缩过后的）。实际上，这次架构改版，本质上也是为了偿还当初技术选型的负债，当时由于刚进这家公司，有些意见发表不具备太高参考性，导致了在选型上出现了较大的问题。如今的工作也算是还债了吧。
我常常喜欢将软件工程项目与土木工程项目视作相同的东西：客户是业主，领导是监理审计单位，产品经理是设计单位，开发是施工单位。互联网上的很多梗图都将开发团队视作上述三者的对立方，这和现实里的工程差不多，即施工单位在整个项目中属于“弱势群体”。当然，这并不是这篇文章讨论重点。本文实际上是以“施工”的角度出发，谈一谈软件工程项目中的一个难点——心智负担。什么是心智负担？简单来说心智负担是指完成一项工作所需要占用的大脑资源，包括但不限于你所拥有的知识、记忆力以及逻辑思维和数学能力。举个简单的例子，有一个100x100的抽屉的柜子，以及一个放着各种各样形状和各种大小的积木的篮子，然后依次给你一些纸条，上面有着许多的操作让你执行，执行完毕之后给你下一张。例如：从篮子里面选出3个小号蓝色圆柱形积木，其中的一号积木放到有红色的三角积木的柜子里面，如果有红色三角积木，则将其放在别处，二号积木放到空柜子，三号积木放到大号绿色方形积木往左数上方第三个柜子里面。完成后需要把所有柜子合上。把上一条指令中的一号小号蓝色圆柱形积木放到三号积木的右下方第五个柜子里。把柜子里面所有的拱形积木拿出来，并把柜子合上。把上一条指令中取出拱形积木的柜子里面放入紫色中号圆锥积木，并把柜子合上。......计算机的工作原理和工作内容大致也是如此：获得输入、执行操作、进行输出；从篮子里取出积木，按照条件找到对应的柜子，然后把积木放进去。但是如果把这个工作交给人脑来处理呢？一万个柜子，假设里面已经放了不少积木了，那么这些操作可能就变得非常复杂了，每一次操作我们可能需要记住上一次开合了哪些柜子，但注意，我这里使用的是“可能需要”，因此为可能之后的纸条上并不会让你用到之前你所记忆的柜子。但是因为你不知道到底需不需要，所以你还是必须记在脑子里。这就是心智负担的一种简单体现，当然，程序员所要面对的问题复杂度更高一些，并且所面对的并不是柜子和积木这类具象的东西，而是一堆抽象的根本不存在的1和0的信息。心智负担的另一个特点即它是动态的，可能一开始这份工作的心智负担并不算高，如果柜子一开始全部是空的，那么这些纸条上的操作都显得无比轻松。但是随着纸条越来越多，柜子里面的积木越来越多，你所需要记忆的在某个纸条中开合过的柜子越来越多，心智负担会逐步提升。超出心智负担会使得我们无法工作吗？就上面的例子来看，并不会，对于信息行业从业者来说也不会。但是这意味着你会花更多的时间，来处理一些本来就很简单的事情上——把上一个打开过的柜子找到，把里面的积木拿出来，把新的积木放进去。但是这些“愚笨的”解决问题的方法，会消费掉你有限的“脑力”，最终让你变得疲惫。这或许也是 AI 的意义吧。虽然它现阶段没法替代脑力劳动者，但是会释放更多的“脑力”，让劳动者更加轻松。如果你是非计算机技术人员，那么你的阅读大概也就到此为止了。因为接下来的内容将涉及到大量的专业术语。工具的心智负担现代软件开发离不开工具，毕竟不会有人从烧沙子开始编写计算机程序。软件开发工具由于工具的开发的需求、时代、人员水平等客观原因，不仅仅会在趁手与否有所不同，也会对使用工具的人造成不同的心智负担。下面是两个典型的例子：内存管理内存管理的心智负担主要出现在 C/C++ 这类需要手动管理内存的语言上，这类语言为了最大限度的去使用内存，将内存管理工作交付给了开发者决定。每一块内存有申请就必须有释放，假设是某个业务功能中临时申请的内存，则更有必要在业务功能结束后释放掉内存。如果没有正确释放，则会导致这块内存再之后可能再也用不了了，最终导致运算资源被浪费从而产生“内存泄漏”。那么我们简单的考虑，是不是只要在函数开头申请在末尾释放就可以了呢？对于简单的需求来说或许如此，但当你的业务需求变得复杂，又有多人一起参与其中，那么内存的释放可能就不一定是在末尾进行了。// The allocated data should be free outside void doSomething(char **input) { char *data = malloc(typeof(char) * 32); // The code you written // ... // ... *input = data; }当然，如果涉及到并发和数据竞争，那么内存管理的心智负担会更大。变量对于某些编程语言来说，变量也是一个会产生心智负担的东西，这主要出现在没有块作用域而且没有常量的语言中。通常来看似乎还好，但是当代码变得具有一定复杂度时，保证已经声明过的变量不会在各种分支和循环中不被修改，变成了一件非常难做到的事情。以 JavaScript 为例，JS 的 var 关键字变量是函数级作用域，并且这个关键字可以直接复写已经声明的变量。function doSomething() { var i = 3; for (var i = 0; i < 10; ++i) { // do something } return i; // now `i` is 10 }这也是为什么，JS 在后面的规范中引入了 const 和 let 两个修正性的关键字。但是 var 的历史遗留问题会导致当维护一个比较旧并且整体架构比较糟糕的系统时，心智负担会变得巨大无比。状态的心智负担对于刚入门编程的人来说，“状态”这一概念是很难理解的。无论大家对于程序的定义是怎么样的，是”算法加上数据结构“，还是”接口加上实现“，但是这些都是对于程序还没有运行时的描述。程序一旦开始运行，上述的这一切都会被转换为对于状态的控制。函数式编程是一种独特的、纯粹的编程范式，其核心在于函数的概念是与数学中的函数的概念对应的。例如 f(x) = x + 1 这个函数，一个给定输入 x，必然会产生一个输出 x + 1 。那么我们就可以将 f(x) 的结果视作是对于 x 的一次变换，即 x 作为状态经过函数 f 的转变。回到现实的编程中，我们绝大多数的函数也都是有输入和输出的，虽然你的输入不一定是从函数的参数里面来的，有可能是从数据库或者是传感器采集到的真实世界的数据；输出也不一定是函数的输出，有可能是输出到数据库或者某个电子屏幕上。但是总归我们的函数一定会有输入和输出。毕竟一个啥都不干的函数，确实没啥存在的必要（通常来看是如此，但是现实世界中只有输出的情况也有很多。）现实中的软件不可能只有一个状态，光是一个函数中我们可能就要维护若干个状态（变量），我们需要根据这些状态来决定其他状态的变换。那么在这种情况下，如果一个状态的维护出现问题，例如它在输入时就不是我们想要的，或者说它在变换的过程中变成了一个我们没有预料到的情况，那么会出现什么问题呢？——短期来看，绝大多数的现实中的软件并不会因为出现了一到两个错误状态就崩溃，但是错误的状态就犹如一个癌细胞，如果不及时处理它就会不断扩散，最终导致系统崩溃。简单且实际的例子：假设一个银行系统中，用户A向用户B转账100元。系统更新了用户A的余额减少。但因某种原因没能更新用户B的余额增加（可能是程序设计错误，甚至有可能是太阳风暴的影响），这个错误状态会导致资金凭空消失。随着越来越多的转账操作，系统的账目平衡会越来越混乱，最终可能需要停机修复并手动对账。尽管现代化的开发工具，以及程序员、测试人员和QA人员会尽所有努力让代码和系统可以处理所有的错误状态，但是人的大脑始终是有限的，并且人类和计算机都很难去处理现实世界的真实随机，毕竟谁也避免不了宇宙射线造成的数据位反转。时至今日面对状态问题，最有效的解决办法只有两个（通俗地讲，因为我也不知道到底该用什么专业术语）：整体成功、整体失败以我们上面的例子来说明一下这个解决办法。假设用户A向用户B转账100元。假设用户A扣减余额成功，但是用户B扣减余额失败。那么这个时候用户A扣减余额的操作将会被撤销（回滚）。用户A向用户B转账的操作则会失败。虽然用户A向用户B转账对于用户来说一个操作，但是对于系统和开发人员来说，其实是两步操作，即先从一个账户上面扣款，然后再加到另一个账户上。整体成功、整体失败的策略可以面对绝大多数现实业务场景中可能会出现的问题。它能够尽可能地确保状态之间的一致性，确保所有状态都是正确的。但是这只能确保状态系统内不会出现不同的错误，它并不能避免因为外部输入问题导致的状态错误。Let it crush （任其崩溃）有一个笑话是说“测试工程师进入一家酒吧点了碗炒饭，然后酒吧爆炸了”。这就是外部输入问题导致的状态错误，对于这种问题，如果是和用户交互的部分实际上反而是最简单的，因为从用户界面、数据处理再到数据库，层层把关，能够杜绝绝大多数输入错误的问题。但是如果输入输出是外部世界呢？例如从一个传感器获取数据数据，假设这个传感器坏了会怎么样。没有经验的开发人员可能会设定一个缺省值，如果设备损坏读取不到值之后，那么就会用这个缺省值作为条件进行处理。但是这个处理方法有一个缺陷，即这个缺省值不是传感器的正常值范围。正确的做法实际上是，在整个模块上一级增设一个低耦合的监视模块/系统（Supervisor），所有子模块、子系统的崩溃都不会影响到监视模块/系统。而如果传感器坏掉之后，那就让整个模块甚至子系统都直接崩溃掉就好了。如果监视模块/系统的策略允许，则尝试重启它，甚至也可以直接放着不管也好。新手程序员会觉得开发语言提供的异常是一个很棘手的东西，因为它会导致整个系统停机。但实际上在绝大多数情况下，哪怕没有监视模块/系统，让系统直接崩掉也好过带病运行。开发环境的心智负担如果说上述两者都是开发过程中客观存在不可避免的心智负担，那么本节所要讨论的“框架与风格的心智负担”则是完全人为的结果。现代的高级的开发环境，通常可以使用许许多多的工具来辅助我们的开发，包括但不限于：语言特性、开发工具的功能（IDE）、外部工具。在这些工具的加持下，绝大多数的信息系统的开发都是一件不太难的事情，也不会让我们的心智负担超载，而是维持在一个平均大多数人都能够处理的水平。但这些工具毕竟都是可选项，一些所谓的经验人士会非常“先验主义”地想当然地认为老方法已经足够了，而不会在项目中使用它们，甚至禁止开发团队的成员使用它们。然而实际上，正确地做法应该是根据项目的需要，从已有的成熟的工具中选择最适合这个项目的，为开发团队成员带来良好的开发体验。例如我的一个真实案例，我此前某家公司的领导禁止使用 JavaScript 中的 Array.map 和 Array.filter 的方法，而是青睐于使用 for 循环：const a = [1, 2, 3, 4]; // Bad var b = []; for(var i = 0; i < a.length; ++i) { b.push(a[i] + 1); }这会有什么问题呢？结合上述此前讲述的两点来看，一是存在工具的心智负担——多了两个变量 b 和 i；二是状态的心智负担——循环内部的下标取值则是一个状态上的心智负担。而正确的做法是什么呢？const b = a.map(x => x + 1);它不仅仅减少了开发者的心智负担，循环时的状态不需要手工去维护，另一方面代码也足够简洁，并不会凭空出现一个之后可能会重复用到的变量。当然，一个好的框架也会减少用户的心智负担。绝大多数系统中都有操作鉴权的这个动作，通常是鉴定用户是否具备某个权限是否可以执行对应的操作。一个好的框架会提供给开发人员足够多的工具可以在各个颗粒度下进行控制，而坏的框架会增加开发人员的心智负担，让人难以下手。一个简单的例子，通过注解来实现重新定义路由以及将控制器级的认证和鉴权，并可以排除某一个操作的鉴权操作。#[Authorize] class DashboardController { // GET /dashboard/public-info #[AllowAnonymous] function getPublicInfo() { } // GET /dashbaord/datalist #[Get('/dashboard/datalist')] function getList() { } }而一个糟糕的架构，处理这个问题则会变得无比复杂，甚至，可能根本做不到。// 主要鉴权在 BaseController 里面 class Dashboard extends BaseController { // 不知道要改动其他什么地方才能让这个控制器方法不参与鉴权 function publicInfo() { if (!request()->isGet()) { // ... } } // 不能给路径进行别名操作 function datalist() { // 只能在函数内部判断是不是想要的 HTTP 方法 if (!request()->isGet()) { // ... } } }有人会反驳“虽然这样写麻烦一点，但是它性能好啊。”可惜现实世界里，人类对于计算机的性能并不敏感，绝大多数的普通人对于300毫秒左右的延迟都没有感觉，但是300毫秒对于计算机网络来说，都是一个比较慢的水平了。而上述两个例子中的错误的示范，所带来的性能优势，可能根本人类根本感知不到。但是对于开发人员来说，痛苦的时间可能就不只是300毫秒了。从入门走向资深如果说初级开发人员只需要保证自己负责功能是正确的，那么高级开发人员，则需要从更加宏观的视角去确保整个系统的正确性。高级开发人员，或者所谓的架构设计人员，不仅仅应该考虑系统的性能、正确性、可靠性等问题，同时也应该考虑为其他开发人员带来的开发体验（Development Experience）。如何设计一个框架，让开发人员能够快速开发，在低心智负担的场景下完成复杂的功能，这对于一个高级开发人员来说是一个非常重要的能力。当然就算你不需要设计一个框架，仅仅是你设计出来的库和方法，也应该尽可能让使用它的人用起来感到舒适和轻松。当然，从入门走向资深，并不是说一定要变成了高级开发人员才需要做到上面这些要求。哪怕只是你自己一个人使用，或者说场景足够单一，你也应该尽可能将一个模块设计地足够简单，让自己哪怕在两个月后看见调用这个模块时，也不会有疑惑：“这到底什么玩意儿”。不过，随着经济下行越来越多的需求是存量的，初级开发人员不能接受到新的需求，或者新的项目也是基于老项目的修改，这些情况下，初级开发人员所能获得的成长有限，而在实际开发过程中，所需要面对的心智负担也不用低。而开发人员面对高心智负担的庞然大物时，处理问题的方法会退回到一种能用就行的状态，而这样的结果会导致后面处理起来更加棘手。而高心智负担的软件项目，会随着维护和开发时的心智负担过高，而变得不可维护。从现在走向未来为什么更早之前的计算机软件开发没有心智负担问题呢？那时的软件开发多以自主研发的商业软件为主，每一家公司内部都有自己的解决方案，而这些问题也因为当时是以闭源为主，没有任何可以对比的情况，所以未能充分的体现出来。而到了如今，软件开发是以开源软件为核心，优质与劣质可以得到充分的比较了，心智负担才得以体现。实际上，现代软件开发工程，使用成熟和便利的工具，搭配上人工智能，开发人员的心智负担会越来越低（当然存量的屎山项目除外），可以以更高的效率产出更多的代码。而不会使用这些现代化工具和人工智能的人，只会被时代所淘汰，就如同马车一样。祝愿大家都能轻松编码！
从2020年编写年终总结文章开始，这已经是第五篇了，回看过去定下目标和职业规划，绝大多数目标都没有实现，而自己的规划也早已偏离既定路线。主观上这些目标没有实现最根本的原因当然还是自我意志不够坚定，客观上环境以及事物的变化导致了目标执行存在困难。尽管许多目标都没有实现，但是值得庆幸的是每年一篇年度总结和记账的习惯保留了下来，这使得我自己可以观察到钱的流向以及我自己的一些变化。回顾旅行年初的时候和妹妹去了趟大理，去了洱海边看了日出，虽然略微有些走马观花，但作为今年我所能回忆起的少数事情，这些回忆我格外珍惜。大理，或者说云南，正如绝大多数人所说只能住在当地游玩，并且如果要与他人同行，你的搭子也最好是和你生活过一段时间，且你也愿意和他继续一起生活的人。如果关系不太深，那在云南旅游就毫无体验，也不过是如同其他旅游目的地一样，到此一游而已。国庆节去了港珠澳，算是这几年久违地独自旅游了，不过由于体质原因，出汗量巨大的我在哪怕不那么炎热的那几天，也并没有去太多地方，留下了太多遗憾，只能今后再去弥补了。这次旅行我也实践了用视频去记录，但是由于剪片需要花费大量的时间和精力，主观客观两方面的作用下，我都迟迟没有剪出来……希望我可以在农历新年之前给剪出来吧，不要把今年的事情留到明年。阅读/观影 （广义输入）今年看过了许多书和内容，但是看完的有作记录的非常少，加之上半年的特殊情况，我都无法回忆起来上半年是否有读完过一本完整的书。或许我也需要像许多人一样，创建一个列表去记录我看过的书和影音作品了，也许后面我会把这个部分做成一个专栏放到个专门的页面上，这样就不用每年再去总结了。《你想活出怎样的人生》（君たちはどう生きるか）这是我今年所能回忆起的看完的影视作品，好像是清明节假期的时候去看的，我还专门写了一篇观影随笔。《控糖革命》这是一本干货不太充足的小书，其主要的观点其实在前几章就已经讲述清楚了，后面的章节通过“他人的案例”作为佐证，反复地强调使得后半部分的阅读体验并不太好。但是其干货部分的内容还是很有价值的：葡萄糖以不同形式存在，且人类必须摄入糖分以维持生命体征葡萄糖以不同形式的存在：淀粉、纤维、果糖、蔗糖控糖实际上是控制葡萄糖峰值的出现，葡萄糖的峰值变动会影响人的饱腹感以及精神状态正确的、避免葡萄糖峰值出现的饮食顺序是：纤维、蛋白质和脂肪、淀粉和糖类餐后70分钟内运动可以有效抑制葡萄糖峰值的出现血糖曲线越平稳，越有利于身体健康，反之则更不健康《地狱乐》今年看过的唯一一本漫画，最开始是周末时看的动画，但是剧情和设定都非常吸引我，加之动画结尾埋下了一个巨大的伏笔，勾起了我的好奇心，于是就去找了原作漫画来看。篇幅并不算长，一共也就 137 话，先期的设定都非常有意思，虽然中期的内容有些拖沓，且加之追加设定（在原有的设定上加入了更多复杂的设定，以确保过去和未来的剧情合理性），但尾声关于反派的结局的处理还算是比较能够让人接受。并且作为群像作品，在结局也加入了后日谈（讲述角色们在剧情之后的故事）的内容，作为读者来说也非常满意了。写作/摄影 （广义输出）相比起前两年除了年终总结外，今年至少还是写了三篇文章——一篇观影随笔，两篇算是开发相关的一些纪实记录。比较特别的是，那两篇开发相关的纪实记录我是用全英文书写的，尽管现在有了大语言模型，能够准确无误地进行多种语言的翻译，但 AI 帮你翻译文章，你再进行阅读检查，这也不过是另一种输入罢了，只有输入没有输出是不可能学好语言的。因此，便诞生了那两篇我“手工”书写的两篇文章，尽管有些用词看起来“非常幼稚”，且因为是记录为主所以没有太多正文，但这是一个好的开始。另外，其实还有一篇英文技术类文章在写，原本是打算介绍某个工具，但是一段时间之后我发现我自己好像没有太多机会去用摄影方面，正如前文所说既有静态摄影也有动态影像，虽然按下的快门并不少，但是让我非常满意的照片和镜头又非常难以挑选，大部分照片在我看来有些太过于“寡淡”了，几经挑选，下面这张照片大概是我今年《年度最佳照片》：财务方面 此内容仅文章作者可见技术和知识方面去年对于自己在技术和知识方面定下来几个方向，今年回头来看居然都多少有所涉及，虽然效果参差不齐但是能够做到之前定下的目标，对我而言也已经是意外之喜了。首先是数学知识，上半年看了一阵子的微积分（《普林斯顿微积分读本》），虽然没看完，而且看了的部分由于搁置了好几个月也忘得差不多了……不过好在用心学过，也做过一些题，如果回头有机会重新捡起来，应该是没啥太大问题。Concurrency 开发技术方面（实在是搞不懂到底中文到底是并发还是并行），由于工作原因不得不接触了我之前非常抵触的 Go 语言，但是却非常意外的掌握了熟练的 Concurrency 开发技术的技巧。虽然更高级的一些用法例如 Context 之类的都没有用到，但是至少能够写出能用的 Concurrency 程序了。而在英语方面，背了一阵子英语单词，但是由于急于求成，目标设得太高了，结果就是每天要花费很多时间来完成计划（大约每天需要花费一个多小时），结果工作一忙起来就根本没有足够的时间。而且感觉背单词的方法，对我目前的状况来说提升太少了，且有些单词过于偏门，日常生活中可能根本就不会用到。新的一年单词肯定还是要继续背，只是方法和计划肯定都要有所改善才行。代码产品方面，没有做出什么太大贡献，为了工作中的项目去 fork 了一份 Go 语言的事件库（太久没更新了，提 PR 也没啥用），并且合并了一些其他人的改进代码，自己也做了一些优化，并且这个项目还意外得到了别人的一个 Star 。另一方面，完善了去年做的一个工具 degit 一个用于从 Git 仓库拉取项目的工具，也利用 GitHub Action 做了一个简单的 CI/CD。展望OKR从DIYGOD的年终总结那儿学到了使用 OKR 追踪自己目标的方法，因此也打算结合自己以往的方法实践下：O1: 身体健康KR1: 体重降低至 BMI 健康标准KR2: 体脂率降低至22%以下（小米体脂秤标准）KR3: 夏天不再一动就出汗KR4: 每周至少运动3次O2: 知识与分享KR1: 读四本书KR2: 每月至少产出一篇文章KR3: 每半年至少出一份多媒体内容O3: 技术与产品KR1: 开发一款独立应用KR2: 用 Ansible 管理自己的服务器KR3: 基于 Gitea 打造一套集成 CI/CD 的开发环境O4: 财务增长KR1: 工资以外收入1万元KR2: 证券投资有所回报
I have a laptop - Thinkpad X1 Carbon 6th generation - with Intel Core i7-8550U, 16GiB RAM and 2K resolution display. It has the performance hardware and a incapable cooling system with only single one fans. After 6 years, the Microsoft Windows bring it terrible experience, the heavy heating make me feel so bad when I use it in the room without air-conditioning. To save this device, I choose to install a Linux on it.Why Debian?There has so many out-of-box Linux distributions with very good Desktop experience like Ubuntu, Fedora. But I finally chose Debian with the following reasons:Ubuntu is "Microsoft Windows (Linux Edition)", Canonical feeds user some features that a few user with non-good experiences.Fedora is good, easy to install and easy to use. Butthe servers I own are running on Debian. And there does not have a good enough rpm-based Linux distro, so I pass the Fedora.For decreasing the cost of learning, I chose Debian as my daily Linux distro.Fighting against the inconvenient networkI spent too many time on the installing Debian into the disk. The most problem is accessing the Debian security repository is bloody slow in China, the average download speed is 2-3KiB per sec and full installing will spend more than 24 hours. So the net-install with the small image is impossible choice.I use the "complete installation image" to install the necessary desktop environment without online package repository. Then I finally finish the installation after spending 4 hours.The problem of 2K resolutionBefore I chose Debian, I tried Fedora on my laptop. The default DE of Fedora workstation is GNOME, it does not have fractional scaling as default, everything is too big in the 200% scale on the 14-inch 2K screen. Then I enable the fractional scaling through the complicated configuring, but the fractional scaling on GNOME is not good, web browser and some applications are blurry.After the bad experience with 2K resolution, I installed the KDE spin of Fedora. Everything is better, include the web browser. I don't know why the both of GNOME and KDE are drived by Wayland, but the GNOME is so terrible on 2K resolution.Even the KDE design system is not unified, I still chose it as my desktop environment.Change the mirrorMost users in China will change the repository mirror of Linux distro package manager after installation. I chose tuna the largest open source mirror site in China (maybe) as my apt repository mirror.Install necessary packagessudo apt install fcitx5 fcitx5-pinyin flatpak plasma-discover-backend-flatpak tmux git curlPackage NamePackage Descriptionfcitx5Input Method Enginefcitx5-pinyinChinese Input MethodflatpakInstall the desktop applicationplasma-discover-backend-flatpakIntegrate into Discover(KDE)Setup flatpak# add the flathub flatpak remote-add --if-not-exists flathub https://dl.flathub.org/repo/flathub.flatpakrepo # change mirror to sjtu flatpak remote-modify flathub --url=https://mirror.sjtu.edu.cn/flathubSetup EnvironmentI have written a post about how to set development environment.
I have 5 devices could be called computer of my own: 3 desktops, 1 laptop and 1 MacBook. One of the desktops became my homelab server and NAS. 4 computers for me may be too more to use, but they can make sure anyone of them down, that I can still work.The motivation to write this article is I want to install Linux on my laptop recently. Its CPU - Intel 8th gen i7-8550U - with terrible efficiency-energy ratio, makes me have no any interesting to use it. Then I spent one week (after work) to install and configure Linux on it. The desktop environments of Linux are not good, even though Gnome and KDE can out-of-box, but I still need to configure more things to make them look great and use conveniently. I do not want to spend more time on it.I back to Windows, the familiar platform with a more complete DE. With Windows Subsystem for Linux (WSL), I also can developing on Linux environment.So I wrote this article, for recoding how I setup my development environment. If I have a new device or re-installing OS, I can recover my workspace quickly.Also, I hope this article can decide my choice of development and production environment.Step 1: Resolve the network problemThe network problem is a big obstacle in China for any programmer. So the first thing for setup is to resolve the problem. For some reasons, I could not provide the detail of it.Step 2: Enable WSL on Windows# Enable the feature of WSL and Virtual Machine Platform dism.exe /online /enable-feature /featurename:Microsoft-Windows-Subsystem-Linux /all /norestart dism.exe /online /enable-feature /featurename:VirtualMachinePlatform /all /norestart# Upgrade WSL wsl --update# Install Debian distro wsl --install DebianStep 3: Install package manager, git and necessary toolsetOn Windows# 1. Allow execution for local script # 2. Install scoop Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser Invoke-RestMethod -Uri https://get.scoop.sh | Invoke-Expression# Install git, it will also install the dependency of it: 7-zip scoop install git, busybox-leanOn macOSInstalling Command Line Tools for Xcode is necessary to install third-party package manager.# after install command line tools, we can use the folloing command to install homebrew /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"On Debiansudo apt update && sudo apt install git curlStep 4: Setup dotfilesBoth for Windows and POSIXgit clone https://github.com/sotvokun/configFor Windows(PowerShell)cd config busybox sh ./setup.shFor POSIXcd config && ./setup.shStep 5: Install the development kit version managerThe package manager provide the convenient way to install program, but the version management of them are too weak. For developing, I prefer to use the common version manager to manage my development kit.versionfox (vfox)versionfox is a cross-platform resolution to manager versions of development kit. It is compatible with asdf-vm.scoop install vfoxasdf-vmthe powerful version manager, asdf-vm only supports the unix-like system. But it is easy to install.git clone https://github.com/asdf-vm/asdf.git ~/.asdf --branch v0.14.0