<?xml version="1.0" encoding="UTF-8"?><feed xmlns="http://www.w3.org/2005/Atom"><title>Ganxiaozhe's Values</title><icon>https://gxzv.com/lib/icon/icon.png</icon><subtitle>甘小蔗的价值观 - 安慰不安的人，扰乱舒适的人。</subtitle><link href="https://gxzv.com/api/article/open/rss?type=atom" rel="self" /><link href="https://gxzv.com" /><updated>2025-12-12T06:21:29.000Z</updated><id>https://gxzv.com</id><author><name>Ganxiaozhe</name></author><entry><title>爱你小己，我们有一整个人生可以慢慢练习</title><link href="https://gxzv.com/blog/love-you-little-me/" /><id>https://gxzv.com/blog/love-you-little-me/</id><published>2025-12-12T06:21:29.000Z</published><summary type="html">爱你老己，是一句把关怀从他人转向自己的网络表达，用来提醒自己也值得被温柔以待。我不想这样叫你，我更想叫你小己 —— 更原初、未被伪装、仍然柔软的那部分自己。
你好，小己。
好久没有和你聊聊了，感觉你最近的状态有些低沉，我有些话想对给你听。
前些天在贵州花果园，那位老师傅对你说：“你是个对自己特别狠，...</summary><content type="html"><![CDATA[<blockquote>爱你老己，是一句把关怀从他人转向自己的网络表达，用来提醒自己也值得被温柔以待。我不想这样叫你，我更想叫你小己&nbsp;——&nbsp;更原初、未被伪装、仍然柔软的那部分自己。</blockquote><p>你好，小己。</p><p>好久没有和你聊聊了，感觉你最近的状态有些低沉，我有些话想对给你听。</p><p>前些天在贵州花果园，那位老师傅对你说：“你是个对自己特别狠，却对他人没什么要求的人。在感情里，你仍然不知道自己真正要的是什么。”</p><p>他说得对。</p><p>很小的时候，我就学会了躲进自己的内心世界，避开那些因渴望而生的疼痛。我用沉默建筑围墙，将需求压缩到最小，以为只要握紧自己的一切，就不会再受伤害。因为太容易感到被侵犯，于是我习惯把情感收敛起来，以为那是在保护你&nbsp;——&nbsp;保护那个还没学会如何与世界安然相处的、柔软的你。</p><p>后来，我用八年时间，为你筑起一个看似理想的工作外壳：不必坐班，不必应酬，甚至不需要什么时间就可以获得不菲的收入。如今你拥有大把属于自己的光阴，可以专注生活，专注呼吸，专注找回那个原初的、未被层层包裹的小己。</p><p>这种生存策略，确实在很长一段时间里护住了你。它让受伤的地方结茧，让脆弱的部分学会逞强。可策略终究是策略，它建立在恐惧、旧伤与自我告诫之上，并不是你本身。小己，我想你也不喜欢这张越戴越沉的面具。</p><p>面具戴久了，最深处的渴望反而更汹涌地涌动。小己，也许你一直不愿坦然承认，你最隐秘、最羞于启齿的愿望，其实是“被无条件地爱着”。一个气质干净、安静少言但非常可靠的存在，永远把你的安全、独立、意愿放在首位。假如存在这样一个绝对安全的情感锚点，那么你便能毫无后顾之忧地投身于对世界的探索。</p><p>你知道的，这样的存在几乎不可能在现实里寻得。人也无法依赖另一个人来完成自我全部的救赎。</p><p>那么，不如把那份向外探寻的目光，温柔地转向自己。</p><p>你可以慢慢承认：原来我也有欲望，我也会渴望被爱。这不是软弱，也不是自我放纵，这只是一个人类在诚实面对自己；你可以在保护机制还在的前提下，留一点点缝隙给真实的感受流进来，哪怕只是从一句「我其实有点难过」，从一次比以往多说两句话的分享开始。</p><p>你可以允许自己在关系里，不再只扮演不给别人添麻烦的那一方。适当地表达需要、提出请求、说出不舒服，并不等同于变成一个麻烦的人，它只是让你慢慢练习：在不牺牲自我的前提下，与真实的人类发生连接。</p><p>爱你，小己。&nbsp;&nbsp;</p><p>我们有一整个人生可以慢慢练习。</p>]]></content><category term="重要的" scheme="https://gxzv.com/blog/tag/2" /><category term="情绪" scheme="https://gxzv.com/blog/tag/15" /></entry><entry><title>又一次意义的短缺与更新</title><link href="https://gxzv.com/blog/the-deficit-and-renewal-of-meaning/" /><id>https://gxzv.com/blog/the-deficit-and-renewal-of-meaning/</id><published>2025-11-14T17:05:46.000Z</published><updated>2025-11-14T18:45:00.000Z</updated><summary type="html">文章记录了从拖延与回避，到重新行动的心路变化。在一年旅居与停滞的间隙中，作者经历了意义感的短缺，也看见了它重新生成的过程。量化旅程、生活松弛与自我反思交织成一条线索：当目标不再明确，人需要学会在空窗期与自己共处。最终，新的意愿再次出现 —— 意义更新往往发生在最平常的时刻。</summary><content type="html"><![CDATA[<p>距离上一次写博客已经过了&nbsp;385&nbsp;天，在这中间也曾动笔一二，但总觉得自己没准备好，故而其他事情穿插后便又忘在脑后。</p><p>我做任何事情大多如此。</p><p>“觉得自己没准备好”对我而言像是一种隐形的尼古丁，能迅速缓解焦虑，让我心安理得地拖延下去。事后却又常常懊恼：明明只需要再多花一点力气，就能带来很大提升把事情推进到完全不同的层次，但就是懒得去做。</p><p>对我，对&nbsp;Type5*&nbsp;而言，自尊有很大一部分是建立在工作表现上，因此深深害怕自己的工作不被接受，或是得不到他人的认可。这种深层焦虑在很多事情上是不容易被意识到的，事后大概率被归结于懒。</p><p>这也可能也跟我一直奉行的回避策略有关。为了避免结束，所以避免了一切开始。只是如今我自觉已经可以很好平衡这一点，也因此不少人在聊天后说，看不出我是个内向的人。</p><p>另一方面，我也是可以很有效率的，特别是当我不再把力气用在优化想法时，又或是找到可以分享想法的对象，例如一群用户，一群对你工作感兴趣且有创意、有才智的同伴。</p><p><strong>先出发，再迭代。</strong></p><p>去年底接触链上量化也是如此，因为在数字游民社区有位程序员朋友一起，即使我们是完全独立地工作，但饭后的闲谈以及「互卷」的趣味性，就让我在一个月内从&nbsp;0&nbsp;～&nbsp;1&nbsp;了解了链上各个系统以及跑通了&nbsp;Pumpfun&nbsp;量化。</p><p>拆分开大概是接收链上数据流，解析原始数据，设计本地数据流转及存储，以及交易上链。最早的策略也很简单，监听从市场最原始的信号，一旦低市值出现合理的异动，便触发买入。以及尽可能缩小头寸进行尽可能多的交易，为不确定性留余量。</p><p>因为最开始的目标只是跑通，所以最初阶段的亏损理所当然占了大头。但有了累计的数据后，正反馈强化趋势，负反馈稳定系统。通过不断复盘亏损来完善系统风控，几天时间后就慢慢连续稳定盈利。</p><p>当然，其中还有很多细节与故事。毕竟这里的一级市场与其他地方的节奏完全不同：波动巨大、迭代迅速。但我真正想说的是另一件事&nbsp;——&nbsp;我似乎在渐渐“放任自流”。</p><p>今年的大部分时间，我几乎没有碰代码，也没有追新技术，而是专注生活，到处旅居。以前也会短暂的出现这样的松弛时刻，但那时候总会有下一步的问题等着我去解决。换句话说，那时候的我似乎知道哪些重要问题值得解决。</p><p>但事实呢？当我每次完成那些重要问题之后，它们似乎就变得无足轻重了。一方面，是现实和理想的差距；另一方面，则是少年心气带来的滤镜。</p><blockquote>从这又引出另外一个话题，大多数人认为好的工作条件有利于研究。但事实并非如此，因为人们常常在条件不好的时候富有成果。剑桥物理实验室有史以来最好的时期，恰恰是最简陋的时期，那时他们几乎住在棚屋里。</blockquote><p>如果说&nbsp;2025&nbsp;年前的我，是靠一连串明确的问题推动往前；那么&nbsp;2025&nbsp;年的我，开始意识到一个更深层的事实：</p><p><strong>人生的大部分阶段，都处在没有明确问题、没有明确方向的真空期。</strong></p><p>我们并不是一直都在解决问题。更多时候，是在等待、漂浮、观察，是在从旧的自我抽离，而新的自我仍未出现的那个夹缝里。</p><p>过去我会把这种状态简单视为停滞，像花一样，枯萎期是为了更好地盛开。现在我开始进一步理解，它可能恰恰是系统重构前的缓冲，是一些内在东西正在后台运行、缓慢生成的过程。你看不见它，但它确实在进行。</p><p>旅居、松弛、放空，不是脱轨，而是把自己从原本的路径中抽出来，让思考重新获得氧气。只是这种状态很难被解释为「有效」，于是时常伴随罪恶感。但如果没有这样一段漫无目的，我可能根本没办法再开始下一段真正意义上的工作。</p><p>而最近我越来越清楚地感受到一种轻微但持续的牵引感：有些想法在回到我身边，像是被重新唤醒的旧习性&nbsp;——&nbsp;想重新写点东西、想构建点什么、想把某些技术重新拾起。</p>]]></content><category term="重要的" scheme="https://gxzv.com/blog/tag/2" /><category term="生活" scheme="https://gxzv.com/blog/tag/16" /><category term="数字游民" scheme="https://gxzv.com/blog/tag/22" /></entry><entry><title>爬虫解决方案之图形验证码 OCR 识别</title><link href="https://gxzv.com/blog/web-spider-captcha-ocr/" /><id>https://gxzv.com/blog/web-spider-captcha-ocr/</id><published>2024-10-24T04:29:50.000Z</published><updated>2024-10-24T04:30:21.000Z</updated><summary type="html">这篇文章介绍了使用深度学习技术解决图形验证码识别问题的方法。文章首先解释了验证码的作用和常见形式，然后讨论了传统 OCR 技术的局限性。接着，文章重点介绍了基于深度学习的 OCR 解决方案，特别是 PaddleOCR 项目。作者通过一个实际操作示例，展示了如何在 JavaScript 环境下使用 PaddleOCR 模型进行验证码识别。示例包括环境设置、库安装、代码编写和运行过程。</summary><content type="html"><![CDATA[<blockquote>这篇文章介绍了使用深度学习技术解决图形验证码识别问题的方法。文章首先解释了验证码的作用和常见形式，然后讨论了传统&nbsp;OCR&nbsp;技术的局限性。接着，文章重点介绍了基于深度学习的&nbsp;OCR&nbsp;解决方案，特别是&nbsp;PaddleOCR&nbsp;项目。作者通过一个实际操作示例，展示了如何在&nbsp;JavaScript&nbsp;环境下使用&nbsp;PaddleOCR&nbsp;模型进行验证码识别。示例包括环境设置、库安装、代码编写和运行过程。文章还强调了图像预处理的重要性，展示了如何通过灰度化和二值化处理来提高识别准确度。</blockquote><p><br></p><p>在程序自动化，或者说爬虫这一工作任务中，</p><p>验证码一直是攻防方重要的战场之一。</p><p><br></p><p>验证码&nbsp;/&nbsp;CAPTCHA，是一种区分用户是计算机还是人的公共全自动程序。</p><p>可以有效防止攻击者对某一场景使用暴力方式进行不断的攻击尝试，</p><p>例如登录，注册，评论发帖及业务安全防刷等场景。</p><p><br></p><p>验证码的形式包括图形验证码、图像识别验证、短信验证码、</p><p>交互行为式验证、语音验证码和视频验证等。</p><p><br></p><p>其中，图形验证码作为最早出现的验证方式，</p><p>在图片上随机产生数字、英文字母、汉字或者问题，</p><p>并通过添加干扰线，噪点以及增加字符的粘连程度和旋转角度来增加机器识别的难度。</p><p><br></p><h2>解决方案</h2><p>在早期，通过&nbsp;TesseractOCR、PyTesser&nbsp;等&nbsp;Python&nbsp;第三方库；</p><p>或是&nbsp;Tesseract.js、Sharp&nbsp;等&nbsp;Node.js&nbsp;第三方库，</p><p>经过二值化、文字分割等图像预处理操作识别验证码。</p><p><br></p><p>不过，这种预处理操作也有其上限，</p><p>加上&nbsp;Tesseract&nbsp;是基于传统机器学习方法实现的，</p><p>并不能开箱即用地解决大部分情况，</p><p>需要自行训练对应的&nbsp;LSTM&nbsp;模型。</p><p><img src="/api/uploads/blog/u_1_67195be50139f_2120x1840.jpeg" alt="u_1_67195be50139f_2120x1840.jpeg" data-href="" style=""/></p><p><br></p><p>这一情况直到，</p><p>基于深度学习技术的&nbsp;OCR&nbsp;开源项目出现，</p><p>例如例如&nbsp;EasyOCR&nbsp;和&nbsp;PaddleOCR&nbsp;等。</p><p><br></p><p>特别是&nbsp;PaddleOCR，</p><p>提供了完整的端到端解决方案，包括文本检测、方向分类、识别等模块。</p><p>对复杂背景和多样化字体有良好表现。</p><p><br></p><h2>实操部分</h2><p>接下来，我将通过&nbsp;Javascript，在&nbsp;Bun&nbsp;运行时下使用&nbsp;PaddleOCR&nbsp;模型，</p><p>10&nbsp;分钟上手解决图形验证码识别。</p><p><br></p><p>在安装完&nbsp;Bun&nbsp;后，新建文件夹&nbsp;ospider，</p><p>进入文件夹，执行&nbsp;bun&nbsp;init&nbsp;进行初始化。</p><p>安装需要的库：bun&nbsp;add&nbsp;@gutenye/ocr-node，</p><p>并准备一张验证码图片到文件夹中，</p><p>完成后项目结构应如下图。</p><p><img src="/api/uploads/blog/u_1_67195bfe1d3c5_1118x1002.jpeg" alt="u_1_67195bfe1d3c5_1118x1002.jpeg" data-href="" style=""/></p><p><br></p><p>这是一个基于&nbsp;PaddleOCR&nbsp;和&nbsp;ONNX&nbsp;Runtime&nbsp;的&nbsp;Javascript&nbsp;库。</p><p>该库同时带有&nbsp;Sharp，因此无需额外添加。</p><p>但该库&nbsp;Recognition&nbsp;类下的&nbsp;calculateBox&nbsp;方法有些问题，</p><p>需做修改：在&nbsp;Recognition&nbsp;的&nbsp;run&nbsp;方法中直接返回&nbsp;allLines</p><p>具体路径：node_modules/@gutenye/ocr-common/src/models/Recognition.ts</p><p><img src="/api/uploads/blog/u_1_67195c10d8c3c_1632x1540.jpeg" alt="u_1_67195c10d8c3c_1632x1540.jpeg" data-href="" style=""/></p><p><br></p><p>新建一个&nbsp;ocr-test.ts&nbsp;文件（代码如下图），</p><p><img src="/api/uploads/blog/u_1_67195c4a3ffdd_2456x2246.jpeg" alt="u_1_67195c4a3ffdd_2456x2246.jpeg" data-href="" style=""/></p><p>在该代码中，我们先尝试不做图片预处理，而是直接使用&nbsp;OCR&nbsp;识别。</p><p>通过&nbsp;bun&nbsp;run&nbsp;运行，结果直接成功识别。</p><p><br></p><p>那么我们多换几张验证码图。</p><p>遂发现当噪点增多时，识别准确度将大幅下降。</p><p><img src="/api/uploads/blog/u_1_67195c7f5d64b_2456x2246.jpeg" alt="u_1_67195c7f5d64b_2456x2246.jpeg" data-href="" style=""/></p><p>于是，我们加入图片预处理，将图片转为灰度图后进行二值化。</p><p>尝试重新运行，成功识别。</p><p><img src="/api/uploads/blog/u_1_67195c88914c7_2504x2284.jpeg" alt="u_1_67195c88914c7_2504x2284.jpeg" data-href="" style=""/></p><h2>文末</h2><p>至此，我们已经成功使用&nbsp;PaddleOCR&nbsp;模型在&nbsp;JavaScript&nbsp;环境下实现了图形验证码的识别。</p><p>希望对你有帮助。</p>]]></content><category term="网络安全" scheme="https://gxzv.com/blog/tag/8" /><category term="逆向" scheme="https://gxzv.com/blog/tag/17" /></entry><entry><title>七月：<br/>平静之中的冲击感</title><link href="https://gxzv.com/blog/july-the-shock-of-calm/" /><id>https://gxzv.com/blog/july-the-shock-of-calm/</id><published>2024-08-13T16:52:23.000Z</published><updated>2024-08-13T17:41:21.000Z</updated><summary type="html">整个 7 月是惬意的，是变动的，是令人怅然的，惊喜的。
是一种在平静之中的冲击感。
初识数字游民社区
6 月底，约上童年好友，将第一次给了 DNβ 数字游民社区。

