[{"content":"如何监控多个 SaaS 软件项目的运营数据，统一给负责人发送运营日报。\n如何监控运营数据？ 常见做法\n在业务层入侵式埋点、上报，定时任务程序读取后汇总发送。\n在业务层非入侵式\n埋点、上报，定时任务程序读取后汇总发送; 独立分析模块通过读存储数据副本、监控日志离线分析，存储分析结果，定时任务程序读取后汇总发送。 简单的解耦合、无状态和分布式发送日报设计和实现 发送日报常见问题\n单个项目 vs 多个项目： 几乎每个项目都要发送， 没有必要重复实现类似逻辑；整个多个项目统一发送同时考虑解耦合 异构运行环境 serverless vs VM vs container： 不同运行环境下统一上报通用格式的数据，集中处理后再发送 统一语言框架 vs 多语言多框架： 和前者类似，实现独立于语言和独立于框架 解决方案架构：\n┌─────────────────────────┐ │ Serverless Worker 1 + DB│──┐ └─────────────────────────┘ │ HTTP POST/RPC (API Key) ├──► ┌─────────────────────────┐ ┌─────────────────┐ ┌─────────────────────────┐ │ │ Central Worker │─────►│ Notification │ │ Serverless Worker 2 + DB│──┤ │ (schedule job @ 10:00) │ │ (IM/Email) │ └─────────────────────────┘ │ └─────────────────────────┘ └─────────────────┘ │ ┌─────────────────────────┐ │ │ Computing Nodes + DB │──┤ └─────────────────────────┘ │ │ .... ──┘ Central Worker 模块\n接收 JSON 格式运营数据 发送日报到负责人 project 管理 定义有什么项目和应用，运营发送日报发送格式 预定义通用的运营日报 section，如 用户总数、日/周/月增长、日/周/月活跃、日/周/月订阅付费 节假日按欧美还是按大中华区，节假日是否发送 上报接口使用的 API 验证 key project \u0026amp; contact 管理， 定义日报使用什么信道、发送给谁 定义通用接收上报数据 JSON 格式\nproject: string \u0026lt;project name\u0026gt; data: array, `[{\u0026#34;key.name\u0026#34;: \u0026lt;data point\u0026gt;},{\u0026#34;key.name\u0026#34;: \u0026lt;data point\u0026gt;}, ...]` timestamp: float64, \u0026lt;unix timestamp in Reporting interval granularity hours/days\u0026gt; 定义日报模板\n# \u0026lt;hard-coded xxx project\u0026gt; ## \u0026lt;hard-coded section name\u0026gt; - DAU: ${key.name} - Installs: {{ `select sum(foo) from \u0026lt;project name as table name\u0026gt;` }} - Uninstalls: {{ `select sum(bar) from \u0026lt;project name as table name\u0026gt;` }} 可按需实现一个简陋的带计算逻辑模板:\nhard-coded ... 部分的直接写死 ${key.name} 的从存储按 project 和 key.name 组合查询 {{ ... }} 聚合计算后再填充模板 如何扩展 水平扩展\nCentral Worker 支持几乎无限个 project 接入 Central Worker 和相关存储过载时，可以部署多套，修改 project 上报 HTTP/RPC 入口即可 安全性\nCentral Worker 改动频率低，设置单管理员用户或写死特定 SSO 用户统一管理即可 单个 project 泄漏对其他项目无影响 单个信道密钥泄漏，对其他项目无影响，重置即可 智能洞察\nmonth-to-month 环比 year-to-year 同比：保留日报每次发送历史，提供接口给大模型分析，在日报基础上实现历史分析，发送环比月报和同比年报 be-better 让大模型根据 日月年报运营数据提供运营改进建议 ","permalink":"https://weblog2.pages.dev/programming/2026-09-18-saas-send-report-daily/","summary":"\u003cp\u003e如何监控多个 SaaS 软件项目的运营数据，统一给负责人发送运营日报。\u003c/p\u003e","title":"SaaS 软件架构设计：发送运营日报"},{"content":"IELTS 雅思的学习和备考。\nIELTS 是什么 The International English Language Testing System(IELTS /ˈaɪ.eltz/) is a standardized test of English language proficiency for non-native speakers.\n雅思 (IELTS) 是一个针对母语为非英语的英语熟练度标准化测试。\n它是在 1980 年代开始建立、由英国的 British Council （英国文化教育协会），IDP Education （IDP教育集团）和 Cambridge University Press \u0026amp; Assessment （剑桥大学出版社与考评部）三个机构一起开发完善的的一套语言评测体系，在英语系国家地区中通常作为留学、工作、移民、学术等的一个前置语言要求。\n它分为听 Listening 读 Reading 写 Writing 说 Speaking 四部分考试。全球全年多个地区有考点，几乎每周都可报考。考试费用约人民币 2000 元。\nIELTS 多次改革完善，以适应时代需求，如让它更贴近生活、实用场景，使用机器考试代替纸张考试等。\n为什么选 IELTS 英语语言测试有多种，大致分英国系 IELTS 和美国系 TOEFL(Test fo English as a Foreign Language)。 如今的美国在军事经济文化方面在全球影响力巨大，大英帝国已没落，为何不选美国系？\n曾经的大英帝国在 19 世纪时殖民地遍布东半球到西半球，因此有“日不落帝国”说法。20 世纪殖民地纷纷独立、帝国解体后，有 56 个相关的国家和地区一起成立了 Commonwealth of Nations 英联邦。 英联邦是一个松散的经济文化联盟，成员国之间旅游、留学、贸易方面通常自由互通，IELTS 测试在所有英联邦国家和地区里面互相承认。\n前英国殖民地香港即使在 1997 年已回归中国大陆，如今香港所有大学依然以 IELTS 考试作为研究生申请要求之一。\n坊间流传，英国女王 Elizabeth 二世曾说过：\nFor the avoidance of doubt, there is no such thing as \u0026lsquo;American English.\u0026rsquo; There is The English language and there are mistakes.\n这当然是谣传，只反映了大众对语言“历史”“传统”尊崇，和体现了英式幽默。\n事实上，不论英国系英语还是美国系英语，大部分可互通。\nIELTS 学习素材和备考 前述三个机构参与开发和维护了 IELTS 体系，它们各自都出了学习素材。\nCambridge English Cambridge University Press \u0026amp; Assessment 是参与开发 IELTS 三个机构之一，每年把历史真实考题汇编出版为 《Cambridge English》（剑桥雅思英语），接近一年一套。 因为出版真题，它的影响影响最广最流行。\n第一套是 \u0026ldquo;剑桥1\u0026rdquo;，指 《剑桥雅思英语 1》。1-3 年代久远过时淘汰，常见提法是 \u0026ldquo;剑桥4\u0026rdquo; 开始。有一些考试培训机构甚至认为 剑桥 5-7 也偏旧，时间不充裕可直接从 “剑桥8” 开始。\n2026 开始 IELTS 逐步推行 听读写机考代替纸考，因此做真题测试应尽可能使用带鼠标键盘的台式机电脑，沉浸式适应考试节奏和习惯。不建议购买纸质真题做测试。\n除了真题外，Cambridge English 官方网站 Learning English https://www.cambridgeenglish.org/learning-english 有不少听读写学习素材。\nBBC Learning English BBC (British Broadcasting Corporation) 英国广播公司是英国国家成立的广播机构，提供电视、广播电台、门户网站内容服务。经费来源为英国民众电视授权费、英国外交部拨款、广告费等。 它受 Royal Charter （皇家特许状）法律保障独立于英国政府运作，政府不能严控内容方向。\nBBC 官方网站 Learning English https://www.bbc.co.uk/learningenglish 有不少听读写学习素材。\n其中 PODCASTS 播客有多种音频节目素材，推荐以下\n6 Minute English Learning English Grammar Learning English Conversations Learning English from the News 注：非英国境内订阅，它会其前插入 110 秒、后插入 75 秒左右的广告。理由是英国境内民众支付了授权费可剥离广告，境外没有。\n一些大胆想法 基于数据分析学习 将剑桥雅思所有真题按文本分析统计单词、词组、句子，分别得到\n高频词汇 常用词组和短语 同位语、同义改写 背单词，按词组、英文解释、相关词组词根背诵，融入英语语言思维； 模仿真题素材词汇和句法，锻炼写作。\nAI 和 LLM 预测 IELTS 三个机构出版和提供的学习素材（含剑桥雅思所有真题）、1980 年开始历年关于语言重大新闻、学术研究综合给 AI 和 LLM 分析，可否预测下一次考试大方向和口语题库？\n语言考试在 AI 时代重要性是否会变化 如果人均可配置一个实时翻译设备，可以是电脑或手机软件，也可以手表手环眼镜穿戴式硬件，甚至是视网膜全息投影，那么学习、生活和工作是不是没有语言障碍？ 英语仅在不同文化语言背景的学术研究和学术出版作为一个中心中转媒介。\n","permalink":"https://weblog2.pages.dev/language/2026-09-15-prepare-for-an-ielts-test/","summary":"\u003cp\u003eIELTS 雅思的学习和备考。\u003c/p\u003e","title":"Prepare for an IELTS Test"},{"content":"Hi! I\u0026rsquo;m Antony, or @tony9527167 on the internet. I build things, mostly software, and occasionally ideas that turn into long-term projects\nI enjoy writing about web service architecture, system evolution, technical debt, and how real-world software stays alive over time. I\u0026rsquo;m also interested in games and interactive systems, because they reveal a lot about human behavior, complexity, and design — just like large-scale software does.\nI believe in building sustainable, self-funded products rather than chasing hype, and in designing systems that people can rely on for years.\nI previously worked at\nHUYA (NASDAQ: HUYA),\nJOYY(YY) (NASDAQ: YY), and\nWondershare Technology (SHE: 300624),\nwhere I built and operated backend and platform systems serving millions of users.\nToday, I work with a software consulting company, providing independent software and architecture consulting:\nDesigning and scaling full-stack platforms (browser extensions, web, and mobile) for products with tens of millions of users Building monitoring and alerting systems for PB-scale data Helping Chinese startups go global, covering technology, compliance, and cross-border infrastructure This blog is where I share what I learn from building systems, playing games, and navigating life in a technical world.\n","permalink":"https://weblog2.pages.dev/about/","summary":"\u003cp\u003eHi! I\u0026rsquo;m Antony, or @tony9527167 on the internet.\nI build things, mostly software, and occasionally ideas that turn into long-term projects\u003c/p\u003e\n\u003cp\u003eI enjoy writing about web service architecture, system evolution, technical debt, and how real-world software stays alive over time.\nI\u0026rsquo;m also interested in games and interactive systems, because they reveal a lot about human behavior, complexity, and design — just like large-scale software does.\u003c/p\u003e\n\u003cp\u003eI believe in building sustainable, self-funded products rather than chasing hype, and in designing systems that people can rely on for years.\u003c/p\u003e","title":"About tony9527167"},{"content":"休长假中间重温通关一个老游戏 Jagged Alliance 2 (JA2) ，发现许多有趣细节，联想古今破了不少疑惑。\n游戏简介 Jagged Alliance 2 (JA2) 是一个战略角色扮演游戏 (Strategy role-playing game) 和 回合制战术 (Turn-based tactics) 2D 游戏。 游戏故事背景发生在 Arulco ，一个虚构的第三世界小国，游戏的目标是通过雇佣兵小队推翻 Arulco 的女王独裁者 Deidranna 。\nJagged Alliance 2: Wildfire (JA2WF) 是 JA2 的扩展修改包，提升了游戏难度、完善物品和枪械平衡性、增加高分辨率支持、修复 bugs 等。\n玩家通过回答 16 个问题形式创建并扮演一个雇佣兵核心角色。回答 16 个问题时选择不同的答案会生成不同专有技能 (skill) 和性格特征 (traits) 的角色。 有技能有开锁、电子改修、使用自动武器、夜间行动、使用重武器等等，性格可稳定，正常或癫狂。\n每个角色还有属性(role attribute)。可通过空闲时间练习 (practice) 提升的 9 个属性：\n体力 strength 灵巧 dexterity 敏捷 agility 健康 health 射击 marksmanship 医疗 medical 电子 mechanical 领导 leadership 爆破 explosives 不能通过练习提升的 2 个属性：\n智力 wisdom 级别 experience level 级别值范围 1-10，其他值范围 0-100 。游戏起始会给玩家固定点数，可自行侧重天赋添加点数到指定属性。\n以核心雇佣兵角色为中心，游戏中通过 \u0026ldquo;国际雇佣兵协会\u0026rdquo; 雇佣、支线任务招募素人等方式组建团队，攻占 7 个地区、若干个中立地区并击杀女王即可通关。\n游戏细节 八十万打六十万优势在我，为何输？ 创建核心角色只有一个，为嫡系雇佣兵。 游戏起始的属性点数有限，为了把嫡系核心雇佣兵训练成六边形战士，会让非嫡系把引诱敌人、打残敌人，然后喂给嫡系补刀，补刀越多、属性点数长得越快、自然经验值也长得越快。\n当肉盾、打冷枪、枪林弹雨总有失误，九死一生，游戏不同现实战争，游戏可无限重来，现实一失足千古狠。\n要解放先抢掠 雇佣兵团名义上攻打地区为了解放地区，推翻女王独裁统治。 反抗军和幕后主脑，初期给两个属性很一般的次要可免费招募角色，此后只做三件事发邮件催你进攻进攻继续进攻，要钱不给钱，要军械不给军械，一点用都没。\n雇佣兵团为了补给获得装备、补给，在攻打地区时，会挨个破门锁搜掠平民房屋。\n游击战屡战屡胜 占领城区容易无偿补给、或进行有偿各类物资交易，但城区精锐防守且易守难攻。\n进攻非城区打遭遇战、游击战，能让雇佣兵涨经验提升技能外，还能获得装备补给，雇佣兵游击队越打越强，屡战屡胜。\n城区防守兵不会离开城区清剿城区外雇佣兵游击队。\n雇佣兵游击队积累突击步枪、狙击枪、迫击炮、反坦克炮等大量高精装备后再进攻城区，几乎是降维打击。\n奇怪设定 贫民窟里搜出霰弹枪子弹、禁制药物； 大学里面搜出色情杂志；\n枪支黑市商人维护独裁统治，攻击非王宫附近地区，黑市商人会若无其事开心和你交易，攻击独裁者王宫附近地区后，商人和你敌对。\n现代战术 现代战术 \u0026ldquo;城区巷战\u0026rdquo; \u0026ldquo;户外丛林口袋战术\u0026rdquo; 在游戏中竟然适用。 难怪以前某国用电子游戏训练部队反应、战术和团队作战。\n","permalink":"https://weblog2.pages.dev/gaming/2025-09-06-notes-on-ja2/","summary":"\u003cp\u003e休长假中间重温通关一个老游戏 \u003cem\u003eJagged Alliance 2\u003c/em\u003e (JA2) ，发现许多有趣细节，联想古今破了不少疑惑。\u003c/p\u003e","title":"Notes on Jagged Alliance 2"},{"content":"使用 Hugo 构建静态博客，并快速免费部署。\n几个静态网站构建工具对比 NAME STARTS FORKS FIRST RELEASED LANGUAGE jekyll 51k 10k Nov, 2008 Ruby Hexo 41k 5k Oct, 2012 TypeScript Hugo 86k 8k Jul, 2013 Go VuePress ~3k ~1k Apr, 2018 TypeScript VitePress 17k ~3k Apr, 2020 TypeScript MkDocs 21k ~3k Nov, 2014 Python 这几个工具个有优缺点\njekyll ：GitHub 最古老最多人关注，构建工具依赖 Ruby 和相关依赖包，在最新的 macOS 系统上安装麻烦，依赖包兼容性差； Hexo ：中文主题多但自身官方文档不完整且混乱； VuePress ：适合开源软件项目简陋官网和文档，易于通过 Vue 编写扩展，但默认缺乏最基本分类 (categories)、标签 (tags) 等静态博客功能。 VitePress ： 定位和功能和 VuePress 类似。基于更现代的 Vue 3 和 Vite 技术栈构建，热更新和构建速度更快。 MkDocs : 对旧系统和旧浏览器（如 Windows 7 和 IE 11）兼容性最好的工具，避免使用浏览器现代技术因此默认主题简陋。适合构建需支持全版本全平台技术类文档。 其他 astro 类似整合 Vue 和 next.js 为一体的工具，强大而复杂。 核心功能对比 FEATURE Hugo MkDocs 使用常见 markdown 语法 no yes 自定义 CSS yes yes 自定义页面列表顺序 yes yes 自定义导航栏 yes yes 自定义边栏 no yes 自定义主题 yes yes 自动深色模式 no no 响应式(Responsive) yes yes 集成搜索 ? yes 支持 IE11 no yes Windows 7 构建 yes yes 最适合现代博客 hugo + Cloudflare 的方案\n在 Cloudflare 上部署 hugo 最新版本构建的静态博客。\n构建工具使用 hugo，安装简单只有一个二进制文件、构建速度快、文档完善； 静态页面托管、域名、SSL 证书都是用 Cloudflare ，免费且稳定； 部署步骤\n以此文编写时刻为例，按 hugo 官方文档是用构建工具版本为 0.147.2， 生成项目后，添加 markdown 页面内容； 安装构建工具参考命令 CGO_ENABLED=1 go install -v -tags extended github.com/gohugoio/hugo@v0.147.2 如果是用官方文档推荐的默认 ananke 主题，需要锁定它的版本 cd themes/ananke ; git checkout v2.12.0 提交内容项目到 GitHub 仓库； 在 Cloudflare 上创建项目，选择 hugo 构建工具，选择 GitHub 仓库，等待构建完成； 配置域名和 SSL 证书，完成。 注意：\nCloudflare Pages 如果不通过环境变量指定 hugo 版本，默认会使用 0.118.2 版本，导致构建失败； 设置环境变量 HUGO_VERSION 和前面生成项目时指定的版本一致即可。 最终方案的效果就是当前博客的样子。\n自定义 hugo CSS 样式 根据使用 theme 不一样可能要按需修改，以 themes/ananke 为例。\n根目录下新建 layouts/partials/head-additions.html 文件， 内容为 \u0026lt;link rel=\u0026quot;stylesheet\u0026quot; href=\u0026quot;{{ \u0026quot;css/main.css\u0026quot; | relURL }}\u0026quot;\u0026gt; 根目录下新建 static/css/main.css ，文件内容为要添加的自定义 CSS 样式 工作原理， themes/ananke 会在 baseof.html 引入 partials/head-additions.html 子模板，默认从根目录开始加载文件覆盖（如有）。\n最适合全平台手册 MkDocs + Cloudflare 的方案\n在 Cloudflare 上部署 MkDocs 最新版本构建的技术手册。\nref: https://developers.cloudflare.com/pages/framework-guides/deploy-an-mkdocs-site/\n自定义 Python 版本 在 Environment variables 环境变量中添加 PYTHON_VERSION 指定版本即可，如 3.8.10 。\n自定义 MkDocs CSS 样式 新建 custom_theme/main.html 文件修改，模板语法和内容参考 https://www.mkdocs.org/dev-guide/themes/\n集成 Disqus 并禁用冗余强制广告 在 custom_theme/main.html 文件添加以下\n{% include \u0026#34;disqus.html\u0026#34; %} 新建 custom_theme/disqus.html 文件，内容如下\n{% block comments %} \u0026lt;div id=\u0026#34;disqus_thread\u0026#34;\u0026gt;\u0026lt;/div\u0026gt; \u0026lt;script\u0026gt; /** * RECOMMENDED CONFIGURATION VARIABLES: EDIT AND UNCOMMENT THE SECTION BELOW TO INSERT DYNAMIC VALUES FROM YOUR PLATFORM OR CMS. * LEARN WHY DEFINING THESE VARIABLES IS IMPORTANT: https://disqus.com/admin/universalcode/#configuration-variables */ /* var disqus_config = function () { this.page.url = PAGE_URL; // Replace PAGE_URL with your page\u0026#39;s canonical URL variable this.page.identifier = PAGE_IDENTIFIER; // Replace PAGE_IDENTIFIER with your page\u0026#39;s unique identifier variable }; */ (function () { // DON\u0026#39;T EDIT BELOW THIS LINE var d = document, s = d.createElement(\u0026#34;script\u0026#34;); s.src = \u0026#34;https://你的-domain-name.disqus.com/embed.js\u0026#34;; s.setAttribute(\u0026#34;data-timestamp\u0026#34;, +new Date()); (d.head || d.body).appendChild(s); })(); \u0026lt;/script\u0026gt; \u0026lt;noscript\u0026gt;Please enable JavaScript to view the \u0026lt;a href=\u0026#34;https://disqus.com/?ref_noscript\u0026#34;\u0026gt;comments powered by Disqus.\u0026lt;/a\u0026gt;\u0026lt;/noscript\u0026gt; \u0026lt;!-- https://stackoverflow.com/a/56613937 --\u0026gt; \u0026lt;style\u0026gt; iframe[src*=\u0026#34;ads-iframe\u0026#34;] { display: none; } \u0026lt;/style\u0026gt; {% endblock %} ","permalink":"https://weblog2.pages.dev/programming/2025-05-12-deploy-weblog-site/","summary":"\u003cp\u003e使用 Hugo 构建静态博客，并快速免费部署。\u003c/p\u003e","title":"Deploy weblog site with hugo"},{"content":"继续原理篇的话题。本篇从 OAuth 诞生的痛点开始，完整介绍如何在一个 SaaS 系统中接入 Google 和 Apple 社交登录，覆盖 Web 应用和 Chrome Extension 两种场景。\n多平台的痛点 → OAuth 的诞生 进入 Web 2.0 时代，资源提供方越来越多。用户在不同平台需重复注册账号和设置密钥，带来两大问题：\n效率低：频繁在多个平台注册账户，操作繁琐。 安全差：用户在不同平台设置同样的访问密钥，一个平台泄露，所有平台都暴露。 OAuth 方式认证应运而生，它分两个部分：\n认证（Authentication）：为用户生成全局唯一标识，验证用户身份； 授权（Authorization）：代理用户授权给第三方平台使用用户信息（如用户标识、昵称等），用户无需直接注册、也无需重复提交信息或设置访问密钥。 ┌─────────────┐ ┌──────────────────┐ ┌─────────────┐ │用户 account │────▶│ 资源提供方 │────▶│ 认证平台 │ │ │ │ Resource Owner │ │ Auth │ └─────────────┘ └──────────────────┘ └─────────────┘ 用现实场景理解信任链 我和系统张三互相认识。我想从系统李四那里借钱，但我和李四不熟。好在系统张三人人认识，我和李四都信任张三。李四从张三那里确认我的身份后，我就可以借钱了。\n后续多了王五、赵六也一样。我在不同平台使用同一个身份，并且可以控制——张三不能把我存钱在别处的秘密告诉李四。这就是授权和信任传递。\n支持多社交账号登录的账号系统 核心思路 在全球范围内，大部分互联网用户要么使用 Google（Gmail），要么使用 Apple（Apple Mail），或者两者都用。因此，如果 SaaS 系统接入它们的账号体系，就意味着支持了全球大多数用户。这类第三方账号统称为 Social Account。\n结合\u0026quot;一处注册，到处可用\u0026quot;的思路，引入一个第三方角色：\nGoogle / Apple Social Account System │ ┌───────────────┼───────────────┐ │ │ │ Platform A Platform B Platform C 用户只需在 Google/Apple 注册一次账号，就可以在多个平台使用。\n信任链模型 用户 aa 在 Google 注册了账号（email + password），Google 分配唯一 ID。 用户 aa 想在应用系统 bar 上使用服务，只需让 bar 去 Google 确认 \u0026#34;我是 aa\u0026#34;。 这个过程包含三个关键点：\n认证：验证用户身份 授权：让用户控制哪些信息可以共享 信任传递：认证平台作为可信第三方 OAuth 授权码模式 vs 隐式模式 在进入实战之前，先了解两种主流的 OAuth 流程。它们在不同的客户端场景下各有用途：\nAuthorization Code Flow（授权码模式）：前端拿到一次性 code，后端再用 code + client_secret 换 token。更安全，适合有后端的 Web 应用。 Implicit Flow（隐式模式）：直接返回 id_token，不需要 client_secret，适合纯前端应用或没有后端的场景（如 Chrome Extension）。 参考：Stack Overflow - OAuth Authorization Code vs Implicit\n下面你会在 Web 应用中看到授权码模式的应用，在 Chrome Extension 中看到隐式模式的应用。\n实战：Sign in with Google Google Cloud 项目配置 前往 Google Cloud Console 新建项目。\n关键概念：\n一个 Project 可以对应多个 OAuth Client ID，以适配多种客户端类型（Web, iOS, Android, Extension） 同一个 Project 内，不同 Client ID 生成的用户唯一标识是一样的。也就是说，用户 foo 在 Web 客户端上使用 Gmail 注册得到 UID，在 iOS / Extension 端登录得到的 UID 值相同。 一个 OAuth Client ID 可以生成多个对应的 Client Secret。 Client ID 可以对外公开（实际编码中一定会公开），但 Client Secret 绝对不能对外公开。 Web 应用配置 在 Google Cloud 左侧导航栏进入 APIs \u0026amp; Services → Credentials，点击顶部 Create Credentials → OAuth Client ID：\nApplication type: Web application Name: 随便写 配置 Authorized JavaScript origins：\n本地测试 URI：http://localhost:8787 生产环境域名 URI：https://foo.bar 保存后记录下 Client ID 和 Client Secret。\nWeb 应用编码 官网文档：Google Identity Services\n完整流程：\n[Main Web App] ───▶ Opens Popup ──▶ [Google Auth Server] ▲ │ │ │ Authenticates User └─── postMessage(auth_code) ◄─────────────┘ 分步说明：\n页面渲染 — 渲染登录按钮，传入 client_id 浏览器侧 — Google Identity Services 弹窗，让用户选账号，拿到一次性授权码 code 后端实现 /api/account/oauth/google 接收浏览器 POST 过来的授权码 code 用 code + client_id + client_secret 向 Google 换 id_token 校验 id_token（验签、校验 aud） 写入或更新 account 和 social_account 表 写入浏览器 session cookie 浏览器跳转 — 登录成功，重定向到应用首页 Chrome Extension 应用配置 OAuth Client 的配置可以复用 Web 的，也可以独立新建。复用便于管理，独立便于统计不同客户端的注册登录数据。下面以独立新建为例。\n在 Google Cloud 新建 OAuth Client：\nApplication type: 选 Web application（而非 Chrome Extension） 注意：Application type 选 Chrome Extension 在非 Chrome 浏览器下不通用。如果你的目标是跨浏览器（Edge、Chrome、Safari），应选择 Web application。参考 Chrome Extension OAuth 文档\n保存后记录 Client ID 和 Client Secret。\n生成 Extension 骨架代码后，用 Chrome 打开 chrome://extensions/，勾选 Developer mode，点击 Load unpacked 加载已解压插件，得到 Extension ID。\n重要：Extension ID 和已解压路径有关，路径变了 ID 也会变。如果需要固定 ID，参考 Keep consistent ID。\n修改 OAuth Client 配置，在 Authorized redirect URIs 中添加：\nhttps://\u0026lt;extension-id\u0026gt;.chromiumapp.org/ 基于 Vite 构建时，配置环境变量：\nVITE_SIGN_IN_WITH_GOOGLE_CLIENT_ID=xxx-ooo.apps.googleusercontent.com VITE_SIGN_IN_WITH_GOOGLE_REDIRECT_URI=https://\u0026lt;extension-id\u0026gt;.chromiumapp.org/ 更多参考：wxt-dev GitHub Discussion #505\nChrome Extension 应用编码 参考文档：\nChrome Extension OAuth Google Identity Platform - Chrome Extension Google OpenID Connect MDN - WebExtensions identity API 完整流程：\nExtension Google Backend (/xxx.AccountService/Auth) │ launchWebAuthFlow │ │ │──────────────────────▶ │ consent screen │ │ #id_token=\u0026lt;JWT\u0026gt; │ │ │◄────────────────────── │ │ │ auth({provider:\u0026#34;google;\u0026lt;EXT_ID\u0026gt;\u0026#34;, idToken}) │ │──────────────────────────────────────────────▶│ │ │ GET /oauth2/v3/certs │ │ │◄─────────────────────│ (fetch JWKS) │ │─────────────────────▶│ verify sig + iss + aud + exp │ { jwt } │ │◄──────────────────────────────────────────────│ │ store jwt, reload tab │ │ 分步说明：\n插件渲染登录页面按钮，生成授权链接：\nhttps://accounts.google.com/o/oauth2/auth ?client_id=\u0026lt;VITE_SIGN_IN_WITH_GOOGLE_CLIENT_ID\u0026gt; \u0026amp;response_type=id_token ← implicit flow（无 code，无 secret） \u0026amp;redirect_uri=https://\u0026lt;ext-id\u0026gt;.chromiumapp.org/ \u0026amp;scope=openid email profile 插件调用 browser.identity.launchWebAuthFlow() 弹窗让用户授权\n用户授权后，插件重定向到 https://\u0026lt;ext-id\u0026gt;.chromiumapp.org/#id_token=\u0026lt;JWT\u0026gt;\u0026amp;...\n插件从 URI 提取 id_token，向后端发起 auth() 请求\n后端实现 auth()：\n验证 id_token（JWKS 验签、校验 iss/aud/exp） 写入或更新 account 和 social_account 表 生成自有服务的、经过签名的 JWT，返回给插件后续使用 插件存储 JWT，刷新页面\n实战：Sign in with Apple 基本概念 官网文档：\nSign in with Apple - Getting Started Configuring Your Environment Apple 的账号体系层级关系：\n一个 Apple Developer Account └── 可以生成多个 App ID └── 多个 App ID 可以归为一组 └── 其中一个为 Primary App ID └── 同一个 Primary App ID 下，账户 UID 相同 └── 一个 App ID 可以关联多个 Service ID └── 每个 Service ID 可对应不同的平台（Web, iOS 等） 应用配置 前提：付费订阅 Apple Developer Program。\n进入 Certificates, Identifiers \u0026amp; Profiles 进行配置：\nStep 1 — 新建 App ID\n例如 info.my-domain.app\nStep 2 — 新建 Service ID\n例如 info.my-domain.web\n勾选启用 Sign in with Apple 修改 Web Authentication Configuration： 关联 Primary App ID Register Website URLs： Domains and Subdomains：添加 Web 应用域名，如 foo-bar.ngrok-free.app Return URLs：添加 Web 处理回调链接，如 https://foo-bar.ngrok-free.app/api/account/oauth/apple 注意：链接必须是 HTTPS 开头。本地开发可使用 ngrok 等工具临时映射到公网。个人开发账号最多关联 10 个链接，企业开发账号可关联 100 个。\nStep 3 — 新建 Key\n前往 Auth Keys，用于校验用户授权登录后 Apple 生成的 JWT。\nKey Name：如 my-domain Key Usage Description (optional)：my-domain sign in with xxx 勾选启用 Sign in with Apple → Configure Key → 勾选前面新建的 Primary App ID Step 4 — 完成设置\nDepending on your product, you may need to configure multiple components for Sign in with Apple – from registering domains for Web Authentication to providing email sources to communicate with your users through the Private Email Relay service. Learn more\n应用编码 参考文档：Sign in with Apple REST API\n后端流程：\n/api/account/oauth/apple — 接收登录回调 用户授权登录后得到 code，后端用 code 向 Apple 服务器校验，换取 id_token 使用 Apple 公钥校验 id_token 签名，解释得到账号信息 写入或更新 account 和 social_account 表 常见问题：\n现象 原因 invalid_request / \u0026ldquo;Invalid client id or web redirect url\u0026rdquo; Client ID 或 Redirect URI 配置不匹配 授权后弹窗没有自动关闭 APPLE_OAUTH_REDIRECT_URI 配置错误 如果遇到\u0026quot;发生错误，无法完成你的请求\u0026quot;提示，首先检查 Redirect URI 是否完全一致（包括 HTTPS 前缀、路径、无多余斜杠）。\nApple：跨团队迁移应用和用户 如果你需要将应用和用户迁移到另一个 Apple Developer 账号：\nTransferring your apps and users to another team Bringing new apps and users into your team 总结 本篇从多平台密码管理的痛点出发，完整介绍了如何通过 OAuth + 社交登录解决这些问题。核心要点：\nOAuth 的核心价值：一处认证，多处使用；平台隔离，一处泄露不会波及全部。 信任传递：通过 Google/Apple 等可信第三方，实现跨平台身份验证。 实战要点： Google Web 应用走 Authorization Code Flow（code + secret） Chrome Extension 走 Implicit Flow（直接返回 id_token） Apple 登录需注意 HTTPS + Redirect URI 精确匹配 安全底线：Client Secret 不暴露、JWT 验签、生产环境限制域名。 ","permalink":"https://weblog2.pages.dev/programming/2024-11-01-saas-account-auth-practice/","summary":"\u003cp\u003e继续原理篇的话题。本篇从 OAuth 诞生的痛点开始，完整介绍如何在一个 SaaS 系统中接入 Google 和 Apple 社交登录，覆盖 Web 应用和 Chrome Extension 两种场景。\u003c/p\u003e","title":"SaaS 软件架构设计：接入社交登录实战（Google \u0026 Apple）"},{"content":"在 2015 年前后，我负责的一个千万级用户数据接口服务并没有采用当时最流行的 .net 和 Java 技术栈，而是基于 Python 2.x 实现数据接口和数据更新服务。性能优化相关要点如下。\n测试繁琐但必要 多写单元测试，尽可能覆盖所有对外服务接口。\n除了测试 RESTful 接口外，还可以使用 Selenium 模拟用户访问网页测试页面。\n变量作用域 1class RankItem(object): 2 def __init__(self, name, idx): 3 self.name = name 4 self.idx = rank 5 6 7rank_list = [ 8 RankItem(\u0026#39;a\u0026#39;, 1), 9 RankItem(\u0026#39;b\u0026#39;, 2), 10 RankItem(\u0026#39;c\u0026#39;, 3), 11] 12 13for rank_obj in rank_list: 14 _ = [rank_obj.idx for rank_obj in rank_list] 15 rank_obj.idx = 10 16 break 14 行 列表表达式里面的 rank_obj 变量会覆盖外层 for 循环的变量， 15 行期望修改 rank_list[0].idx ，实际会修改 rank_list[2].idx 。\n解决方法是, 列表表达式里面使用简短、和外层不一样变量名避免覆盖污染：_ = [obj.idx for obj in rank_list]\n或使用生成器代替列表表达式：_ = (rank_obj.idx for rank_obj in rank_list)\n另见\nShort Description of Python Scoping Rules http://stackoverflow.com/questions/291978/short-description-of-python-scoping-rules\nPython 2.7.12 documentation - 4. Execution model https://docs.python.org/2.7/reference/executionmodel.html\n性能瓶颈定位和优化 Python 中各个 Web 框架 profiler 功能大多数是基于 profiler/cProfile 模块实现。\nFlask、Django 中可以使用 中间件(Middleware) 方式，通过非入侵式代码启用 profiler 功能。Flask ProfilerMiddleware 示例 见 https://werkzeug.palletsprojects.com/en/2.0.x/middleware/profiler/\n注意： µs 是 microsecond (微秒)简写, ms 是 millisecond (毫秒)， 1 second (秒) = 100_0000 µs(微秒)， 1 second (秒) = 1000 ms(毫秒)。\n在生产环境，通常有多台 Web 机器，只需要在一台机器上设置随机 dump profiler 文件即可，然后下载到本地分析\n$ python -m pstats samples/POST.api.v1.foo.1277ms.1450101273.prof % sort time calls % stats 15 输出\nMon Dec 14 21:54:34 2015 samples/POST.api.v1.foo.1277ms.1450101273.prof 6962 function calls (5876 primitive calls) in 1.277 seconds Ordered by: internal time, call count List reduced from 492 to 15 due to restriction \u0026lt;15\u0026gt; ncalls tottime percall cumtime percall filename:lineno(function) 1 0.249 0.249 0.249 0.249 /usr/lib/python2.7/json/decoder.py:372(raw_decode) 1278 0.209 0.000 0.209 0.000 {isinstance} 15 0.187 0.012 0.189 0.013 ./champion/models.py:52(get_by_name_en) 574/1 0.159 0.000 0.371 0.371 ./client_msg_parser.py:17(lower_keys) 5 0.141 0.028 0.141 0.028 {_codecs.utf_8_decode} 2 0.120 0.060 0.312 0.156 ./client_msg_parser.py:211(parse_team) 2 0.096 0.048 0.096 0.048 /usr/local/lib/python2.7/dist-packages/kombu/utils/__init__.py:143(uuid4) 1 0.052 0.052 0.054 0.054 /usr/local/lib/python2.7/dist-packages/flask_restful/reqparse.py:85(source) 2 0.043 0.021 0.043 0.021 {method \u0026#39;recv\u0026#39; of \u0026#39;_socket.socket\u0026#39; objects} 1505 0.002 0.000 0.002 0.000 {method \u0026#39;lower\u0026#39; of \u0026#39;unicode\u0026#39; objects} 504/9 0.002 0.000 0.371 0.041 ./client_msg_parser.py:21(\u0026lt;genexpr\u0026gt;) 920 0.001 0.000 0.001 0.000 {method \u0026#39;lower\u0026#39; of \u0026#39;str\u0026#39; objects} 1 0.001 0.001 1.275 1.275 /usr/local/lib/python2.7/dist-packages/flask_restful/__init__.py:569(dispatch_request) 411 0.001 0.000 0.001 0.000 {method \u0026#39;find\u0026#39; of \u0026#39;unicode\u0026#39; objects} 22 0.001 0.000 0.001 0.000 {method \u0026#39;split\u0026#39; of \u0026#39;str\u0026#39; objects} 从上向下阅读； tottime(total time) 0.249 是指 decoder.py 文件 372 行中 raw_decode 函数调用（不包括里面子函数调用）耗时 0.249 秒(second)； 方括号{} 表示 Python 内置模块/内置函数，不能直接优化，只能通过优化业务逻辑减少调用优化； 上面的例子中，需要优化的地方是 ./champion/models.py 文件 52 行，get_by_name_en 函数。\n另外，如果有大量 {method 'recv' of '_socket.socket' objects} 慢日志， 要特别注意存储(数据库缓存没有命中或命中率特别低，数据库冗余 I/O 过多？）是否遇到瓶颈。\ndict 和 list 中查找 在一个大 list 中查找特定元素，比 dict 中通过 key 获取，慢接近 100 倍：\n1import timeit 2 3import sample_item_list 4import sample_item_dict 5 6 7TARGET_ITEM_ID = 3086 8 9def test_list(): 10 for item in sample_item_list.items: 11 if int(item[\u0026#39;id\u0026#39;]) == TARGET_ITEM_ID: 12 return item 13 14def test_dict(): 15 return sample_item_dict.items.get(TARGET_ITEM_ID) 16 17 18if __name__ == \u0026#39;__main__\u0026#39;: 19 print(\u0026#39;list length %s\u0026#39; % len(sample_item_list.items)) 20 eta_dict = timeit.timeit(\u0026#34;test_dict()\u0026#34;, setup=\u0026#34;from __main__ import test_dict\u0026#34;, number=10000) 21 print(\u0026#39;test_dict %f\u0026#39; % eta_dict) 22 eta_list = timeit.timeit(\u0026#34;test_list()\u0026#34;, setup=\u0026#34;from __main__ import test_list\u0026#34;, number=10000) 23 print(\u0026#39;test_list %f\u0026#39; % eta_list) 24 print(\u0026#39;dict faster than list %s times\u0026#39; % int((eta_list / eta_dict))) 输出结果\nlist length 225 test_dict 0.004473 test_list 0.421391 dict faster than list 94 times dict 的实现是 hash，时间复杂度是 O(1)，而 list 是 O(n)，list 越长越慢。\n核心关键模块改用 POSIX C 实现 通常 POSIX C 改写模块要符合以下条件\n频繁调用 重计算(非 I/O) 某业务核心解密纯 Python 实现和 POSIX C 实现模块调用评测\npython2 ./benchmarks/benchmark_x_encryption.py test_x_decrypt_py 131.605892 test_x_decrypt_c 8.931102 近 14 倍的性能提升。\n影响性能的几个重要因素 存储 I/O —— Web 一般应用中九成瓶颈跟存储相关，使用队列+连接池+内存缓存可以缓解 字符串高频创建/复制 —— 有可能导致内存碎片 重计算型 —— 可以使用 C 实现模块或改用 pypy 解释器 ","permalink":"https://weblog2.pages.dev/programming/2016-06-16-notes-on-python2.x-in-production/","summary":"\u003cp\u003e在 2015 年前后，我负责的一个千万级用户数据接口服务并没有采用当时最流行的 .net 和 Java 技术栈，而是基于 Python 2.x 实现数据接口和数据更新服务。性能优化相关要点如下。\u003c/p\u003e","title":"性能优化：Python 2.x 在生产环境千万级用户数据接口服务中使用"},{"content":"我的第一篇文章。\nIntroduction This is bold text, and this is emphasized text.\n","permalink":"https://weblog2.pages.dev/2013-09-14-helloworld/","summary":"\u003cp\u003e我的第一篇文章。\u003c/p\u003e","title":"Hello, World!"}]