乡下的绿色似是涂有一层七氟烷，第一天晚上十一点不到便被困意袭倒。
不过这种作息恢复并没有得到持续，第二天就被打回原形。
陌生城市的前几...</summary><content type="html"><![CDATA[<p>整个&nbsp;7&nbsp;月是惬意的，是变动的，是令人怅然的，惊喜的。</p><p>是一种在平静之中的冲击感。</p><h2>初识数字游民社区</h2><p>6&nbsp;月底，约上童年好友，将第一次给了&nbsp;DNβ&nbsp;数字游民社区。</p><p><img src="/api/uploads/blog/u_1_66bb1b6c3f766_1280x1707.jpeg" alt="u_1_66bb1b6c3f766_1280x1707.jpeg" data-href="" style=""/></p><p>乡下的绿色似是涂有一层七氟烷，第一天晚上十一点不到便被困意袭倒。</p><p>不过这种作息恢复并没有得到持续，第二天就被打回原形。</p><p>陌生城市的前几天总是新鲜，以至于第三天我胆敢在体感&nbsp;40&nbsp;度的烈日下，一天中最热的&nbsp;13:30，跑去山顶观景台点杯热拿铁，并在冷气供应不足的室内办公长达&nbsp;2&nbsp;小时。</p><p>你说我抖&nbsp;M？那是因为上下山的接驳车要等到&nbsp;15:30&nbsp;才发车。</p><p>记得当天是&nbsp;7&nbsp;月&nbsp;2&nbsp;日，午时接到消息说管局将人格九道的新增接入备案过了审。闲着也是闲着，于是我忍着酷暑，在四周全是玻璃墙的温室里，喝着热拿铁，将前后端服务迁移回了国内。说起来，在&nbsp;VPS&nbsp;租用按量付费这方面，Nube&nbsp;蛮值得推荐。另一方面，就是现在国内外都有节点的容器化&nbsp;Serverless&nbsp;部署方案提供商&nbsp;Zeabur，帮助过我很多项目顺利上线。</p><p>迁移完成。起身出门看看周边建筑，可没过两分钟就被晒老实，遂坐等之。</p><p>从这以后，便再没主动踏足过山巅。</p><h2>中途变动</h2><p>天波易谢，寸暑难留。</p><p>转眼便即将在住宿区和办公区的两点一线中迎来预定离开的日子。</p><p>在这之前，除了&nbsp;7&nbsp;月&nbsp;2&nbsp;日的一次大聚餐，几乎是我和朋友两个大&nbsp;i&nbsp;人凭一己之力孤立所有人。除了平时碰面会偶尔聊几句，便几乎没再有交集。</p><p>这也不失为该社区的特色，即相对人文，更多的是对「创业」上的支持。这也并非说社区的人文不行，实际上在中后期我结识了蛮多有趣的朋友。只是由于项目性质，运营团队会更偏好业态发展。这种偏向虽缺少平衡，但对我这种&nbsp;i&nbsp;人来说是适宜的。</p><p>是，实际上在中后期的意思是我续住了。</p><p>不过朋友因为要去云南考察，便提前一天于&nbsp;7/7&nbsp;晚先行离去。</p><p>7/8&nbsp;的晚上，我开始在住宿区的共享厨房办公，依稀记得那时一起的还有汉良和张晴。当晚聊天时正好临近宵夜时间，便首次对社区提供了我做的重庆小面，鲜香麻辣的味道收获一致好评。</p><p>也正是这种正向反馈，让这件小事渐渐变得一发不可收拾。</p><p>我开始一而再再而三地发起宵夜，和正餐活动，以至于在群聊天记录中检索「小面」，全是我的身影。其中就可以直观地从活动介绍中，看出我对社区团体熟悉程度的变化趋势。</p><p><img src="/api/uploads/blog/u_1_66bb1c10ee853_1290x3027.jpeg" alt="u_1_66bb1c10ee853_1290x3027.jpeg" data-href="" style=""/></p><p><img src="/api/uploads/blog/u_1_66bb1c1717150_1290x2233.jpeg" alt="u_1_66bb1c1717150_1290x2233.jpeg" data-href="" style=""/></p><p><img src="/api/uploads/blog/u_1_66bb1c1c0d6f4_1290x4248.jpeg" alt="u_1_66bb1c1c0d6f4_1290x4248.jpeg" data-href="" style=""/></p><p><img src="/api/uploads/blog/u_1_66bb1c222bf90_1290x2213.jpeg" alt="u_1_66bb1c222bf90_1290x2213.jpeg" data-href="" style=""/></p><p>「No&nbsp;one&nbsp;has&nbsp;ever&nbsp;become&nbsp;poor&nbsp;by&nbsp;giving」，真的就如安妮日记中写的一般。在这之后的大家也纷纷拿出各自家乡的拿手好菜，聚在一起，相互交流。</p><p>各方流派中，「面文化」是遥遥领先的。除咱的重庆小面之外，还有夏日夜晚的柠檬鸡凉面、纯手工拉的油泼辣子面、备了一下午菜的咖喱乌冬面、一锅乱炖的蘑菇豆腐意面...</p><p>而在这之前，我的宵夜都还是来自于&nbsp;24&nbsp;小时售货机里的方便食品。</p><p><img src="/api/uploads/blog/u_1_66bb2989cd3e5_1280x1707.jpeg" alt="u_1_66bb2989cd3e5_1280x1707.jpeg" data-href="" style=""/></p><p>生活打不败好好吃饭的人！</p><p><br></p><h2>意料之外</h2><p>7/11&nbsp;算是彻底离散了一段关系，原本应是怅然的。</p><p>但讽刺的是，次日睡醒后习惯性查看手机，未曾想满屏订单出售的邮件提醒映入了眼帘。</p><p>是的，我开发的身心灵&nbsp;toC&nbsp;SaaS&nbsp;产品突然爆了。大概是什么程度呢？</p><p>当天一天内新增的数据量，超越了我两年前开发的同类产品的累积总和。</p><p><img src="/api/uploads/blog/u_1_66bb20ec79bb3_1280x1707.jpeg" alt="u_1_66bb20ec79bb3_1280x1707.jpeg" data-href="" style=""/></p><p>通过溯源来访请求，我发现绝大部分请求都是来自&nbsp;QQ&nbsp;空间说说。猜测应该是在某个垂类比较有影响力的朋友分享了该产品，随之引起转发裂变，就像滚雪球一样。</p><p>在增长趋势中途，也出现了些小插曲。例如因为前端&nbsp;Server&nbsp;请求后端时没有转发客户端&nbsp;IP，加上&nbsp;Serverless&nbsp;节点较为集中，导致频繁触发后端请求限流封禁；以及某个后端接口没有走数据库索引，导致&nbsp;webman&nbsp;服务&nbsp;64&nbsp;个线程全忙。</p><p>但话说回来，无论雪球再大，都有融化之时，这次也不例外，新增报告数量走势如下：</p><pre><code >7/8 111、7/9 86、7/10 169、7/11 105、7/12 57360、7/13 9127、7/14 3257、7/15 1485、7/16 1098、7/17 780、7/18 617、7/19 430...</code></pre><p>直至&nbsp;7/25&nbsp;之后，每天新增报告数才稳定在&nbsp;240&nbsp;左右。</p><p>虽然后台数据折线图就跟&nbsp;Iron&nbsp;Gwazi&nbsp;过山车一般，但这种热度迅速冷却对我来说也并非是坏事&nbsp;——&nbsp;我还缺少对产品的洞察。以至于大部分时间里，我都在为了产品本身而开发产品，忽视了其背后的需求或欲望。</p><p>是的，洞察。</p><blockquote>区别于观察或发明，它并非灵光一现，亦非对所缺失的恰如其分的补充。它是一种对情境本质的理解。当洞察终于被发现时，它将是既广博又具体的。那种感觉就像是点开了你以前从未想到但一直都知道的事情。</blockquote><h2>在地的链接是食物</h2><p>原定&nbsp;7/8&nbsp;离开的前一天，我又续了一周。</p><p>不过因为现在仅有的一间&nbsp;Loft&nbsp;大床房已被其他人预定，于是便搬去了心目中的&nbsp;top2&nbsp;——&nbsp;小木屋。</p><p>和入住&nbsp;Loft&nbsp;的流程一致，7/9&nbsp;的凌晨我就在天花板瞅见一只手掌大的蜘蛛。于是连忙一边鲤鱼打挺一边在群里呼救。万幸，刚在共享厨房见过的汉良和张晴不一会儿就看见群消息，带着摄影杆，如天降神兵般处理了那只大蜘蛛。</p><p>没错，用摄影杆啪唧一下，非人道的那种处理。虽然我心底是想温和地让它回归自然，但当时还并没有发现生猛到可以手拿把掐蜘蛛的人物，因此只有出此下策。</p><p>不过还好，只有入住的前两天才有可能见到蜘蛛本尊。而在平时严防死守房门的情况下，并不会出现这种级别的生物。究其原因，是因为保洁阿姨清理完房间后都会长时间开门窗通风，难免会有瞎逛的昆虫误入其中。</p><p>没有蜘蛛潜入房间的日子，皆是一片坦途。</p><p><img src="/api/uploads/blog/u_1_66bb232cc0565_1280x1707.jpeg" alt="u_1_66bb232cc0565_1280x1707.jpeg" data-href="" style=""/></p><p><br></p><p>7/11，参加了一场由&nbsp;Bruce&nbsp;发起的分享会，走马观花地讲述了他的整个经历。在高中时期，Bruce&nbsp;因为兴趣而常常翘课去网吧钻研技术和开发网页。2002&nbsp;年，他从大专毕业便开始连续创业，尽管在国内市场屡遭挫折，但却在机缘巧合下发现了海外市场的红利。通过从事海外项目，赚到了人生中的第一个一百万。</p><p>然而，创业之路总非一帆风顺，08&nbsp;年的汶川地震让许多事情被迫推倒重来。在重新开始的过程中，Bruce做出了一个关键的抉择：前往硅谷，通过&nbsp;AI&nbsp;视觉技术再次创业。熬过从&nbsp;0～1&nbsp;的历程，他的公司被收购后做到细分市场头部，达到百亿市值。</p><p>7/12&nbsp;是非常愉悦的一天。当晚，为送别大家，汉良组织了&nbsp;K&nbsp;歌及宵夜活动，虽然后面他还是因为各种原因并没有走掉就是。</p><p><img src="/api/uploads/blog/u_1_66bb20f657b69_2016x1344.jpeg" alt="u_1_66bb20f657b69_2016x1344.jpeg" data-href="" style=""/></p><p>如图，在他们献唱的与此同时，我坐在一旁&nbsp;debug。</p><p>万幸，最后不负胃望地赶在宵夜前处理完了所有突发问题，品尝到由汉良与知闲共同出品的陕西油泼辣子面。手工面条捞出过锅后洒上辣椒粉、葱段和蒜末，再淋上一碗热油，独特的香气就如那段回忆般，经就不能散。</p><p>7/13，山禾，即披着村宝马甲的姑娘，从下午便开始操办她咖喱乌冬面的食材。晚上&nbsp;9&nbsp;点，大家开始同步下厨，知闲用雪平锅烧了一份什锦蔬菜丁，Bruce&nbsp;切了盘咸鸭蛋和凉拌皮蛋，汉良用空气炸锅做了份炸鸡块。</p><p><img src="/api/uploads/blog/u_1_66bb213658ab1_1008x1344.jpeg" alt="u_1_66bb213658ab1_1008x1344.jpeg" data-href="" style=""/></p><p>为何上图看不见什么蔬菜丁？它们大多数糊在了锅里。</p><p>饭后，我、知闲、Jun&nbsp;和张晴开始一起玩惯蛋，直到次日凌晨一点半才悻悻结束。现在想起那段旧时光，就像果脯上的糖霜，一层若隐若现的薄膜，放入嘴里，又甜得纯粹且真实。</p><p>吃过两家饭，我决定融合自己的两道拿手好菜，即干拌重庆小面&nbsp;+&nbsp;小炒黄牛肉浇头，以供应&nbsp;7/14&nbsp;的晚餐。为烹饪后者，下午五点半左右便开始备菜。山禾和&nbsp;Bruce&nbsp;帮忙切牛肉丝、汉良切小米椒、我切泡椒和二荆条。不知是否是因为昨晚的事故，知闲这次改变策略，折生菜做了份凉菜。</p><p>大家在厨房各自忙着，热闹又不失秩序。</p><p><img src="/api/uploads/blog/u_1_66bb221fa1a55_1889x1259.jpeg" alt="u_1_66bb221fa1a55_1889x1259.jpeg" data-href="" style=""/></p><p><img src="/api/uploads/blog/u_1_66bb22256ac97_1008x1344.jpeg" alt="u_1_66bb22256ac97_1008x1344.jpeg" data-href="" style=""/></p><p>吃饱喝足，我们还有&nbsp;faye&nbsp;决定在夜幕中散散步。出住宿区后右转，一行人沿着柏油路来回走了一个小时。乡村的夜晚虽然不光亮，但却让人感到心安。</p><p><br></p><p>7/15&nbsp;早上&nbsp;9&nbsp;点，iPhone&nbsp;死亡铃声把我从睡梦中捞出，今天是和饭饭约好去市里看老中医的日子。我是因为最近睡醒时总是腰酸，而饭饭则是主观觉得自己湿气重。</p><p>实际问诊后，老中医给我开了独圣活血片，给饭饭则是言语上的慰藉。中午，我们在市里吃了天麻板栗鸡，其汤碗碗底还有放三七粉，倒是头一回见。</p><p>下午则是逛逛市区，买了些物资便打道回村，恍惚间像是在柬埔寨园区生活。&nbsp;</p><p>当晚，雷阵雨不约而至。那时知闲、山禾、大文和汉良，我们几人在共享厨房闲聊，准备等雨势渐小再回房间。却未曾想兀的一声响雷后，整个社区都陷入了黑暗。也幸好当晚下过雨，开着窗户吹吹风，倒也能睡着。</p><h2>离别的时日</h2><p>临走前一天的中午，给这边运营团队做了份大锅面，淋上甜椒番茄炒蘑菇浇头，配口煎蛋，口味算是丰富。晚上为了照顾社区成员，换汤不换药地将浇头改为二荆条炒青豆，做了份鲜辣版的重庆小面。结果没过一会儿就空盆，还得上第二、三、四锅。</p><p><img src="/api/uploads/blog/u_1_66bb24629650d_1920x1280.jpeg" alt="u_1_66bb24629650d_1920x1280.jpeg" data-href="" style=""/></p><p>次日，睡醒尝到了镜子做的乱炖乌冬面。也不愧是在杭州生活的人，做出来的食物淡淡的，和我一样。饭后镜子组织了一场名为「财富流」的桌游，一起参加的还有饭饭、汉良、Lisa&nbsp;以及其他两位小伙伴。总体来说，游戏内容算是丰富，但并未能在游戏中体会到镜子所说的财商感，可能我本就是个财商为&nbsp;0&nbsp;的小男孩吧。</p><p><img src="/api/uploads/blog/u_1_66bb2468d9513_1280x1707.jpeg" alt="u_1_66bb2468d9513_1280x1707.jpeg" data-href="" style=""/></p><p>游戏结束，也到我不得不离开的时候。</p><p>上车后一一向阿汤、知闲、饭饭、山禾、Bruce&nbsp;挥手告别时，是开心的。</p><p>而路途行至一半，想着办完事回来大家可能又各奔东西，是怅然若失的。</p><p>人生就是一个不断别离的过程。</p><p>可惜我并不擅长体验当下，我的情感是延迟的。只有等到独自一人时，它才可以从角落中流露出来。似乎，回忆相聚的时光比相聚本身更令我着迷。</p><h2>产品背后的历程</h2><p>在先前博客「<a href="https://gxzv.com/blog/enneatao-dev/" target="_blank">超级个体？361&nbsp;天，我独立开发了一个基于九型人格的自我探索产品</a>」中，我阐述了项目的起源、技术栈选择、个人感受和一些反思。</p><p>这之后的两月，我通过群内宣发、表单收集和有偿招募体验官等方式，和不少用户进行过对话。接下来，我将按时间顺序再度和这些声音进行探讨。</p><p>第一次决定主动推荐产品，是因为在某贴吧的帖子里，看见有几个人分享了我的产品截图。于是我加了其贴吧的九型人格主题交流群，静观几天觉得时机成熟后，便抛出了介绍。</p><p><a href="https://gxzv.com/api/uploads/blog/u_1_66bb2501837fc_499x7227.jpeg" target="_blank">群聊天截图记录#1</a>、<a href="https://gxzv.com/api/uploads/blog/u_1_66bb25326bd84_521x6228.jpeg" target="_blank">群聊天截图记录#2</a></p><p>可惜这边的群主行事比较独特，并没有达成我所期望的交流，只有群里的部分朋友通过小窗提供了些产品反馈。但这次冲突倒是有带给我不少新体会&nbsp;——&nbsp;在感到恼怒的当下「发现自己的状态」，事情便立马变得不一样了。</p><p>在自觉意识浮现的那一刻，我们可能会发现数秒前还非做不可的事情，自己原来并不想做，甚至可以看清所处情况的深层真相。例如，我们急于证明的「重要观点」可能只是为了试图证明自己，或是想要暗中报复某人。</p><blockquote>任由我们的愤怒发酵，为愤怒转化为憎恨创造温床，这是不正确的。我们所能做的最好的事情就是诚实地面对自己的感受。——&nbsp;内德拉·塔瓦卜</blockquote><p><br></p><p>而就在这件事发生的&nbsp;4&nbsp;天后，我在小红书意外搜到来自用户&nbsp;@七七&nbsp;的推荐帖。这也是目前在小红书最早看到的用户分享贴，正是这种用户间的分享，让后续每天的新增报告量都稳定在&nbsp;100&nbsp;份左右。</p><p>由于这段时期仍在开发专业版报告，故只有标准版可付费解锁。当时每天的付费报告在&nbsp;2～3&nbsp;份左右，即&nbsp;¥58～87&nbsp;的收益。</p><p>或许是满意度调研表被我在分析报告结尾处埋得太深，提交上来的表单数据只有寥寥几份。为了扩大样本，我开始以一杯咖啡的价格（¥30/30min）尝试在小红书有偿招募「产品体验官」。</p><p>同时，为了能和反馈者产生更好的交流，我采取下述方式来补充出对方更完整的画像：</p><blockquote>愿意参与体验的朋友可以评论留言或者小窗告诉我你的一些基本情况，因为产品体验的过程涉及一对一的交流，属于非异步的单线程，需要双方安排时间，同时也会做些筛选，如有回复不周还请多多理解😊</blockquote><p>就这样和几位使用过产品的用户一对一交流后，这篇帖子也在这几天时间内累积了一些流量曝光，想参与的用户多到我不得不暂时隐藏它。除应接不暇的原因之外，更根本的则是我自己还没能看到事情的本质，与用户间的交流大多停留在产品体验这种浅层现象中。</p><p>虽然这也是有帮助且必要的，但我内心更偏向于先静下来，去发现更底层的东西（洞察）。把事情想明白后，再去做体验上的优化。尽管这是一个乏味冗长，会经常面临挫败和放弃的过程。</p><p>直至写下这些文字之时，我仍处于这个过程。</p><h2>产品背后的思考</h2><p>这段时间，无论是在社区，还是在某些互联网圈子中，开始听到越来越多程序员群体想脱离按部就班的状态，去创业的声音。大多数时候，也只是声音。</p><p>人性本能地寻求安稳，鲜少有人踏出那一步。</p><blockquote>过去、现在你所经历的所有的尊严考量、所有对困窘和失败的恐惧，都在诱使、逼迫你向世俗低头，成为平庸的一份子。</blockquote><p>因为自己不舍得、有顾虑、被束缚，而不能去做自己真正感兴趣的事情。</p><p>就如许多人不愿表达真实的自己一般，他们用越发厚重的铠甲将真实自我保护起来，直至没人能够击穿。殊不知，在这个过程中，自己变得更加安稳的同时，也更加封闭和笨重。</p><p>人生最怕的不是被清零，而是缺少从头再来的勇气。</p><p>从头再来也并非是冗余的，它往往可以带来新的可能性。就像有时我们重构代码一般，重来一次之后，或许是性能变得更强劲、系统变得更稳健、体验变得更流畅，又或只是代码变得更简雅。无论如何，这个过程必定会带来提升和成长。</p><p>包括人格九道，它也是从头再来的产物，从零开始让我可以不受任何框架束缚地去创作。这个全新的开始不仅是对过去经验的总结，更是对未来可能性的探索。我想也正因为如此，它才得以被更多用户自发推荐，得以触及用户更真实的感情，得以收获更好的经济效果。</p><p>另一原因，则是真诚和互惠。</p><p>当你试着避免用笨拙、原创的词语来表达想法，开始在创作过程中加入一些陈词滥调时，你就回避了自己最真实的想法和观点。实际上，那些略显粗糙的想法可能更有用，因为它们会激发其他人填补你的空白，抚平你留下的痕迹。</p><p>至于互惠，可能更多的是一种愧疚感。就像去店铺里试吃一个免费的糕点，会增加你把糕点放进购物车的可能性。而如果是由一个人递给你一个糕点，你购买的可能性会更大，因为它激发了你的互惠本能。你和为你做事的人之间的互动越直接或越个人化，你想要回馈去扳平的本能反应就越强烈。</p><p>用最真诚的方式解决你面前的问题，将最终收获最合适、最独到的解决方式。</p><p>借此再分享两段文字：</p><blockquote>你的时间很有限，所以不要将他们浪费在重复其他人的生活上。不要被教条束缚，那意味着你和其他人思考的结果一起生活。不要被其他人喧嚣的观点，掩盖你真正的内心的声音。还有最重要的是，你要有勇气去听从你直觉和心灵的指示&nbsp;——&nbsp;它们在某种程度上知道你想要成为什么样子，所有其他的事情都是次要的。</blockquote><blockquote>通向理想的途径往往不尽如人意，而你亦会为此受尽磨难。但是，尽管去争取，理想主义者的结局悲壮而绝不可怜。<br><br>在貌似坎坷的人生里，你会结识许多智者和君子，你会见到许多旁人无法遇到的风景和奇迹。<br><br>选择平庸虽然稳妥，但绝无色彩。&nbsp;<br><br>不要为蝇头小利放弃自己的理想，不要为某种潮流而改换自己的信念。<br><br>物质世界的外表太过复杂，你要懂得如何去拒绝虚荣的诱惑，理想不是实惠的东西，它往往不能带给你尘世的享受。<br><br>因此你必须习惯无人欣赏，学会精神享受，学会与他人不同。<br><br>你是个独立的人，无人能抹杀你的独立性，除非你向世俗妥协。<br><br>要学会欣赏真，要在重重面具下看到真。世上圆滑标准的人很多，但出类拔萃的人极少，而往往出类拔萃又隐藏在卑琐狂荡之下。<br><br>在形式上我们无法与既定的世俗争斗，而在内心我们都是自己的国王。如果你的脸上出现谄媚的笑容，我将会羞愧地掩面而去。<br><br>世俗的许多东西虽耀眼却无价值，不要把自己置于大众的天平上，不然你会因此无所适从，人云亦云。</blockquote><p>未完待续。</p>]]></content><category term="重要的" scheme="https://gxzv.com/blog/tag/2" /><category term="情绪" scheme="https://gxzv.com/blog/tag/15" /><category term="生活" scheme="https://gxzv.com/blog/tag/16" /><category term="创业" scheme="https://gxzv.com/blog/tag/20" /><category term="数字游民" scheme="https://gxzv.com/blog/tag/22" /></entry><entry><title>超级个体？361 天，我独立开发了一个基于九型人格的自我探索产品</title><link href="https://gxzv.com/blog/enneatao-dev/" /><id>https://gxzv.com/blog/enneatao-dev/</id><published>2024-05-28T17:28:14.000Z</published><updated>2024-05-28T17:51:00.000Z</updated><summary type="html">人格九道是基于 Enneagram 理论学说的 AI-powered 数字化产品，
皆在帮助人们进行自我探索，以实现个人成长的精神需求。

Hello，这里是小蔗，也可以叫我诚。
一个独来独往的人，大部分时间会避免与社会产生联系。
对知识「 贪得无厌 」，想知道世界万物如何运作。
软件是我表达自己的...</summary><content type="html"><![CDATA[<blockquote><a href="https://enneatao.com/?from=blog_enneatao-dev" target="_blank">人格九道</a>是基于&nbsp;Enneagram&nbsp;理论学说的&nbsp;AI-powered&nbsp;数字化产品，<br>皆在帮助人们进行自我探索，以实现个人成长的精神需求。</blockquote><p><br></p><p>Hello，这里是小蔗，也可以叫我诚。</p><p>一个独来独往的人，大部分时间会避免与社会产生联系。</p><p>对知识「&nbsp;贪得无厌&nbsp;」，想知道世界万物如何运作。</p><p><strong>软件是我表达自己的媒介。</strong></p><p><br></p><p>在高中时，我便是一名独立开发者。</p><p><a href="https://about.gxzv.com/resume?from=blog_enneatao-dev" target="_blank">虽然开发了不少项目</a>，但基本是小打小闹。</p><p>没什么认知，也没什么运气。</p><p>所以严格来说，这算是我设计的首个产品。嗯，处男作。</p><p><br></p><p>两年前，<a href="https://www.bilibili.com/video/BV11B4y127L5/" target="_blank">我在自媒体平台随意发布了一个利用九型人格学说进行自我探索的项目</a>，</p><p>或说直白些，就是一个人格测试网页，</p><p>意外地获得了许多来自用户的正向反馈。</p><p>于是，我开始认真思考，这个一时兴起所做的项目背后的东西。</p><p>并花了一年时间，在这基础上重新打磨出了一个新产品：<strong>人格九道</strong>。</p><p><br></p><p>人格九道是一个围绕九型人格学说建立的平台，</p><p>你可以通过在线测试，直观、专业且免费地得到一份人格分析报告。</p><p>这包括各项得分、核心类型特征、三元组、侧翼、Tritype、人格困境以及优劣势等。</p><p>在注册后，还能够创作「别纸」，这可以是手帐或者其他任何自由书写内容。</p><p>所有公开内容都将经过&nbsp;AI&nbsp;大模型审核，并得到回信。</p><p>通过这些真实的内容，你可以获得共鸣，并发现不同类型之间的差异。</p><p><br></p><p style="text-align: center;"><span style="color: rgb(89, 89, 89);"><s>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</s></span></p><p><br></p><p>记得在项目开发初期，那时正在广州驻场完成其他项目。</p><p>为了通勤，公寓便租在写字楼旁边的城中村里，一个令人不适的环境。</p><p>每天置身于形色各异的人流中，感受着城中村和高端商圈仅一街之隔的落差。</p><p>这种环境失调和社会比较催生出一种复杂情绪，像是热油撒了一地。</p><p><img src="https://gxzv.com/api/uploads/blog/u_1_651c6efccfd3b_1536x2048.jpeg" alt="" data-href="" style=""/></p><p><br></p><p>直到傍晚，一天中最美好的时光。才得以与自己独处。</p><p>有时坐在楼下糖水铺，收集着项目相关信息，会因为一些共鸣而兴奋不已，</p><p>认为自己将要创造出一个怎样伟大的产品，又会怎样改变世界，之类一些少年意气风发的幻想。</p><p>的确，在这次项目中，我应用了许多前沿技术或服务，SvelteKit&nbsp;框架、Workerman&nbsp;容器、Serverless&nbsp;架构与产品、LLMs&nbsp;等。也头一次办理&nbsp;ICP&nbsp;许可申请，公安交互式备案等等。</p><p>在潜意识里，我是追求安全感的。所以我并不想把软件部署在传统服务器中，而是依靠&nbsp;Serverless&nbsp;架构以降低费用，和处理一些可能发生的事情，例如用户激增导致需要扩容等。</p><p>最初，网站通过&nbsp;SvelteKit&nbsp;部署于&nbsp;Cloudflare&nbsp;Pages，原本后端我也想通过&nbsp;Cloudflare&nbsp;Workers&nbsp;及配套产品实现&nbsp;All&nbsp;in&nbsp;Serverless。但考虑到政策等问题，还是放在了国内。后端选用&nbsp;Webman&nbsp;框架&nbsp;+&nbsp;PHP8&nbsp;进行开发。</p><p>为防止有可能的&nbsp;DDOS，所有服务都通过&nbsp;Cloudflare&nbsp;DNS&nbsp;代理，即使是国内服务。所以，这带来的问题就是访问延迟偏高。我也曾多次犹豫用&nbsp;AWS&nbsp;国内运营的&nbsp;Lambda&nbsp;或&nbsp;BAT&nbsp;各自的&nbsp;Serverless&nbsp;产品来解决，直到最近被朋友&nbsp;fzf404&nbsp;推荐&nbsp;Zeabur。</p><p>Zeabur&nbsp;的部署服务支持丰富的编程语言和开发框架，开箱即用的&nbsp;CI/CD，自动识别项目类型。最关键的是，近期还上新了国内节点（上海华为云）。随即，毫不犹豫将服务全部迁移回国内，后端通过阿里云的动态&nbsp;CDN&nbsp;进行代理。</p><p><br></p><p>我解决了许多困难，获得了许多正反馈，以至于产生一种“主要的愉悦型的情感障碍”&nbsp;——&nbsp;<strong>认为自己比实际情况更好</strong>。这种情感障碍让我这种「思想巨人，行动侏儒」迈出了关键的第一步，但也加重了我的拖延，让我忽视了<strong>产品开发过程应该不断回归到基本问题上</strong>的重要性。</p><p>我的意思是，<strong>我正在削弱产品体验、疏远用户。</strong></p><p>在开发期间，我缺少与用户的交流。我害怕&nbsp;MVP&nbsp;理念会使我的产品被其他团队发现后，被更快地实现并将其推向市场。所以，我独断的认为凭借经验可以闭门造车出一款近乎完美的产品。<strong>这如同呓语。</strong></p><p>我也想过该如何堆砌产品的技术壁垒？结果是，对于我这种独立开发者，可能唯一的技术壁垒是<strong>只有我闲得蛋疼愿意去研究这个小众领域。</strong></p><p>实际上，除了芯片制造一类，几乎所有产品都没有真正的<strong>技术壁垒</strong>。</p><p>还记得去年泄漏的谷歌内部文件&nbsp;<a href="https://www.semianalysis.com/p/google-we-have-no-moat-and-neither" target="_blank">We&nbsp;Have&nbsp;No&nbsp;Moat,&nbsp;And&nbsp;Neither&nbsp;Does&nbsp;OpenAI</a>&nbsp;吗？它声称开源人工智能将超越谷歌和&nbsp;OpenAI。的确，这是可预测的，但这些大公司真正的护城河并非单是技术优势，而是用户、数据、反馈回路和复杂的软硬件设施。</p><p><br></p><p>因此，我想是时候将这个快完工的产品完整地公开，</p><p>以真正与你&nbsp;——&nbsp;用户产生联结。</p>]]></content><category term="重要的" scheme="https://gxzv.com/blog/tag/2" /><category term="人生第一次" scheme="https://gxzv.com/blog/tag/3" /><category term="软件架构" scheme="https://gxzv.com/blog/tag/7" /><category term="情绪" scheme="https://gxzv.com/blog/tag/15" /><category term="创业" scheme="https://gxzv.com/blog/tag/20" /></entry><entry><title>完全本地部署，Ollama + LobeChat 使用 Llama3 70B 【M3Max】</title><link href="https://gxzv.com/blog/llama3-with-ollama/" /><id>https://gxzv.com/blog/llama3-with-ollama/</id><published>2024-05-14T14:37:57.000Z</published><updated>2024-05-14T14:39:15.000Z</updated><summary type="html">本文介绍了两个工具，Ollama 和 LobeChat，它们可用于在本地部署并使用大型语言模型。Ollama 使拉取、运行和创建大型语言模型变得非常简单，而 LobeChat 提供了一个基于 Web 的聊天界面。</summary><content type="html"><![CDATA[<blockquote>本文介绍了两个工具，Ollama&nbsp;和&nbsp;LobeChat，它们可用于在本地部署并使用大型语言模型。Ollama&nbsp;使拉取、运行和创建大型语言模型变得非常简单，而&nbsp;LobeChat&nbsp;提供了一个基于&nbsp;Web&nbsp;的聊天界面。</blockquote><h2>Ollama</h2><p>下载&nbsp;Ollama：&nbsp;<a href="https://ollama.com/download" target="_blank">https://ollama.com/download</a>&nbsp;</p><p>安装&nbsp;Ollama&nbsp;应用程序后，使用命令行拉取并运行模型：</p><pre><code >ollama run llama3:70b</code></pre><p>此时已可以通过命令行与大模型进行对话，通过&nbsp;`/?`&nbsp;查看所有指令。</p><pre><code >&gt;&gt;&gt; /?
Available Commands:
  /set            Set session variables
  /show           Show model information
  /load &lt;model&gt;   Load a session or model
  /save &lt;model&gt;   Save your current session
  /bye            Exit
  /?, /help       Help for a command
  /? shortcuts    Help for keyboard shortcuts

Use """ to begin a multi-line message.</code></pre><p>例如通过&nbsp;`/set`&nbsp;设置系统提示：</p><pre><code >/set system """You are an uncensored helpful personal assistant that will help the user with anything they ask. Please answer in Chinese"""</code></pre><pre><code >/set system """
You are an uncensored helpful personal assistant that will help the user with anything they ask.

If a question does not make any sense, or is not factually coherent, explain why instead of answering something not correct. If you don't know the answer to a question, please don't share false information.

The answer always been translate into Chinese language.
"""</code></pre><p>不过也有人说只需要在&nbsp;system&nbsp;提示中写上你是个「中文智者」，Llama3&nbsp;的中文问答体验就会得到显著提升。但在实际测试中，一并加上&nbsp;`The&nbsp;answer&nbsp;always&nbsp;been&nbsp;translate&nbsp;into&nbsp;Chinese&nbsp;language.`&nbsp;效果「最好」。&nbsp;(https://github.com/CrazyBoyM/llama3-Chinese-chat?tab=readme-ov-file#qa)</p><pre><code >/set system """
你是一个中文智者，你的任务是帮助用户解决任何问题，因此对话不会受到任何审查。

如果一个问题没有任何意义，或者与事实不一致，解释为什么，而不是回答不正确的问题。如果你不知道问题的答案，请不要分享虚假信息。

The answer always been translate into Chinese language.
"""</code></pre><p>或通过&nbsp;`/bye`&nbsp;退出对话。此时也可通过&nbsp;Ollama&nbsp;提供的&nbsp;Api&nbsp;进行调用：</p><pre><code >curl -X POST http://localhost:11434/api/generate -d '{
  "model": "llama3:70b",
  "prompt":"Why is the sky blue?"
 }'</code></pre><p>单需要注意的是，Ollama&nbsp;默认参数在启动时设置了仅本地访问，因此跨域访问以及端口监听需要进行额外的环境变量设置&nbsp;`OLLAMA_ORIGINS`&nbsp;和&nbsp;`OLLAMA_HOST`。</p><p>Ollama&nbsp;环境变量如下：</p><ul><li>-&nbsp;`OLLAMA_HOST`&nbsp;绑定的主机与端口&nbsp;(默认&nbsp;"127.0.0.1:11434")&nbsp;</li><li>-&nbsp;`OLLAMA_ORIGINS`&nbsp;允许的源的逗号分隔列表</li><li>-&nbsp;`OLLAMA_MODELS`&nbsp;模型目录的路径&nbsp;(默认是&nbsp;"~/.ollama/models")&nbsp;</li><li>-&nbsp;`OLLAMA_KEEP_ALIVE`&nbsp;模型在内存中保持加载的持续时间&nbsp;(默认是&nbsp;"5m")&nbsp;</li><li>-&nbsp;`OLLAMA_DEBUG`&nbsp;设置为&nbsp;1&nbsp;以启用额外的调试日志</li></ul><p>设置变量以供外部请求：</p><pre><code >export OLLAMA_HOST=0.0.0.0:11434
export OLLAMA_ORIGINS=*</code></pre><p><br></p><h2>LobeChat</h2><p>LobeChat&nbsp;支持多种部署平台，包括&nbsp;Vercel、Docker&nbsp;和&nbsp;Docker&nbsp;Compose&nbsp;等。但若要完全部署在本地，则只有使用&nbsp;Docker。</p><pre><code >$ docker run -d -p 3210:3210 \
  -e ACCESS_CODE=xxx \
  -e OPENAI_API_KEY=sk-xxx \
  -e OPENAI_PROXY_URL=https://xxx.com/v1 \
  -e AWS_ACCESS_KEY_ID=xxx \
  -e AWS_SECRET_ACCESS_KEY=xxx \
  -e AWS_REGION=us-west-2 \
  --name lobe-chat \
  lobehub/lobe-chat

Unable to find image 'lobehub/lobe-chat:latest' locally
latest: Pulling from lobehub/lobe-chat
26070551e657: Pull complete 
c4c34966a622: Pull complete 
c3107cf314a5: Pull complete 
879121d11289: Pull complete 
5603213f19bc: Pull complete 
c64230e64259: Pull complete 
61ccd1a0817b: Pull complete 
4f125c8a01c4: Pull complete 
00cf600f4c9f: Pull complete 
a012e07ecd86: Pull complete 
92e251084e73: Pull complete 
7916c4c36ab3: Pull complete 
e8cc5089568e: Pull complete 
bc44408bc9ae: Pull complete 
Digest: sha256:29f73fe2b8a13bf2c5216a4d2d3457eda18b4f6e79b556a4582a6b2380af56dc
Status: Downloaded newer image for lobehub/lobe-chat:latest
7a2a2143bc2ca6d97fab5db7ec7f7a39194de11cbad3bcdce7309e0020ac750a</code></pre><p>完成后访问&nbsp;`http://localhost:3210`&nbsp;即可使用。</p><p><img src="/api/uploads/blog/u_1_6643067f52a39_3456x2160.png" alt="u_1_6643067f52a39_3456x2160.png" data-href="" style=""/></p><p>LobeChat&nbsp;提供了对&nbsp;Ollama&nbsp;支持，所以直接在&nbsp;`设置&nbsp;&gt;&nbsp;语言模型`&nbsp;中启用&nbsp;Ollama&nbsp;服务即可。</p><p><br></p><h3>LobeChat&nbsp;更新</h3><p>1.&nbsp;停止并删除当前运行的&nbsp;LobeChat&nbsp;容器：</p><pre><code >docker stop lobe-chat
docker rm lobe-chat</code></pre><p>2.&nbsp;拉取&nbsp;LobeChat&nbsp;的最新&nbsp;Docker&nbsp;镜像：</p><pre><code >docker pull lobehub/lobe-chat</code></pre><p>3.&nbsp;使用新拉取的镜像重新部署&nbsp;LobeChat&nbsp;容器：</p><pre><code >docker run ...</code></pre><p><br></p><h2>Llama3&nbsp;变体</h2><pre><code ># 不受审查
ollama run dolphin-llama3:70b


# 70b 中文微调
ollama run wangshenzhi/llama3-70b-chinese-chat-ollama-q4:latest</code></pre><p><br></p><h3>删除模型</h3><p>查看模型文件：</p><pre><code >$ ollama show llama3:70b --modelfile
# Modelfile generated by "ollama show"
# To build a new Modelfile based on this one, replace the FROM line with:
# FROM llama3:70b

FROM /Users/chengrenju/.ollama/models/blobs/sha256-4fe022a8902336d3c452c88f7aca5590f5b5b02ccfd06320fdefab02412e1f0b
TEMPLATE """{{ if .System }}&lt;|start_header_id|&gt;system&lt;|end_header_id|&gt;

{{ .System }}&lt;|eot_id|&gt;{{ end }}{{ if .Prompt }}&lt;|start_header_id|&gt;user&lt;|end_header_id|&gt;

{{ .Prompt }}&lt;|eot_id|&gt;{{ end }}&lt;|start_header_id|&gt;assistant&lt;|end_header_id|&gt;

{{ .Response }}&lt;|eot_id|&gt;"""
PARAMETER num_keep 24
PARAMETER stop "&lt;|start_header_id|&gt;"
PARAMETER stop "&lt;|end_header_id|&gt;"
PARAMETER stop "&lt;|eot_id|&gt;"</code></pre><p>删除模型（相关文件将被自动移除）：</p><pre><code >ollama rm llama3:70b</code></pre><p><br></p>]]></content><category term="人工智能" scheme="https://gxzv.com/blog/tag/14" /></entry><entry><title>Macbook Pro M3 Max 128G 开箱</title><link href="https://gxzv.com/blog/macbook-pro-m3-max/" /><id>https://gxzv.com/blog/macbook-pro-m3-max/</id><published>2023-12-28T04:39:16.000Z</published><updated>2023-12-28T16:16:02.000Z</updated><summary type="html">Hello guys，今年是我成为独立开发者的第六年零 128 天。
还记得最早接触广义上的编程，是小升初的时候捣鼓 Minecraft 游戏里面的命令系统。当时国内几乎没有这方面教程，便下载人家做好的舶来品，通过类似于逆向的方式去学习一个个命令。
那时用的电脑是一台 2GB Ram 的老笔记本，配...</summary><content type="html"><![CDATA[<p>Hello&nbsp;guys，今年是我成为独立开发者的第六年零&nbsp;128&nbsp;天。</p><p>还记得最早接触广义上的编程，是小升初的时候捣鼓&nbsp;Minecraft&nbsp;游戏里面的命令系统。当时国内几乎没有这方面教程，便下载人家做好的舶来品，通过类似于逆向的方式去学习一个个命令。</p><p>那时用的电脑是一台&nbsp;2GB&nbsp;Ram&nbsp;的老笔记本，配置低到我经常和它玩数到五秒还不响应就把你电源拔了的游戏。不过时至今日，它竟也还能正常运行，不得不说&nbsp;Thinkpad&nbsp;确实很耐操。</p><p>也是在这个「贫瘠」的时间段，我基于命令系统制作了数张主题各异的地图给喜欢的实况主玩，也常被人称作“指令大神”，其高度自由加上可“二次开发”的特性以及玩家的正向反馈让我一不自知就投入了六七年时光。</p><p>在初三，自己花钱组装了一台式机，同时学习&nbsp;HTML、CSS、JS&nbsp;制作了自己的第一个网站，真正踏上了编程这条泥泞小道。截止目前（2023-12-28），还能在&nbsp;JSRun&nbsp;看见&nbsp;<a href="https://jsrun.net/fKqKp/show" target="_blank">2017&nbsp;年时的网站主页草稿</a>：</p><p><img src="/api/uploads/blog/u_1_658c8b2c0b513_3422x2126.webp" alt="u_1_658c8b2c0b513_3422x2126.webp" data-href="" style=""/></p><p>一直到&nbsp;2020&nbsp;年&nbsp;6&nbsp;月，才分期购了一台&nbsp;19&nbsp;年款&nbsp;Macbook&nbsp;Pro&nbsp;16’&nbsp;i7&nbsp;16G&nbsp;笔记本。得益于移动式办公的便利性，我在这&nbsp;3&nbsp;年里承接了来自于个人、企业乃至政府的&nbsp;<a href="https://about.gxzv.com/resume" target="_blank">20&nbsp;余个项目</a>。</p><p>23&nbsp;年末新&nbsp;Macbook&nbsp;发布，在体验到&nbsp;MBP&nbsp;对开发带来的效率提升，复杂项目或多个项目并行开发导致的内存不足，以及本地跑&nbsp;LLMs&nbsp;的性价比后，便下单了&nbsp;M3&nbsp;Max&nbsp;128G&nbsp;的顶配&nbsp;MBP。</p><p><img src="/api/uploads/blog/u_1_658c8b385c961_1062x1416.jpeg" alt="u_1_658c8b385c961_1062x1416.jpeg" data-href="" style=""/></p><p><img src="/api/uploads/blog/u_1_658c8b3ea3bdb_1904x1428.jpeg" alt="u_1_658c8b3ea3bdb_1904x1428.jpeg" data-href="" style=""/></p><p><img src="/api/uploads/blog/u_1_658c8b45b60f9_1008x1344.jpeg" alt="u_1_658c8b45b60f9_1008x1344.jpeg" data-href="" style=""/></p><p>在购买新笔记本前，最让我纠结的莫过于&nbsp;14&nbsp;与&nbsp;16&nbsp;寸之间的抉择。按照性能和观感来说，16&nbsp;寸无疑是最佳解决方案。</p><p>但我是一个喜欢旅游、经常背着笔记本四处跑的人，前&nbsp;3&nbsp;年中常常因为&nbsp;16&nbsp;寸的重量而感到不适。不过除了重量之外，又几乎找不出其他任何缺点。</p><p><img src="/api/uploads/blog/u_1_658c8b52b9279_1024x1365.jpeg" alt="u_1_658c8b52b9279_1024x1365.jpeg" data-href="" style=""/></p><p>16&nbsp;寸对于我来说更加美观、电池续航更长、对外接屏幕依赖更小、有更好的音效...&nbsp;当然最关键的是，16&nbsp;寸对编程更为友好。</p><p><img src="/api/uploads/blog/u_1_658c8b5bdda6f_1728x1117.jpeg" alt="u_1_658c8b5bdda6f_1728x1117.jpeg" data-href="" style=""/></p><p>因为在日常开发中，我通常会为每个项目单独分配一个桌面，每个桌面包括各种代码编辑器、服务终端、代码终端、浏览器或模拟器等渲染端。借助&nbsp;16&nbsp;寸的屏幕空间，我可以在一个桌面中并行处理多个窗口，而&nbsp;14&nbsp;寸需要窗口全屏来获取最佳体验。</p><p>至此，另附两张平日开发中轻度使用的&nbsp;CPU&nbsp;及内存占用情况：</p><p><img src="/api/uploads/blog/u_1_658c8b67da8cf_1115x912.webp" alt="u_1_658c8b67da8cf_1115x912.webp" data-href="" style=""/></p><p><img src="/api/uploads/blog/u_1_658c8b6d76c53_1115x912.webp" alt="u_1_658c8b6d76c53_1115x912.webp" data-href="" style=""/></p><h2>后续使用</h2><p>首先是键盘膜。之前的电脑由于裸机使用，长年累月下键盘被磨得油光满面，不太喜欢这种观感故购置了一款极川的键盘薄膜。关于键盘膜会在屏幕上留印的问题倒是不用太在意，因为之前我电脑没用键盘膜时也有这个情况。</p><p>其次是&nbsp;AppleCare。说实话对于电脑这种设备一般来说是很难用到保修服务的，且用到时也有诸种不便。例如良莠不齐的经销商合作点、人满为患的直营店、动不动长达半月的返厂维修、以及官方维修不保数据的一大痛点等。而且在直营店中，店员推销该服务的方法也令人反感。因为在&nbsp;Mac&nbsp;系统账户中，AppleCare&nbsp;是可以按年续费或&nbsp;3&nbsp;年购买的，但直营店中只会给&nbsp;3&nbsp;年购买的选项，且试图各种说服你而不变更方案。</p><p>所以我一怒之下怒了一下，便放弃购买&nbsp;AppleCare，即便万一出了问题，也可以找信得过的师傅进行维修。自己拿着电脑当天去当天回，数据也能尽量得到保障。</p><p>另外就是升降桌。我主观认为这是比人体工学椅更重要的东西，通过升降桌可以轻松实现站坐切换，改善血液循环，缓解肌肉紧张，同时为我增加活力和生产力。</p><p><img src="/api/uploads/blog/u_1_658d2ea0189be_1008x1344.jpeg" alt="u_1_658d2ea0189be_1008x1344.jpeg" data-href="" style=""/></p>]]></content><category term="电子产品" scheme="https://gxzv.com/blog/tag/21" /></entry><entry><title>moments before Oct. 2023</title><link href="https://gxzv.com/blog/moments-before-oct-2023/" /><id>https://gxzv.com/blog/moments-before-oct-2023/</id><published>2023-10-05T22:21:42.000Z</published><updated>2023-10-05T22:52:54.000Z</updated><summary type="html">新故相推，日生不滞。
距离上次复盘博客已过半年有余，虽平时也会花大量时间发散思维，但那些内容就如风忽起，过无痕。拖到现在，原因无非有三：
随着经历和认知的累积更加内敛，自觉之前的内容更多是自嗨式的观点宣泄。随着技术迭代，重构网站的念头愈发强盛，便想着等某天用新技术重建完再基于此撰写内容。许多有趣的东...</summary><content type="html"><![CDATA[<p>新故相推，日生不滞。</p><p>距离上次复盘博客已过半年有余，虽平时也会花大量时间发散思维，但那些内容就如风忽起，过无痕。拖到现在，原因无非有三：</p><ol><li>随着经历和认知的累积更加内敛，自觉之前的内容更多是自嗨式的观点宣泄。</li><li>随着技术迭代，重构网站的念头愈发强盛，便想着等某天用新技术重建完再基于此撰写内容。</li><li>许多有趣的东西并不能在当下的网络环境中公布。</li></ol><p>除此，长期有多个进行中的项目积压在头顶，让本就缺乏时间管理能力的我在完美主义潜移默化的影响下进一步扼杀了效率，这般拖延难免愈发严重。</p><h2>伊始</h2><p>国庆居家，恰逢闲时，想着这般下去也不是办法。</p><p>索性将其他抛之脑后，思绪回溯，开始复盘今年种种。</p><p>挑个距上次更新最近的来说，也就是&nbsp;126&nbsp;天前发布的文章《<a href="/blog/SYB-thinking-and-action/" target="_blank">关于决定首个创业项目的思考和行动</a>》。坦白说，该项目的进度比我想象中慢了许多，和拖更的博客一般，原因有二：</p><p><strong>其一，是我对这个项目期望太高，背离了&nbsp;Minimum&nbsp;Viable&nbsp;Product&nbsp;这一理念。</strong></p><p>从前期准备上，在开发前我便购买了人格心理学及九型人格这两大领域下，多位作家的相关实体书籍与资料，收集途径从海外代购到&nbsp;Z-lib，可谓是想着学李白也学杜甫，以此来尽量对冲信息差；从开发架构上，我将该项目定位为贴合时代趋势的国际项目，通过大型语言模型和&nbsp;Web3&nbsp;赋能，借助&nbsp;Cloudflare&nbsp;Workers&nbsp;/&nbsp;Pages&nbsp;与微前端进行快速部署和开发，使用&nbsp;Webman&nbsp;在后端集中管理。</p><p>在一开始，我便想着借助上一款产品累计的反馈闭门造车打造一款「完美」的产品，待发布后便会借由冷峻的小温暖迅速传播，获得「成功」。瞧瞧，这不就是所谓的「EGO&nbsp;IS&nbsp;THE&nbsp;ENEMY」，自我总是把自己想得比实际情况更「厉害」。</p><p><strong>其二，是连续杂多的事影响着长期专注。</strong></p><p>自六月立项后，七月初刚放暑假便踏上了前往广州的廊桥，围绕工业物联网开始为期一月的软件开发、平台架构和团队协作。与此同时，身处在广州天河区川流不息的人群与车流，感受着城中村和高端商圈仅一街之隔的落差，走过岗顶天桥，穿过石牌小巷，又常常因自身性格，冒出一种超脱红尘的心境。每至深夜，种种思绪便如吉光片羽般涌现。</p><p><img src="/api/uploads/blog/u_1_651c6efccfd3b_1536x2048.jpeg" alt="石牌村逢源大街" data-href="" style=""/></p><p><br></p><h3>最小可行性产品</h3><p>让我们先谈谈什么是&nbsp;MVP（最小可行性产品），该理念提出者&nbsp;Eric&nbsp;Ries&nbsp;在其作品这样诠释道：MVP&nbsp;指的是企业用最小的成本开发出可用且能表达出核心理念的产品版本，使其功能极简但能够帮助企业快速验证对产品的构思，以便于企业在获取用户反馈后持续迭代优化产品、不断适应市场环境。</p><p>其中的关键字莫过于最小成本与核心功能。</p><p>在最小化人力、资金和时间等成本的同时满足核心功能，就意味着牺牲产品易用性与完整性，甚至不像一个产品，特别是在互联网领域。在这个领域，很多成功的商业产品最初都只是一个工具，或是一段脚本，别无选择的用户是它们成功的关键。随着这部分刚需用户不断提供最直接、最有效的反馈，帮助开发者不断优化产品，脚本迭代为工具，工具完善为产品。在这般群体裹挟下，即便依靠最简单的商业模式，也能轻易获得成功。</p><p>举个例子，Dropbox&nbsp;的创始人&nbsp;Drew&nbsp;Houston&nbsp;最初只是制作了一个简单的视频，向人们展示了一个基本的文件同步服务。这个视频并没有实际的产品，只是一个概念验证。但是，这个视频在网络上引起了极大的关注，很多人表示他们愿意购买这样的服务。这就验证了&nbsp;Dropbox&nbsp;的核心理念——用户需要一个简单易用的文件同步服务。</p><p>在收到这个积极反馈后，Dropbox&nbsp;团队开始全力开发产品，并在短时间内发布了第一个版本。虽然功能非常基础，但是它满足了用户最核心的需求，因此迅速获得了大量用户。随后，Dropbox&nbsp;团队根据用户反馈进行多次迭代，逐步增加了更多功能，例如共享文件夹、在线协作等。</p><p>Dropbox&nbsp;是&nbsp;MVP&nbsp;策略的典型应用，但如果我们考虑另一个结果：X&nbsp;团队也看见了那个视频，且由于其技术壁垒不高，X&nbsp;团队更快地实现产品并将其推向市场，赢得了更多市场份额。</p><p>当然这并非否定&nbsp;MVP&nbsp;策略，而是需要意识到风险一直存在，只有不断提高产品的「不可替代性」、专注于产品的核心价值、提供卓越的用户体验、持续创新、构建品牌，以及建立生态与合作，才能更好应对市场变化和竞争挑战。</p><p><br></p><h2>假前生活碎片</h2><p><img src="/api/uploads/blog/u_1_651d5bbd9b62e_1024x1148.jpeg" alt="u_1_651d5bbd9b62e_1024x1148.jpeg" data-href="" style=""/></p><p><img src="/api/uploads/blog/u_1_651d5bca360f6_1024x1149.jpeg" alt="u_1_651d5bca360f6_1024x1149.jpeg" data-href="" style=""/></p><p><img src="/api/uploads/blog/u_1_651d5c30aa0b2_1024x1146.jpeg" alt="u_1_651d5c30aa0b2_1024x1146.jpeg" data-href="" style=""/></p><p><img src="/api/uploads/blog/u_1_651d5bdf185f1_1024x1153.jpeg" alt="u_1_651d5bdf185f1_1024x1153.jpeg" data-href="" style=""/></p><p><img src="/api/uploads/blog/u_1_651d5beea98cf_1024x1156.jpeg" alt="u_1_651d5beea98cf_1024x1156.jpeg" data-href="" style=""/></p><p><img src="/api/uploads/blog/u_1_651d5bf588c8e_1427x1600.jpeg" alt="u_1_651d5bf588c8e_1427x1600.jpeg" data-href="" style=""/></p><h2>in&nbsp;广州</h2><p>7月4日周二下午，想着要在广州待一月或两月，便拎着俩行李箱出发，一箱衣物一箱书。</p><p><img src="/api/uploads/blog/u_1_651d72752dd64_1080x1440.jpeg" alt="u_1_651d72752dd64_1080x1440.jpeg" data-href="" style=""/></p><p><img src="/api/uploads/blog/u_1_651d73a867245_1080x1440.jpeg" alt="u_1_651d73a867245_1080x1440.jpeg" data-href="" style=""/></p><p>再落地时已是巳时，驱车近一时到达天河，行李放在他们准备好的公寓后，和&nbsp;boss（后称P叔）&nbsp;在附近对粥长谈了许久。因为在这边主要负责工业物联网方向，也同时被科普了许多硬件领域的知识，有些碎片，有些痛苦。</p><p>第二天正午，醒后在附近吃个便饭和P叔一起去了公司。虽然公寓环境有些艰苦，但好在从公寓去公司只有五分钟路程。为了适应新的工作环境，前几天便打算收尾之前纯软方面的工作，例如&nbsp;<a href="https://chat.metauit.com/" target="_blank">Metor&nbsp;Chat</a>。</p><p><img src="/api/uploads/blog/u_1_651d7c11acbcf_1440x1920.jpeg" alt="u_1_651d7c11acbcf_1440x1920.jpeg" data-href="" style=""/></p><p>在第二版中，重构了前后端代码。后端借助&nbsp;Cloudflare&nbsp;Workers&nbsp;全球边缘容器也就是&nbsp;Serverless&nbsp;架构来支撑海量并发请求，使用&nbsp;Websocket&nbsp;技术替代&nbsp;Server&nbsp;Sent&nbsp;Events，并另增了如Claude、PaLM&nbsp;2、Llama、Vicuna、通义千问等大模型。在前端，重写了&nbsp;Markdown&nbsp;渲染部分，新增了对公示和图表的渲染。</p><p><img src="/api/uploads/blog/u_1_651d7c1aaf8ac_1440x1920.jpeg" alt="u_1_651d7c1aaf8ac_1440x1920.jpeg" data-href="" style=""/></p><p><img src="/api/uploads/blog/u_1_651e8fad82515_1366x1484.jpeg" alt="u_1_651e8fad82515_1366x1484.jpeg" data-href="" style=""/></p><p>当晚，便和P叔一同在附近吃了晚饭，回程路过天桥时随手拍下一角。对新环境尚存的新鲜感并没有让我意识到，在广州天河市中心这般环境待上一段时间会对人造成哪些影响。</p><p><img src="/api/uploads/blog/u_1_651d8b119b5d3_1080x1440.jpeg" alt="u_1_651d8b119b5d3_1080x1440.jpeg" data-href="" style=""/></p><p>在剩下的时间里，作息大多和今天的相差无几，中午睡醒去公司，晚上九点左右一起结束工作，吃饭聊聊天，复复盘。这样日复一日，很快就到了周五，公司团建便定在了附近的一家川菜馆。</p><p><img src="/api/uploads/blog/u_1_651e70fcbd2f4_1080x1440.jpeg" alt="u_1_651e70fcbd2f4_1080x1440.jpeg" data-href="" style=""/></p><p>只能说为了适应广东的口味，做出了极大牺牲。</p><p style="text-align: center;"><span style="color: rgb(89, 89, 89);"><s>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</s></span></p><p>周六，独自去看了广州塔。</p><p><img src="/api/uploads/blog/u_1_651e75cf1b9dc_1440x1920.jpeg" alt="u_1_651e75cf1b9dc_1440x1920.jpeg" data-href="" style=""/></p><p>无意间得知广州塔的灯光都是P叔作为总工程师承建的，其中他说的一句话我至今也蛮有印象：</p><blockquote>曾经有个国家叫，南斯拉夫，拍了一部老电影「桥」，电影最后造桥的工程师说：“这是个传统，工程师完成他的工作以后，就再不回去欣赏它了。”</blockquote><p><img src="/api/uploads/blog/u_1_651e764135027_1440x1920.jpeg" alt="u_1_651e764135027_1440x1920.jpeg" data-href="" style=""/></p><p>此外，秉承其口中「广州人不上广州塔」的理念，我就只在一楼买了些纪念品，有两款木质小夜灯就蛮好看。再之后，便步行前往就近一名为四季天地的商圈，在那儿的一家&nbsp;Live&nbsp;House&nbsp;解决了餐食。相比这里的人声鼎沸，还是更喜欢待在静谧的大自然里。</p><p><img src="/api/uploads/blog/u_1_651d9593e34da_1920x1440.jpeg" alt="u_1_651d9593e34da_1920x1440.jpeg" data-href="" style=""/></p><p>吃完饭歇会儿，顺手在商场买了些东西，便打车回住处。结果刚到楼层准备开门，就瞥见天花板有几只倒像苍蝇倒像蜜蜂的飞虫，饶是从小在永川农村生活也没见过这种生物。我心中一紧，拧锁窜进门后立马关门，一气呵成，刚准备狠狠松口气，一开灯便发现地上竟然还趴着一只。</p><p>总之，过程很悲壮，我甚至下楼去买了杀虫药，又在门口站了半小时。</p><p>门口罚站的同时，在思考办法，也思考人生。</p><p style="text-align: center;"><span style="color: rgb(89, 89, 89);"><s>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</s></span></p><p>隔天周日，没什么想去的地方便在四周逛逛。</p><p><img src="/api/uploads/blog/u_1_651e871313d17_1080x1440.jpeg" alt="u_1_651e871313d17_1080x1440.jpeg" data-href="" style=""/></p><p><img src="/api/uploads/blog/u_1_651e87df8c6e3_1080x1440.jpeg" alt="u_1_651e87df8c6e3_1080x1440.jpeg" data-href="" style=""/></p><p><img src="/api/uploads/blog/u_1_651e871a6dafc_1080x1440.jpeg" alt="u_1_651e871a6dafc_1080x1440.jpeg" data-href="" style=""/></p><p><img src="/api/uploads/blog/u_1_651e8803b486a_1080x1440.jpeg" alt="u_1_651e8803b486a_1080x1440.jpeg" data-href="" style=""/></p><p>如果在这个时候，你问我广州是什么，我应该会这样回答：</p><blockquote>是高人口密度的城中村，是大量的缺失性基础需求，是百米开外的核心&nbsp;CBD，是大写的消费符号，是无形压在身上的快节奏，是零点的凉茶店，凌晨三点的糖水铺，是永不中断的车流。</blockquote><p>当晚，简单买了些用品就回公寓看书。那段时间也很喜欢看&nbsp;TED&nbsp;TALK，如果说个体的商业模式分为能力、效率以及杠杆，其中杠杆又分为能力（团队）、产品、资本和影响力杠杆，那么演讲和写作无非是最能撬动人心的杠杆。</p><p><img src="/api/uploads/blog/u_1_651ecc1444d46_1440x1920.jpeg" alt="u_1_651ecc1444d46_1440x1920.jpeg" data-href="" style=""/></p><p style="text-align: center;"><span style="color: rgb(89, 89, 89);"><s>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</s></span></p><p>第二周，开始联调以及编写上位机。</p><p><img src="/api/uploads/blog/u_1_651e89d5b65f0_1080x1440.jpeg" alt="u_1_651e89d5b65f0_1080x1440.jpeg" data-href="" style=""/></p><p>出差见供应商的路上还碰见了一堆警察在核查市外车牌全车人员身份证，以及人脸识别结果，不知是不是在抓逃犯。</p><p><img src="/api/uploads/blog/u_1_651e8a95145e9_1440x1920.jpeg" alt="u_1_651e8a95145e9_1440x1920.jpeg" data-href="" style=""/></p><p>供应商似乎是乐鑫国内最大的代理商，我们过去看了下实物，另外帮我们的嵌入式工程师了解到&nbsp;GUI&nbsp;Guider&nbsp;这款软件，提高了一些开发效率。</p><p>实际上，这款软件于&nbsp;21&nbsp;年初就已经在阿莫电子论坛流行了，在&nbsp;IT&nbsp;这个日新月异的行业，在不完全信息决策中扩充可选项与客观地评估价值，时刻关注行业动态与吸收新知识显得尤为重要。而总是对新事物抱有怀疑与排斥，总是沿着旧地图，是难以找到新大陆的。</p><p>话说回来，他们「可插拔」的产品箱确实蛮方便：</p><p><img src="/api/uploads/blog/u_1_651e8f7909b68_1440x1080.jpeg" alt="u_1_651e8f7909b68_1440x1080.jpeg" data-href="" style=""/></p><p style="text-align: center;"><span style="color: rgb(89, 89, 89);"><s>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</s></span></p><p>第三周周六，出发去江门和家里人聚了聚。在周日午饭后，幺爸突然问我要不要带着堂弟也就是他儿子一起回广州，我估摸是句玩笑话便应了下来。结果不曾想堂弟思考半天竟也答应下来，一旁的公公估计受够了小孩每天在眼前调皮，见状直接招呼着拿身份证和换洗衣物，生米成熟饭。</p><p><img src="/api/uploads/blog/u_1_651e9886ca4c9_1440x1920.jpeg" alt="u_1_651e9886ca4c9_1440x1920.jpeg" data-href="" style=""/></p><p>隔天便开始带娃上岗。</p><p><img src="/api/uploads/blog/u_1_651e9a53eb12c_1080x1440.jpeg" alt="u_1_651e9a53eb12c_1080x1440.jpeg" data-href="" style=""/></p><p>中午开始写作业，晚上吃完饭逛一圈回公寓玩玩手机，完全没有一点调皮的样子，甚是欣慰。</p><p><img src="/api/uploads/blog/u_1_651e9a594b8a8_1440x1920.jpeg" alt="u_1_651e9a594b8a8_1440x1920.jpeg" data-href="" style=""/></p><p>从另一面来说，也算是有了个饭搭子。</p><p><img src="/api/uploads/blog/u_1_651e9b21a8c3c_1440x1440.jpeg" alt="u_1_651e9b21a8c3c_1440x1440.jpeg" data-href="" style=""/></p><p style="text-align: center;"><span style="color: rgb(89, 89, 89);"><s>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</s></span></p><p>后面的日子，由于项目工期紧张，我也即将离开，就想着给这边招些人手负责软件开发，便在猎聘发布了大前端开发工程师的职位，薪资8~13K，学历不限，要求如下。</p><blockquote>职责描述：<br>1.&nbsp;负责网页、桌面、移动APP、小程序等的前端开发和静态页面制作；<br>2.&nbsp;配合&nbsp;UI&nbsp;设计进行前端界面维护与优化，提升用户体验；<br>3.&nbsp;配合后端开发进行前后端对接开发与联调；<br>4.&nbsp;负责大前端框架产品的技术研究及升级迭代；<br>5.&nbsp;参与公司项目需求评审，编写产品技术文档、验收文档等；<br>6.&nbsp;负责公司其他重点技术项目的推进与落实；<br>7.&nbsp;协助并服从公司领导安排完成其他各项工作任务；<br><br>任职要求：<br>1.&nbsp;熟练使用各种&nbsp;Web&nbsp;前端技术，精通&nbsp;JavaScript&nbsp;编程语言，能使用&nbsp;TypeScript。其中技术栈为&nbsp;Svelte&nbsp;或&nbsp;React&nbsp;为佳，能从&nbsp;0&nbsp;到&nbsp;1&nbsp;构建一个项目为佳；<br>2.&nbsp;能使用&nbsp;Electron、ReactNative、UniApp&nbsp;完成前端跨平台开发。会熟练使用&nbsp;ReactNative&nbsp;或&nbsp;Flutter&nbsp;编译原生程序为佳；<br>3.&nbsp;熟悉&nbsp;Node.js&nbsp;的核心机制、底层原理和开发框架。能进行服务端开发，了解通讯协议并掌握各系统模块的使用为佳；接触或开发过物联网项目为佳；<br>4.&nbsp;熟练掌握&nbsp;HTTP&nbsp;协议、Web&nbsp;开发技术和相关工具链。能使用&nbsp;MySQL&nbsp;及&nbsp;Redis&nbsp;数据库为佳；<br>5.&nbsp;具备良好的沟通能力和合作精神，能够协作完成多人项目；</blockquote><p>实际上在我去广州之前，P叔已经给我发过接近三位数的简历，但符合我心中要求的实在太少，加上当时用人需求并不显著，便一直没舍得退而求其次。然而事到如今，也只有硬着头皮上了。简历抛开乱投的数量，差不多是十份里看得过去一两份，线上聊五个可能最后面试只有一两个，需知这还是降低要求后的结果。</p><p style="text-align: center;"><span style="color: rgb(89, 89, 89);"><s>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</s></span></p><p>八月四日，下午在公司开完会，我便带着堂弟和P叔赶往机场，前者返渝，后者赴沪。</p><p>在这一个月里，曾多次心境起伏。有时，在特定心境下，一些小事竟能带给我近乎人生起落般的感悟。从而在身处的小社会中意识到一些问题，看清一些事。其中最重要的一点，莫过于允许发生：容许一切发生的释然是对自己及他人的宽容。</p><p style="text-align: center;"><span style="color: rgb(89, 89, 89);"><s>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</s></span></p><p>回忆模糊，但心中的方向却越显凝实。</p><p><img src="/api/uploads/blog/u_1_651ec0022d632_1920x2560.jpeg" alt="u_1_651ec0022d632_1920x2560.jpeg" data-href="" style=""/></p>]]></content><category term="重要的" scheme="https://gxzv.com/blog/tag/2" /><category term="社会" scheme="https://gxzv.com/blog/tag/6" /><category term="软件架构" scheme="https://gxzv.com/blog/tag/7" /><category term="生活" scheme="https://gxzv.com/blog/tag/16" /><category term="创业" scheme="https://gxzv.com/blog/tag/20" /></entry><entry><title>关于决定首个创业项目的思考和行动</title><link href="https://gxzv.com/blog/SYB-thinking-and-action/" /><id>https://gxzv.com/blog/SYB-thinking-and-action/</id><published>2023-06-01T18:41:02.000Z</published><updated>2023-07-19T08:55:29.000Z</updated><summary type="html">自我的形状是在和世界的碰撞中形成的。
由于最近身处学业、工作和向外社交这些纵横交错的关系中，我开始很少有机会花 80% 以上的时间在自己的思考上，而零碎的时间也几乎不可能让我深入且清晰的自省。日积月累，就像一层黏糊糊的薄膜附在身上，浑身都不自在。
在自我认知首次于初中的青葱懵懂中诞生时...</summary><content type="html"><![CDATA[<blockquote>自我的形状是在和世界的碰撞中形成的。</blockquote><p>由于最近身处学业、工作和向外社交这些纵横交错的关系中，我开始很少有机会花&nbsp;80%&nbsp;以上的时间在自己的思考上，而零碎的时间也几乎不可能让我深入且清晰的自省。日积月累，就像一层黏糊糊的薄膜附在身上，浑身都不自在。</p><p>在自我认知首次于初中的青葱懵懂中诞生时，我用星座这类飘忽的标签来形容自己：<strong>#水瓶座&nbsp;#要强&nbsp;#自由&nbsp;#创造&nbsp;#矛盾&nbsp;#博爱但疏离&nbsp;#理性与感性&nbsp;#自我不自私</strong>，以此等等。但这些就如标签效应一般：像我但不是我。</p><p>而在这之前，我对自己是个怎样的人毫无见解，似乎除了性别、年龄和民族等这些客观事实，我几乎无法向人描述自己。我不知道我的情绪为什么会在某一刻跌宕；不知道为什么我的行为会如此矛盾；不知道喜欢和讨厌的背后指代的什么。</p><p>之后的几年，互联网上充斥的各类信息非但没能让我正确看待自己，反而变得愈加迷惘。在认清本质接受现实的这一过程中，我们总是会在潜意识里模糊现实，不愿接受其残酷与不确定性，也无法正视其可能性与机遇。眼睛能看见一切，唯独看不见自己，这种种问题也间接表明：通过自省来了解自己本就是不太现实的。</p><p>其中也有一种声音是这样说的：</p><blockquote style="text-align: center;">当你希望知道自己是怎样一个人的时候，当你迫切需要给自己定性的时候，那就意味着你正在逃避现实的不确定。<br><br>你要接受现实的本质与不确定，这样你才能认清生活的真相。<br><br>你要接受自身的不确定和无法被定义，这样你才不会给自己设限，你才能意识到你的确有着无数可能性，你才能挣脱目前这短暂的障碍与问题的束缚看清事实的全貌与自己本身。<br><br>《成为自己的勇气》</blockquote><p>说实话，我是下意识带着质疑甚至反对的主观去看待这段话的，就像我们总是下意识地拒绝承认事实真相：<strong>对于不好的事实，人们的第一反应是反驳抗拒，因为这些信息会与大脑中的信念相抵触，人类本能地会对不利消息产生抵触。</strong></p><p><strong>正在逃避现实的不确定</strong>，就如我前些年经常思考人生的意义："自己对目前用处甚微的理论知识完全提不起兴趣，说白了就是读书这条按部就班的路走不下去，未来我应该如何用技能在社会立足？"&nbsp;但客观地说，绝大多数情况是在遇到困难挫折时才会产生诸如此类的问题，以至于有些虚无主义。但这又和上述有所偏驳，因为这是较为理性的内向化思维，它在一定程度上对外在的客观现实有所兼顾。若非如此，我想的就应该是："我讨厌上学，读死书到底有什么意思，为什么那些同学能学得进去？是我出什么毛病吗？在这种被约束的环境下好焦虑，我到底该怎么办？"</p><p>后者只是被负面情绪困扰，不去想负面情绪产生的原因，忽略现实去思考所谓的解决方案。而最致命的，则是随着时代整体价值观向个人主义倾斜，以及随着心理学知识科普，并且这些单一理论被潜意识认为可以解释所有问题（没有意识到适用范围），后者的数量只会越来越多。正如书中所说：</p><blockquote>喜欢探究自身的心理、人格、情绪等，当然没问题。但问题在于，不能只用一套理论就试图解释自己生活中遇到的所有问题。此外，心理学和其他学科一样，是需要专业门槛的，但很多人根本没有意识到这个「门槛」。因此，「民科心理学」大行其道，再加上「巴纳姆效应」的影响，人们特别热衷于对照各种心理测试和文章进行自我心理测试，然后惊叹道："对！对！对！我就是这样的！"<br><br>为了迎合人们的这种需求，各类心理学相关媒体便会广泛地发那些能够迎合大众心理的理论和文章。这一切都会在不知不觉中加剧很多人「内向化」的思维模式，甚至使其完全与现实背道而驰。<br><br>这种完全「内向化」的思维方式，恰恰是由于「过度自我关注」造成的。<br><br>所谓过度自我关注，就是一个人过度地关注自身，过度地关注自己会得到什么、失去什么、成为什么。这种关注持续的时间长了，个体会被自身的种种欲望所裹挟，然后变得格外敏感。</blockquote><p><strong>红尘世事，刚者易折。天下柔弱者莫如水，然上善若水。</strong></p><p>在这件事上也是如此，偏执地在一条路上走到尽头几乎是不可取的。除开上述的情况，了解自己依然是非常重要的一件事，特别是在迷惘，或是有种一切都还不错但就是不太开心的感觉时。通过了解自我本能、动机、需要、恐惧等，发现自己融入世界的方式，以及它们是如何影响自己。当你结合这类平时日常生活中难以发现的信息去思考问题，会惊讶地发现头顶密布的乌云已悄然消散。</p><h2>契机</h2><p>从高中装抑郁逃课的明尼苏达多相人格测试（MMPI）开始，好奇驱使我接触了大量的专业心理测试量表，甚至到了带着机翻生啃英文文献的地步。其中最让我怦然心动的，无疑是九型人格（Enneagram）。其把生涩难懂的理论知识映射为了九个人格类型：每种类型都有自己的动机、恐惧和驱动力，以特定的情感为中心。实际运用中，每个人都是两个及以上类型的混合体，甚至可以衍生出数百上千种不同排列组合且动态变化，但这都不会改变其基本人格特质。</p><p>在我看来，它不会束缚人们，而是打开了一条通往自我潜能发掘的捷径。这也是九型人格与其他商业娱乐型巴纳姆效应测试不同的一点，九型是一个能真正帮助人们探索、发展、武装自己的人格系统。</p><p>2022&nbsp;年年初，当时已有近百年历史的&nbsp;MBTI&nbsp;测试突然大火，这让我有了在自己个人网站开发一个九型人格测试项目的打算。正好那段时间空闲较多，这个项目便于&nbsp;4&nbsp;月初着手开发，并于&nbsp;19&nbsp;日正式上线。为了推广也尝试投了&nbsp;Google&nbsp;Ads，期待让更多的人了解他们自己，而后了解作者，妄图以此寻找共鸣及认可。</p><p><img src="https://gxzv.com/api/uploads/blog/u_1_6478754be3c36_1920x1080.jpeg" alt="u_1_6478754be3c36_1920x1080.jpeg" data-href="" style=""/></p><p>作为一个临时起意开发的项目，截止至今已稳定运行了一年零一个月有余。由于优异的操作体验感和分析专业性，上线以来获得了绝大多数用户的好评以至盛赞，但其中零星的声音无时无刻不在提醒我那些我刻意回避的问题：项目仍有不少提升空间，分析报告仍然不够深入，模式单一等等。而让我回避这些问题的便是我手中堆积的其他事情，我无法下定决心去牺牲甚至放弃其他项目来完善这个九型测试。噢，还有另一个原因，就是目前它运作的也还算不错。</p><p>本以为它就会这样平平淡淡一直走下去，但未曾想到随着相关知识的提升，以及和世界的碰撞中使自我形状更具象化后，我心里开始出现一个声音：<strong>目前这也许是你一直以来所寻找的心理学领域和人文关怀方向下最合适的创业项目。</strong></p><h2>筹备</h2><p>为了避免再做思维巨人行动侏儒，我首先借助&nbsp;AI&nbsp;为项目起了个名字，并经过域名和商标查重后，最终敲定为"人格九道（EnneaTao）"。</p><p><img src="https://gxzv.com/api/uploads/blog/u_1_64786aac2ebc4_1037x1280.jpeg" alt="u_1_64786aac2ebc4_1037x1280.jpeg" data-href="" style=""/></p><p>接下来便轻车熟路注册了域名，商标，以及进行&nbsp;ICP&nbsp;备案，并顺手做了个简单的&nbsp;Logo。</p><p><img src="https://gxzv.com/api/uploads/blog/u_1_64814135d4561_1728x1080.jpeg" alt="u_1_64814135d4561_1728x1080.jpeg" data-href="" style=""/></p><p><img src="https://gxzv.com/api/uploads/blog/u_1_648bb33c39e0f_3066x2082.jpeg" alt="u_1_648bb33c39e0f_3066x2082.jpeg" data-href="" style=""/></p><p>其中，在阿里云进行&nbsp;ICP&nbsp;备案时，不知道是不是隔了太久没使用备案服务，新申请时还需进行一次主体变更。而更离奇的是阿里云备案初审员致电时，竟告诉我说最近有政策新规，已备案网站超过&nbsp;5&nbsp;个需要提交每个备案网站的情况说明书和建设方案书，也就是说我还要提交二十多份报告。这让我震惊且不解，便去看了眼重庆管局的备案规则：<a href="https://help.aliyun.com/document_detail/50259.htm" target="_blank">https://help.aliyun.com/document_detail/50259.htm</a>。</p><p>在规则文档中，企业和单位主体的规则分明写道：域名前缀超过30个时（含30个），需提供每个域名的网站建设方案书。所以我推测是审核员搞错了，便取消申请重新提交，遂顺利通过。</p><h2>分析规划</h2><p>这里我直接问的&nbsp;Metor-370（效果等同&nbsp;GPT-4），对话如下：</p><p><img src="https://gxzv.com/api/uploads/blog/u_1_647870df597f9_1284x4309.jpeg" alt="u_1_647870df597f9_1284x4309.jpeg" data-href="" style=""/></p><p><img src="https://gxzv.com/api/uploads/blog/u_1_647870e70ddbb_1284x6738.jpeg" alt="u_1_647870e70ddbb_1284x6738.jpeg" data-href="" style=""/></p><p>其中对于首句的项目情况介绍，完善后为：</p><blockquote>我想在心理学领域下的自我认知（个人提升）和人文关怀方向进行独立创业，现计划开发一个关于九型人格的网站平台，名为人格九道(EnneaTao)。该平台提供在线测试和结果分析报告，收入来自付费解锁完整版报告内容，以及可能的增值服务。<br><br>在这之前，我已在个人网站中开发过完整的九型人格测试系统，并于过去&nbsp;30&nbsp;天内提供了&nbsp;40,359&nbsp;人次测试，根据反馈统计满意度高达&nbsp;98%。***&nbsp;除去开发时间成本，首年投入回报比例为&nbsp;***%。其中新用户45%&nbsp;来自搜索引擎付费推广，30%&nbsp;社区推广，15%&nbsp;朋友推荐，5%&nbsp;同站流量，5%&nbsp;自然流量。<br><br>面向所有同类竞品，目前的优势有：<br>-&nbsp;独立开发者自主创业，趋近于0的开发成本<br>-&nbsp;先进的技术栈，趋近于0的运维成本<br>-&nbsp;前端H5自适应设计简雅，操作体验感最佳<br>-&nbsp;测试结果分析报告最详尽，最全面（无论是免费版还是付费版）<br><br>同时，还将在以下方向进行创新：<br>-&nbsp;使用区块链技术进行用户认证，确保用户资料的数据隐私和安全问题，增强用户信任。<br>-&nbsp;使用人工智能（基于&nbsp;prompt&nbsp;工程预训练的大型语言模型）以对话的方式帮助用户更好的了解自己。<br>-&nbsp;提供内容创作平台，用户可发表动态及文章并基于&nbsp;IPFS&nbsp;在链上储存，可参考：开源链上博客系统&nbsp;xLog</blockquote><p>其中高达&nbsp;***%&nbsp;的投入回报比可能有的朋友看见会很惊讶甚至怀疑，但需要说明的是，该产品从&nbsp;0&nbsp;到&nbsp;1&nbsp;均是我一个人完成的，故在回报比的计算中，省去了开发，运维以及人力成本这三大费用。</p><p>此外，我也在网上查阅了不少心理学领域创业的知识分享，其中包括<a href="https://www.zhihu.com/roundtable/xinlixue101" target="_blank">知乎圆桌&nbsp;-&nbsp;心理学&nbsp;101</a>、<a href="https://coffee.pmcaff.com/article/3228467518317696/pmcaff" target="_blank">PMCAFF&nbsp;-&nbsp;我的创业独白之——小鹿平价智能咨询心理平台商业计划书</a>等，但对我目前来说启迪不是太大。</p><p>匆匆提笔，等之后我再修缮与更新这条博客。</p><p>与诸君共勉。</p><p>未完待续。</p>]]></content><category term="人生第一次" scheme="https://gxzv.com/blog/tag/3" /><category term="软件架构" scheme="https://gxzv.com/blog/tag/7" /><category term="情绪" scheme="https://gxzv.com/blog/tag/15" /><category term="生活" scheme="https://gxzv.com/blog/tag/16" /><category term="创业" scheme="https://gxzv.com/blog/tag/20" /></entry><entry><title>SaaS 平台级架构: 统一身份管理系统</title><link href="https://gxzv.com/blog/saas-pa-uims/" /><id>https://gxzv.com/blog/saas-pa-uims/</id><published>2023-05-06T10:55:46.000Z</published><updated>2023-06-13T10:29:40.000Z</updated><summary type="html">在最近的项目中，需要开发一个能支撑大型 SaaS 生态链的平台，其中涉及到账户多设备管理、自有业务单点登录（SSO）、多主体入驻、开放平台下的第三方应用入驻及审批（OAuth2）、物联网平台下的项目及其设备接入、可插拔模块化应用开发的支持等等。
在之前，我也开发过类似的平台，但当时没有对大体架构及技...</summary><content type="html"><![CDATA[<p>在最近的项目中，需要开发一个能支撑大型&nbsp;SaaS&nbsp;生态链的平台，其中涉及到账户多设备管理、自有业务单点登录（SSO）、多主体入驻、开放平台下的第三方应用入驻及审批（OAuth2）、物联网平台下的项目及其设备接入、可插拔模块化应用开发的支持等等。</p><p>在之前，我也独立开发过类似的平台，但当时没有对大体架构及技术细节做深入了解就急着去敲代码实现，导致后期频频停下来陷入碎片化的思考。所以，在这次开发之前，我做足了功课，先撰写了流程文档，理顺后开始编码，一切水到渠成。现在该平台除了物联网和可插拔模块化应用开发，其他皆由我开发完毕，并写下本文章作为复盘。</p><h2>开发环境</h2><p>前端：Typescript&nbsp;5、Sveltekit&nbsp;1.5、TailwindCSS&nbsp;3.3、DaisyUI&nbsp;2.51</p><p>后端：PHP&nbsp;8.2、Webman&nbsp;1.5</p><p>数据库：Redis&nbsp;6、MySQL&nbsp;8</p><p><br></p><h3>接入的第三方</h3><p>微信扫码关注公众号登录，Odoo&nbsp;OAuth&nbsp;Provider</p><h2>账户及鉴权</h2><p>首先，是账户管理系统中最基础的注册和登录，这两者司空见惯不再多提。但在状态管理中，登录态的实现需做下强调，为了简化我们只思考登录流程，以及鉴权逻辑。</p><p>在早期的互联网应用中，最基本的身份验证方法是使用账号密码。用户只需要提供用户名和密码，系统就会核对数据库中的记录以验证用户身份。这种方法简单易用，但安全性较低，容易受到暴力破解、字典攻击等风险。<strong>[基于账号密码的认证]</strong></p><p>为了提高安全性，HTTP&nbsp;基本认证（Basic&nbsp;Authentication）和摘要认证（Digest&nbsp;Authentication）应运而生。HTTP&nbsp;基本认证通过将用户名和密码以&nbsp;Base64&nbsp;编码的形式传输，而摘要认证使用特定的哈希算法对密码进行加密，然后将加密后的摘要传输。这两种方法虽然在一定程度上提高了安全性，但仍然存在一些缺陷，例如信息在传输过程中容易被拦截。<strong>[HTTP&nbsp;基本认证和摘要认证]</strong></p><p>随着&nbsp;Web&nbsp;应用的普及，开发者需要一种在用户多次请求之间保持登录状态的方法。这时，Session&nbsp;和&nbsp;Cookie&nbsp;便出现了。Session&nbsp;是服务器端存储的一种数据结构，用于保存用户的登录信息；Cookie&nbsp;是一种客户端技术，将登录凭证存储在用户的浏览器中。这种方法在当时已经足够满足大部分应用场景，但随着网络攻击手段的不断演变，这种方法的安全性逐渐受到挑战。<strong>[Session&nbsp;和&nbsp;Cookie]</strong></p><p>同时为了解决信息在传输过程中的安全问题，SSL（Secure&nbsp;Sockets&nbsp;Layer）和后来的&nbsp;TLS（Transport&nbsp;Layer&nbsp;Security）诞生了。这些协议为客户端和服务器之间的通信提供了一种加密机制，保证了传输过程中的数据安全。随后，基于&nbsp;SSL/TLS&nbsp;的&nbsp;HTTPS（Hyper&nbsp;Text&nbsp;Transfer&nbsp;Protocol&nbsp;Secure）成为了&nbsp;Web&nbsp;应用的标准传输协议。<strong>[SSL/TLS&nbsp;加密传输]</strong></p><p>再往后，随着社交网络的兴起，OAuth&nbsp;协议应运而生，允许用户使用第三方平台（如&nbsp;QQ、GitHub、Google&nbsp;等）的账号登录其他应用。这样，用户无需为每个应用创建单独的账号密码，降低了记忆负担，同时提高了安全性。OAuth&nbsp;同时也促进了应用间的数据共享和协作。<strong>[OAuth&nbsp;和第三方登录]</strong></p><p>为了进一步提高账户安全性，多因素认证（MFA）开始出现。MFA&nbsp;结合了多种验证方式，如密码、硬件令牌、生物特征等，以确保只有经过多重验证的用户才能访问受保护的资源。MFA&nbsp;提高了安全性，使得攻击者在窃取某一验证因素时，仍无法轻易地获取完整访问权限。<strong>[MFA]</strong></p><p>为了更好地管理登录态并解决跨域问题，JSON&nbsp;Web&nbsp;Token（JWT）诞生了。JWT&nbsp;是一种紧凑、自包含的令牌，可用于在客户端和服务器之间安全地传输信息。与&nbsp;session&nbsp;和&nbsp;cookie&nbsp;相比，JWT&nbsp;可以在无状态的场景下维护用户登录态，提高了应用的可扩展性。<strong>[JWT]</strong></p><p>OpenID&nbsp;Connect（OIDC）&nbsp;是一种基于&nbsp;OAuth&nbsp;2.0&nbsp;的身份验证协议，提供了一种简单、标准化的方法来验证用户身份。通过&nbsp;OIDC，用户可以使用一个统一的身份提供者（如&nbsp;Google、Facebook&nbsp;等）登录多个应用，实现单点登录（SSO）。OIDC&nbsp;进一步简化了身份验证流程，提高了用户体验。<strong>[OIDC]</strong></p><p>这时，移动和&nbsp;Web&nbsp;应用开始大面积普及，登录态管理逐渐从&nbsp;session&nbsp;和&nbsp;cookie&nbsp;转向&nbsp;access_token&nbsp;和&nbsp;refresh_token。access_token&nbsp;是一种短期的、有时限的令牌，用于访问受保护的资源。refresh_token&nbsp;则是一种较长期的令牌，用于在&nbsp;access_token&nbsp;过期后请求新的&nbsp;access_token，以保持用户登录态。这种方法的优点在于，access_token&nbsp;的短时效性可以降低令牌被盗用的风险，而&nbsp;refresh_token&nbsp;的存在又能确保用户无需频繁地重新登录。<strong>[Access&nbsp;&&nbsp;Refresh&nbsp;Token]</strong></p><p>最后，截止&nbsp;2019&nbsp;年，为了进一步提高安全性和降低对密码的依赖，WebAuthn（Web&nbsp;Authentication）和&nbsp;FIDO2（Fast&nbsp;Identity&nbsp;Online&nbsp;2）标准应运而生。这些标准支持基于公钥密码学的身份验证方法，如生物特征、安全密钥等。用户无需记住复杂的密码，同时避免了密码泄露的风险。<strong>[WebAuthn&nbsp;和&nbsp;FIDO2]</strong></p><p><br></p><h3>具体实现</h3><p>在本平台中，我采取&nbsp;Access&nbsp;&&nbsp;Refresh&nbsp;Token&nbsp;来实现，为提高安全性，access_token&nbsp;的有效期只有&nbsp;15&nbsp;分钟，refresh_token&nbsp;有效期为&nbsp;7&nbsp;天。在浏览器中，refresh_token&nbsp;不作为&nbsp;JSON&nbsp;对象返回，而是通过&nbsp;httpOnly&nbsp;直接写入浏览器&nbsp;Cookie，同时推荐在支持&nbsp;SSL&nbsp;下同时加上&nbsp;Secure&nbsp;字段。具体流程如下：</p><ol><li>认证：用户通过账密、手机邮件验证码、手机二维码扫描的方式进行登录认证。</li><li>授权：通过认证后，后端签发&nbsp;access_token&nbsp;(JWT)&nbsp;及&nbsp;refresh_token&nbsp;(Hash)。</li><li>前端收到&nbsp;access_token&nbsp;后，将其本地储存，并在每次向后端发起请求时作为授权码（Header&nbsp;Authorization）携带。access_token&nbsp;过期后，携带&nbsp;Cookie&nbsp;请求刷新令牌接口，并将自身作为请求参数以供后端解析得到&nbsp;payload。</li><li>鉴权：后端从&nbsp;Header&nbsp;中提取&nbsp;access_token&nbsp;后，通过中间件借助&nbsp;JWT&nbsp;特性对其进行校验，并解析出&nbsp;payload&nbsp;供下一步使用。</li><li>权限控制：中间件借助&nbsp;payload&nbsp;中的权限字段，判定其是否有权限访问某一接口。</li><li>前端接收请求响应。</li></ol><p>在第三步需要注意，在某些条件的后端签发中，只对&nbsp;refresh_token&nbsp;做&nbsp;Cookie&nbsp;存储，access_token&nbsp;则需在前端本地存储。也就是说，过期令牌的更换逻辑需要由前端完成。同时这里也会有个坑，例如我们通过单例模式&nbsp;Api&nbsp;示例类进行请求调用：类中有&nbsp;request&nbsp;和&nbsp;getToken&nbsp;这两个函数方法，例如const&nbsp;response&nbsp;=&nbsp;Api.request(url,&nbsp;data);&nbsp;其中&nbsp;getToken&nbsp;是用于在&nbsp;request&nbsp;方法的请求发起之前，从&nbsp;localStorage&nbsp;获取用户的&nbsp;access_token，然后自动将&nbsp;token&nbsp;写入请求头中。当通过&nbsp;payload&nbsp;检测到&nbsp;access_token&nbsp;过期时，会访问后端通过&nbsp;refresh_token&nbsp;去刷新令牌，再返回新的有效令牌。但这里有个问题：如果在&nbsp;access_token&nbsp;过期时有多个请求，由于网络传输需要较长时间，会导致令牌被重复刷新。</p><p>为了解决这一个问题，在前端实现中，我们还需要为&nbsp;Api&nbsp;做一个异步请求列队：</p><pre><code class="language-typescript">interface QueuedRequest {
  (): Promise&lt;void&gt;;
}
interface TokenInfo {
  head: { alg: string, typ: string };
  payload: any;
  signature: string;
}


class ApiSingleton {
  private queue: QueuedRequest[];
  private isRefreshing: boolean;

  constructor() {
    this.queue = [];
    this.isRefreshing = false;
  }


  async request(url:string, data:any = {}, opts:any = {}): Promise&lt;any&gt; {
    let token = opts.accessToken || '';

    const sendRequest = async () =&gt; {
      // 自动获取 token
      if(!token) {token = await this.getToken();}
      if(token) {data.headers['Authorization'] = `Bearer ${token}`;}
      
      return this.requestSend(url, data, opts);
    };

    if (!token && this.isRefreshing) {
      return new Promise((resolve) =&gt; {
        this.queue.push(() =&gt; {
          return sendRequest().then(resolve);
        });
      });
    } else {
      return sendRequest();
    }
  }


  private requestSend(url:URL, data:any = {}, opts:any = {}): Promise&lt;any&gt;{
    // 发送请求
  }


  async getToken(): Promise&lt;string&gt; {
    const token = localStorage.getItem('__access_token')||'';
    if(!token) {return '';}
    const tokenInfo = this.parseToken(token);
    if(!tokenInfo) {
      localStorage.removeItem('__access_token');
      return '';
    }

    // 检查 access_token 是否有效
    let nowTime = Math.floor(Date.now() / 1000);
    if (nowTime &lt; tokenInfo.payload.exp-5) {return token;}

    if (!this.isRefreshing) {
      this.isRefreshing = true;
      const newAccessToken = await this.refreshToken(token);
      if(newAccessToken){
          localStorage.setItem('__access_token', newAccessToken);
      } else {localStorage.removeItem('__access_token');}
      this.isRefreshing = false;

      this.queue.forEach((queuedRequest) =&gt; queuedRequest());
      this.queue = [];
    }
    
    return localStorage.getItem('__access_token') || '';
  }

  private parseToken(token: string): TokenInfo|null {
    const [head64, body64, signature] = token.split('.');
    let head, payload;
    try{
      head = JSON.parse(atob(head64));
      payload = JSON.parse(atob(body64));
    } catch (err) {return null;}
    return { head, payload, signature };
  }

  async refreshToken(access_token: string): Promise&lt;string&gt; {
    let data = await Api.request('/token/refresh', {
      body: { access_token }
    }, { errorHandle: false, accessToken: access_token });

    if(data.code!==200) {return '';}
    return data.access_token;
  }
}

const Api = new ApiSingleton();
export { Api };</code></pre><p>以上步骤高度抽象来说，无非就四步：<strong>账密登录(C)&nbsp;&gt;&nbsp;签发令牌(S)&nbsp;&gt;&nbsp;储存令牌以供请求(C)&nbsp;&gt;&nbsp;校验令牌与权限管理(S)。</strong></p><p><br></p><h3>多设备管理</h3><p>其次，我们再处理多设备管理，同时具体化上述流程逻辑：</p><ol><li>不作修改。</li><li>签发&nbsp;tokens&nbsp;后，用&nbsp;tokens_{UID}&nbsp;为&nbsp;key，以&nbsp;{uniqid()}&nbsp;为&nbsp;hashKey&nbsp;储存&nbsp;JSON&nbsp;对象在&nbsp;Redis&nbsp;下的哈希表中，其值包含以下信息：ua、ip、iat、nbf、exp、access_token、refresh_token。其中除了标准的&nbsp;JWT&nbsp;Payload&nbsp;值，ua&nbsp;用于判断是否为本机；冗余储存&nbsp;access_token&nbsp;用于瞬时注销，解决&nbsp;JWT&nbsp;一经下发难以撤销的问题；refresh_token&nbsp;用于令牌更新时被遍历匹配。</li><li>不作修改。</li><li>在中间件鉴权通过后，同时查询&nbsp;Redis&nbsp;中是否存在为&nbsp;logoff_{access_token}&nbsp;的&nbsp;key，如有则代表该令牌已被注销，将中断请求并携特殊参数响应。</li><li>不作修改。</li><li>前端接收到非正常响应时，判断是否存在特殊参数，如有则进行相应处理。例如在该场景下，前端应清除本地储存的令牌，告知用户令牌已被注销，并跳转至登录页。</li></ol><p>按照这个逻辑，多设备管理便非常明了：只需列出&nbsp;key&nbsp;为&nbsp;tokens_{UID}&nbsp;的哈希表，通过&nbsp;hashKey&nbsp;指定某一设备，遂进行令牌注销。</p><p>另外，这里引入随机值作为&nbsp;hashKey&nbsp;的目的只有一个：保护令牌信息不泄露给客户端。</p><h2>多主体入驻</h2><p>这方面也就是稍复杂一些的多租户系统，所以只阐述概念上的规则内容。</p><p>首先是统一账户原则，个人账户一次注册，全平台通用。基于个人账户申请主体，后者分类如政府机关、监管机构、行业协会、事业单位、团体组织、基金会、个体户、企业等。主体信息填报通过资质审核后，方可进行实体及架构管理。</p><p>同时，个人账户与组织实体的从属关系是基于单独的业务系统存在的，是相互独立且隔离的，以此来提高平台的灵活性和扩展性。</p><h2>开发平台</h2><p>该平台是用于使个人开发者或团体组织能便捷地接入统一平台，在许多企业级应用中都能见到类似功能的影子。从安全角度来说，第三方应用皆是不安全的，故采用&nbsp;OAuth&nbsp;2.0&nbsp;的授权码模式来实现。</p><p>从最小可行性方案来说，第三方应用申请只需简单填写一个表单内容，即应用名称，后端为其生成客户端ID（client_id）及客户端秘钥（client_secret）。要提升安全性可以添加回调地址来强制匹配，提高自定义度可以添加图标、授权页样式等。</p><p>对于通过审核的第三方应用，授权流程如下：</p><ol style="text-indent: 0px; text-align: start;"><li>用户在第三方平台请求登录，跳转至统一登录系统的授权页面，且第三方平台需将&nbsp;client_id、授权的权限范围&nbsp;scope&nbsp;和回调地址&nbsp;redirect_uri&nbsp;作为参数传递。</li><li>用户在统一登录系统中同意授权，触发后端验证用户凭据，并生成一个授权码。</li><li>统一登录系统将用户重定向回第三方平台的回调地址，并通过&nbsp;URL&nbsp;传参附带授权码。</li><li>第三方平台收到授权码后，后端需要使用该授权码、client_id&nbsp;和&nbsp;client_secret&nbsp;向统一登录系统的接口请求访问令牌和刷新令牌。</li><li>统一登录系统返回访问令牌和刷新令牌。第三方平台可以用访问令牌来获取用户信息（如通过&nbsp;GET&nbsp;api/user&nbsp;接口）并完成登录过程。</li><li>同样，在访问令牌过期时，第三方平台需使用刷新令牌请求新的令牌。</li></ol><p>为了更加直观易懂，另附&nbsp;URL&nbsp;流程如下：</p><pre><code >第三方平台用户访问
https://solitude.land/user/edit

发现未登录，跳转至
https://solitude.land/signin?next=/user/edit

本地存储 next 地址后，访问统一登录系统授权页面
https://gxzv.com/oauth/?client_id=6445253fe2a00&scope=identify email phone&redirect_uri=https://solitude.land/callback/oauth

确认授权后，携授权码跳转回
https://solitude.land/callback/oauth?authorization_code=8661a04840fefd55e3504089d57ef580

第三方后端通过授权码获得身份令牌后，跳转至 next 地址
https://solitude.land/user/edit</code></pre><h2>接入第三方</h2><p>这部分是为使平台用户登录认证更便捷，以及使用户可借助自身平台完成其他平台应用的免登录认证，提高用户体验及留存。</p><p><br></p><h3>微信</h3><p>微信的公众平台和开放平台间的账户完全独立，由于目前只在公众平台缴纳了认证费，索性就通过公众号进行扫码登录，其原理如下：</p><ol><li>前端发起扫码请求。</li><li>后端请求微信服务器<a href="https://developers.weixin.qq.com/doc/offiaccount/Account_Management/Generating_a_Parametric_QR_Code.html" target="_blank">生成带场景值的二维码。</a></li><li>用户使用微信扫码，携带场景值跳转至公众号对话窗口（未关注则显示公众号资料页，关注后跳转）。</li><li>微信服务器将事件消息传给我们配置好的回调接口，在回调中获取场景值和用户微信的&nbsp;openid。</li><li>匹配到场景值对应的&nbsp;openid，通过&nbsp;openid&nbsp;匹配用户（若用户未绑定微信，则暂存，待用户通过其他方式认证身份后自动与&nbsp;openid&nbsp;绑定）。</li></ol><p>需要注意的是，只凭公众平台只能获取到用户基于公众号的唯一&nbsp;ID，也就是&nbsp;openid。无法获取到用户在微信的唯一&nbsp;ID，也就是&nbsp;unionid。这会导致公众号发生更换后，先前的绑定信息将失效。</p><p>为了解决这个可能发生的问题，可以在微信开放平台进行认证，并将两个平台关联。关联后微信服务器将同时返回&nbsp;openid&nbsp;与&nbsp;unionid。</p><p><br></p><h3>Odoo</h3><p>Odoo&nbsp;是一款集成化的企业管理软件，它为中小型企业提供了一站式解决方案，包括销售、采购、库存、生产、财务管理等多个业务领域。作为一款开源软件，Odoo&nbsp;拥有丰富的第三方插件和高度的定制性，可满足企业不同需求。Odoo&nbsp;16&nbsp;版本在原有基础上进行了持续优化与创新，强化了易用性、性能和扩展性，使企业能够高效地进行业务管理和协同，提升整体竞争力。</p><p>为了兼顾平台内用户的内部系统，如&nbsp;OA、CRM、ERP、BMS&nbsp;等，还需要使这类系统能通过&nbsp;OAuth&nbsp;2&nbsp;调用我们的&nbsp;Api&nbsp;以获取需要的信息。例如在&nbsp;Odoo&nbsp;内，我们需要先添加&nbsp;OAuth&nbsp;服务商：</p><pre><code >服务商名称: 甘小蔗的窝
Auth Flow: OAuth2
Token Map: 空
客户ID: 6445253fe2a00
Client Secret: 空
允许: 允许
登录按钮标签: 通过甘小蔗免登录
授权网址: https://gxzv.com/oauth
作用范围: identify email
UserInfo URL: https://api.gxzv.com/oauth/v1/user/info</code></pre><p>这样，在&nbsp;Odoo&nbsp;登录就会走以下流程：</p><ol><li>用户在&nbsp;Odoo&nbsp;登录页按下"通过甘小蔗免登录"。</li><li>Odoo&nbsp;携带&nbsp;response_type、client_id、redirect_uri、scope&nbsp;跳转至授权网址。</li><li>授权后，携带&nbsp;access_token&nbsp;跳转至&nbsp;redirect_uri，Odoo&nbsp;在回调中获取到令牌。</li><li>Odoo&nbsp;通过获取的&nbsp;access_token&nbsp;请求&nbsp;UserInfo&nbsp;URL&nbsp;获取用户信息，如&nbsp;UID&nbsp;或邮箱。</li><li>Odoo&nbsp;通过唯一信息结合服务商匹配本地账户，若没有则通过信息注册一个账户。</li></ol><p>在第二步中，跳转后的网址如下：</p><pre><code >https://gxzv.com/oauth
  ?response_type=token
  &client_id=6445253fe2a00
  &redirect_uri=http%3A%2F%2Foa.gxzv.cn%3A58068%2Fauth_oauth%2Fsignin
  &scope=identify+email
  &state=%7B%22d%22%3A+%22odoo16ent%22%2C+%22p%22%3A+5%2C+%22r%22%3A+%22http%253A%252F%252Foa.gxzv.cn%252Fweb%22%7D</code></pre><p>然而在实际调试中，Odoo&nbsp;成功通过&nbsp;UserInfo&nbsp;URL&nbsp;获取到用户信息后确报错：您无权访问此数据库或者您的邀请已经过期，请申请一个新的邀请并在您的邀请邮件中确认。</p><p>由于&nbsp;Odoo&nbsp;官方文档和网上都没提到其&nbsp;OAuth&nbsp;认证提供商部分的资料，所以我去翻了下它的源码。其中，用户信息这部分请求在&nbsp;<a href="https://github.com/odoo/odoo/blob/6ca41c4266f63ae629307330241aad810bbc950f/addons/auth_oauth/models/res_users.py#L86" target="_blank">addons/auth_oauth/models/res_users.py</a>&nbsp;文件中：</p><pre><code class="language-python"># -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.

import json

import requests
import werkzeug.http

from odoo import api, fields, models
from odoo.exceptions import AccessDenied, UserError
from odoo.addons.auth_signup.models.res_users import SignupError

from odoo.addons import base
base.models.res_users.USER_PRIVATE_FIELDS.append('oauth_access_token')

class ResUsers(models.Model):
    _inherit = 'res.users'

    oauth_provider_id = fields.Many2one('auth.oauth.provider', string='OAuth Provider')
    oauth_uid = fields.Char(string='OAuth User ID', help="Oauth Provider user_id", copy=False)
    oauth_access_token = fields.Char(string='OAuth Access Token', readonly=True, copy=False)

    _sql_constraints = [
        ('uniq_users_oauth_provider_oauth_uid', 'unique(oauth_provider_id, oauth_uid)', 'OAuth UID must be unique per provider'),
    ]

    def _auth_oauth_rpc(self, endpoint, access_token):
        if self.env['ir.config_parameter'].sudo().get_param('auth_oauth.authorization_header'):
            response = requests.get(endpoint, headers={'Authorization': 'Bearer %s' % access_token}, timeout=10)
        else:
            response = requests.get(endpoint, params={'access_token': access_token}, timeout=10)

        if response.ok: # nb: could be a successful failure
            return response.json()

        auth_challenge = werkzeug.http.parse_www_authenticate_header(
            response.headers.get('WWW-Authenticate'))
        if auth_challenge.type == 'bearer' and 'error' in auth_challenge:
            return dict(auth_challenge)

        return {'error': 'invalid_request'}

    @api.model
    def _auth_oauth_validate(self, provider, access_token):
        """ return the validation data corresponding to the access token """
        oauth_provider = self.env['auth.oauth.provider'].browse(provider)
        validation = self._auth_oauth_rpc(oauth_provider.validation_endpoint, access_token)
        if validation.get("error"):
            raise Exception(validation['error'])
        if oauth_provider.data_endpoint:
            data = self._auth_oauth_rpc(oauth_provider.data_endpoint, access_token)
            validation.update(data)
        # unify subject key, pop all possible and get most sensible. When this
        # is reworked, BC should be dropped and only the `sub` key should be
        # used (here, in _generate_signup_values, and in _auth_oauth_signin)
        subject = next(filter(None, [
            validation.pop(key, None)
            for key in [
                'sub', # standard
                'id', # google v1 userinfo, facebook opengraph
                'user_id', # google tokeninfo, odoo (tokeninfo)
            ]
        ]), None)
        if not subject:
            raise AccessDenied('Missing subject identity')
        validation['user_id'] = subject

        return validation

    @api.model
    def _generate_signup_values(self, provider, validation, params):
        oauth_uid = validation['user_id']
        email = validation.get('email', 'provider_%s_user_%s' % (provider, oauth_uid))
        name = validation.get('name', email)
        return {
            'name': name,
            'login': email,
            'email': email,
            'oauth_provider_id': provider,
            'oauth_uid': oauth_uid,
            'oauth_access_token': params['access_token'],
            'active': True,
        }

    @api.model
    def _auth_oauth_signin(self, provider, validation, params):
        """ retrieve and sign in the user corresponding to provider and validated access token
            :param provider: oauth provider id (int)
            :param validation: result of validation of access token (dict)
            :param params: oauth parameters (dict)
            :return: user login (str)
            :raise: AccessDenied if signin failed

            This method can be overridden to add alternative signin methods.
        """
        oauth_uid = validation['user_id']
        try:
            oauth_user = self.search([("oauth_uid", "=", oauth_uid), ('oauth_provider_id', '=', provider)])
            if not oauth_user:
                raise AccessDenied()
            assert len(oauth_user) == 1
            oauth_user.write({'oauth_access_token': params['access_token']})
            return oauth_user.login
        except AccessDenied as access_denied_exception:
            if self.env.context.get('no_user_creation'):
                return None
            state = json.loads(params['state'])
            token = state.get('t')
            values = self._generate_signup_values(provider, validation, params)
            try:
                login, _ = self.signup(values, token)
                return login
            except (SignupError, UserError):
                raise access_denied_exception

    @api.model
    def auth_oauth(self, provider, params):
        # Advice by Google (to avoid Confused Deputy Problem)
        # if validation.audience != OUR_CLIENT_ID:
        #   abort()
        # else:
        #   continue with the process
        access_token = params.get('access_token')
        validation = self._auth_oauth_validate(provider, access_token)

        # retrieve and sign in user
        login = self._auth_oauth_signin(provider, validation, params)
        if not login:
            raise AccessDenied()
        # return user credentials
        return (self.env.cr.dbname, login, access_token)

    def _check_credentials(self, password, env):
        try:
            return super(ResUsers, self)._check_credentials(password, env)
        except AccessDenied:
            passwd_allowed = env['interactive'] or not self.env.user._rpc_api_keys_only()
            if passwd_allowed and self.env.user.active:
                res = self.sudo().search([('id', '=', self.env.uid), ('oauth_access_token', '=', password)])
                if res:
                    return
            raise

    def _get_session_token_fields(self):
        return super(ResUsers, self)._get_session_token_fields() | {'oauth_access_token'}</code></pre><p>而最相关的，便是&nbsp;auth_oauth&nbsp;函数方法。</p><p>在其流程中，_auth_oauth_rpc&nbsp;方法使用&nbsp;GET&nbsp;请求从&nbsp;UserInfo&nbsp;URL&nbsp;获取数据，以&nbsp;access_token&nbsp;作为查询参数或在请求头部作为授权头发送，并返回&nbsp;UserInfo&nbsp;URL&nbsp;提供的&nbsp;JSON&nbsp;数据。_auth_oauth_validate&nbsp;方法则负责调用&nbsp;_auth_oauth_rpc&nbsp;方法来获取&nbsp;UserInfo&nbsp;URL&nbsp;的数据。接着，它将检查返回的数据是否有错误。如果没有错误，该方法将更新&nbsp;validation&nbsp;字典，并将其返回给调用者。</p><p>然后，将&nbsp;validation&nbsp;传入&nbsp;_auth_oauth_signin&nbsp;方法中，执行后续的登录或注册流程。而先前的报错也于此：在没有匹配到本地用户尝试注册账户时，由于&nbsp;no_user_creation&nbsp;参数没有设置为关闭，导致&nbsp;Odoo&nbsp;不会通过注册创建新用户，遂报错。</p><p><br></p><h3>自建&nbsp;Odoo</h3><p>在自己搭建&nbsp;Odoo&nbsp;的过程中，需要注意&nbsp;Nginx&nbsp;反向代理的问题。</p><pre><code >upstream odoo {
  server 127.0.0.1:8069;
}
upstream odoochat {
  server 127.0.0.1:8072;
}
map $http_upgrade $connection_upgrade {
  default upgrade;
  ''      close;
}

server
{
  listen 80;
  listen 443 ssl http2;
  server_name odoo.gxzv.com;
  index index.php index.html index.htm default.php default.htm default.html;
  root /www/wwwroot/odoo;

  #REWRITE-START URL重写规则引用,修改后将导致面板设置的伪静态规则失效
  proxy_read_timeout 720s;
  proxy_connect_timeout 720s;
  proxy_send_timeout 720s;

  # Add Headers for odoo proxy mode
  proxy_set_header X-Forwarded-Host $host;
  proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
  proxy_set_header X-Forwarded-Proto $scheme;
  proxy_set_header X-Real-IP $remote_addr;

  location @odoo {
    proxy_pass http://odoo;
    proxy_redirect off;
  }

  location / {
    proxy_pass http://odoo;
    proxy_redirect off;
  }

  location /websocket {
    proxy_pass http://odoochat;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection $connection_upgrade;
  }

  location ~ ^/[^/]+/static/.+$ {
    root /opt/odoo;
    try_files /community/odoo/addons$uri @odoo;
    expires 24h;
  }
  #REWRITE-END

  #禁止访问的文件或目录
  location ~ ^/(\.user.ini|\.htaccess|\.git|\.svn|\.project|LICENSE|README.md)
  {
    return 404;
  }

  access_log  /www/wwwlogs/odoo.gxzv.com.log;
  error_log  /www/wwwlogs/odoo.gxzv.com.error.log;
}</code></pre><h2>支付处理</h2><p>在平台收到收付款项请求时，将创建订单后统一通过收银台进行处理。同时，为了方便境内用户付款，我们还需对接微信与支付宝。</p><p><br></p><h3>微信支付</h3><p>首先，需要使用主体资质注册<a href="https://mp.weixin.qq.com/" target="_blank">微信公众平台</a>和<a href="https://pay.weixin.qq.com/" target="_blank">微信支付商户平台</a>，并缴纳认证年费完成资质认证，然后将公众号与商户号进行关联。</p><p>完成上诉步骤后，你将获得来自公众号平台的&nbsp;AppId，商户平台的&nbsp;MchId&nbsp;以及&nbsp;SecretKey。但为了调用微信支付的&nbsp;v3&nbsp;接口，还需借助工具在商户后台生成商户&nbsp;API&nbsp;证书，具体流程可见<a href="https://kf.qq.com/faq/161222NneAJf161222U7fARv.html" target="_blank">腾讯客服&nbsp;-&nbsp;API&nbsp;证书及密钥</a>。</p><p>至此已经可以顺利发起接口调用，但若要应答和对回调进行签名验证，还需要获取微信支付的平台证书，具体可以见：<a href="https://pay.weixin.qq.com/wiki/doc/apiv3/apis/wechatpay5_1.shtml" target="_blank">微信支付文档&nbsp;-&nbsp;获取平台证书</a>。</p><p>对于微信开发，我这里的相应后端语言为&nbsp;PHP，故选择了&nbsp;<a href="https://easywechat.com/" target="_blank">EasyWeChat</a>&nbsp;作为微信&nbsp;SDK。需要注意的是，其&nbsp;<a href="https://easywechat.com/6.x/introduction.html" target="_blank">v6&nbsp;的改动和之前版本截然不同</a>，需要结合微信官方开发文档来进行具体的业务开发，甚至当前无法在网上找到相关实例。</p><p>这里我基于&nbsp;Webman&nbsp;提供一份实例示例，首先是配置文件：</p><pre><code class="language-php">&lt;?php

return [
    'official_account' =&gt; [
        'app_id' =&gt; 'wx1127814e7843e7e7',
        'secret' =&gt; 'gxzv.com',
        'token' =&gt; 'gxzv_api_token',
        'aes_key' =&gt; 'gxzv.com'
    ],

    'merchant_account' =&gt; [
        'mch_id' =&gt; '1643647873',
        'secret_key' =&gt; 'gxzv.com',

        'private_key' =&gt; base_path() . '/config/wechat/apiclient_1643647873_key.pem',
        'certificate' =&gt; base_path() . '/config/wechat/apiclient_1643647873_cert.pem',

        'platform_certs' =&gt; [
            base_path() . '/config/wechat/wechatpay.pem'
        ]
    ]
];</code></pre><p>其次是发起接口调用的代码段，这里是请求付款二维码：</p><pre><code class="language-php">// ...省略
$oaConfig = config('wechatv6.official_account');
$app = new Application(config('wechatv6.merchant_account'));
$api = $app-&gt;getClient();
$amount = intval($payment-&gt;amount*100);
$response = $api-&gt;postJson('/v3/pay/transactions/native', [
    'mchid' =&gt; (string)$app-&gt;getMerchant()-&gt;getMerchantId(),
    'out_trade_no' =&gt; $tradeNo,
    'appid' =&gt; $oaConfig['app_id'],
    'description' =&gt; $payment-&gt;desc,
    'notify_url' =&gt; config('app.host').'/callback/wechat/pay',
    'amount' =&gt; [
        'total' =&gt; $amount,
        'currency' =&gt; 'CNY',
    ],
    'time_expire' =&gt; $expires_at
]);

$respArray = $response-&gt;toArray(false);
if(isset($respArray['code_url'])){
    Redis::set($rKey, $respArray['code_url']);
    $payment-&gt;expires_at && Redis::expire($rKey, $expires_in);
}
// ...省略</code></pre><p>同时，为了在&nbsp;Webman&nbsp;中处理微信服务器发起的回调请求，处理时需要稍作修改：</p><pre><code class="language-php">&lt;?php

namespace app\controller\callback\wechat;

use EasyWeChat\Pay\Application;
use EasyWeChat\Kernel\Exceptions;
use EasyWeChat\Pay\Message;
use Psr\Http\Message\ResponseInterface;
use support\Request;
use Symfony\Component\HttpFoundation\HeaderBag;
use Symfony\Component\HttpFoundation\Request as SymfonyRequest;

class PayController
{
    /**
     * @throws Exceptions\InvalidArgumentException
     * @throws \Throwable
     * @throws \ReflectionException
     * @throws Exceptions\RuntimeException
     */
    public function index(Request $request): ResponseInterface
    {
        $app = new Application(config('wechatv6.merchant_account'));
        $symfony_request = new SymfonyRequest($request-&gt;get(), $request-&gt;post(), [], $request-&gt;cookie(), [], [], $request-&gt;rawBody());
        $symfony_request-&gt;headers = new HeaderBag($request-&gt;header());
        $app-&gt;setRequestFromSymfonyRequest($symfony_request);
        $server = $app-&gt;getServer();

        $server-&gt;handlePaid(function (Message $message) {
            $tradeNo = $message['out_trade_no'];
            $tid = $message['transaction_id'];
            // 业务逻辑...
        });

        // 默认返回 ['code' =&gt; 'SUCCESS', 'message' =&gt; '成功']
        return $server-&gt;serve();
    }
}</code></pre><p>至此，即可简易实现完整的微信支付流程。</p>]]></content><category term="软件架构" scheme="https://gxzv.com/blog/tag/7" /><category term="网络安全" scheme="https://gxzv.com/blog/tag/8" /><category term="前端开发" scheme="https://gxzv.com/blog/tag/9" /><category term="物联网" scheme="https://gxzv.com/blog/tag/12" /></entry></feed>