├── pages ├── review │ ├── _meta.json │ ├── learn.mdx │ ├── future.mdx │ └── fault.mdx ├── idea │ ├── _meta.json │ ├── millions.mdx │ ├── mvp.mdx │ └── community.mdx ├── grow │ ├── _meta.json │ ├── monetize.mdx │ ├── strategy.mdx │ └── tactics.mdx ├── launch │ ├── _meta.json │ ├── pricing.mdx │ ├── acquire.mdx │ └── feedback.mdx ├── appendix │ ├── _meta.json │ ├── lemonsqueezy.mdx │ ├── capacitor_p1.mdx │ └── stripe.mdx ├── build │ ├── _meta.json │ ├── services.mdx │ ├── design.mdx │ ├── saving.mdx │ ├── llm.mdx │ └── buildstack.mdx ├── _app.mdx ├── update.mdx ├── _meta.json ├── authors.mdx ├── index.mdx ├── toc.mdx └── preface.mdx ├── public └── attachments │ ├── idea │ ├── mvp.png │ ├── brainstorm.png │ ├── userstory.png │ └── podcastindex.png │ ├── other │ ├── ARR.png │ ├── planet.png │ ├── booktitle.png │ └── readonline.png │ ├── grow │ ├── campaign.png │ ├── longtail.png │ ├── trending.png │ ├── engagement.png │ └── highlight.png │ ├── build │ └── langchain.png │ └── launch │ ├── discord.png │ └── tallyform.png ├── next.config.js ├── package.json ├── .gitignore ├── theme.config.jsx └── README.md /pages/review/_meta.json: -------------------------------------------------------------------------------- 1 | { 2 | "future": "对远景的一些规划", 3 | "fault": "最不应该犯的错误", 4 | "learn": "还会坚持干的几件事" 5 | } 6 | -------------------------------------------------------------------------------- /public/attachments/idea/mvp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hardhackerlabs/book/HEAD/public/attachments/idea/mvp.png -------------------------------------------------------------------------------- /pages/idea/_meta.json: -------------------------------------------------------------------------------- 1 | { 2 | "community": "从社区出发", 3 | "millions": "百万美金的利基市场", 4 | "mvp": "MVP MVP MVP" 5 | } 6 | -------------------------------------------------------------------------------- /public/attachments/other/ARR.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hardhackerlabs/book/HEAD/public/attachments/other/ARR.png -------------------------------------------------------------------------------- /pages/grow/_meta.json: -------------------------------------------------------------------------------- 1 | { 2 | "strategy": "到杠杆高的地方去做宣传", 3 | "tactics": "互动、回复、关注长尾", 4 | "monetize": "意料之外的爆发" 5 | } 6 | -------------------------------------------------------------------------------- /pages/launch/_meta.json: -------------------------------------------------------------------------------- 1 | { 2 | "acquire": "最初的 200 个用户", 3 | "feedback": "永远跟用户站在一起", 4 | "pricing": "定价要关注成本" 5 | } 6 | -------------------------------------------------------------------------------- /public/attachments/grow/campaign.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hardhackerlabs/book/HEAD/public/attachments/grow/campaign.png -------------------------------------------------------------------------------- /public/attachments/grow/longtail.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hardhackerlabs/book/HEAD/public/attachments/grow/longtail.png -------------------------------------------------------------------------------- /public/attachments/grow/trending.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hardhackerlabs/book/HEAD/public/attachments/grow/trending.png -------------------------------------------------------------------------------- /public/attachments/other/planet.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hardhackerlabs/book/HEAD/public/attachments/other/planet.png -------------------------------------------------------------------------------- /public/attachments/build/langchain.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hardhackerlabs/book/HEAD/public/attachments/build/langchain.png -------------------------------------------------------------------------------- /public/attachments/grow/engagement.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hardhackerlabs/book/HEAD/public/attachments/grow/engagement.png -------------------------------------------------------------------------------- /public/attachments/grow/highlight.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hardhackerlabs/book/HEAD/public/attachments/grow/highlight.png -------------------------------------------------------------------------------- /public/attachments/idea/brainstorm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hardhackerlabs/book/HEAD/public/attachments/idea/brainstorm.png -------------------------------------------------------------------------------- /public/attachments/idea/userstory.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hardhackerlabs/book/HEAD/public/attachments/idea/userstory.png -------------------------------------------------------------------------------- /public/attachments/launch/discord.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hardhackerlabs/book/HEAD/public/attachments/launch/discord.png -------------------------------------------------------------------------------- /public/attachments/launch/tallyform.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hardhackerlabs/book/HEAD/public/attachments/launch/tallyform.png -------------------------------------------------------------------------------- /public/attachments/other/booktitle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hardhackerlabs/book/HEAD/public/attachments/other/booktitle.png -------------------------------------------------------------------------------- /public/attachments/other/readonline.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hardhackerlabs/book/HEAD/public/attachments/other/readonline.png -------------------------------------------------------------------------------- /public/attachments/idea/podcastindex.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hardhackerlabs/book/HEAD/public/attachments/idea/podcastindex.png -------------------------------------------------------------------------------- /pages/appendix/_meta.json: -------------------------------------------------------------------------------- 1 | { 2 | "lemonsqueezy": "Lemonsqueezy 计费场景支持和常见问题", 3 | "stripe": "Stripe 接入实践", 4 | "capacitor_p1": "Capacitor & Next.js 实战 Part I" 5 | } 6 | -------------------------------------------------------------------------------- /pages/build/_meta.json: -------------------------------------------------------------------------------- 1 | { 2 | "design": "设计与开发一体", 3 | "buildstack": "用方便且擅长的技术栈", 4 | "llm": "关于大模型必须知道的事", 5 | "saving": "省下的都是赚的", 6 | "services": "能用的服务千万别做" 7 | } 8 | -------------------------------------------------------------------------------- /pages/_app.mdx: -------------------------------------------------------------------------------- 1 | import { GoogleAnalytics } from '@next/third-parties/google' 2 | 3 | export default function App({ Component, pageProps }) { 4 | return ( 5 | <> 6 | 7 | 8 | 9 | ); 10 | } 11 | -------------------------------------------------------------------------------- /pages/update.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: 作者 3 | --- 4 | 5 | ### 更新历史 6 | 7 | - 2024-08-12: 新增[「Stripe 接入实践」](/appendix/stripe) 8 | - 2024-06-02:新增[「Lemonsqueezy 计费场景支持和常见问题」](/appendix/lemonsqueezy) 9 | - 2025-03-28: 新增[「Capacitor & Next.js 开发 App 实战 Part I」](/appendix/capacitor_p1) 10 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | const isProd = process.env.NODE_ENV === 'production' 2 | 3 | const withNextra = require("nextra")({ 4 | theme: "nextra-theme-docs", 5 | themeConfig: "./theme.config.jsx", 6 | }); 7 | 8 | module.exports = withNextra({ 9 | assetPrefix: isProd ? 'https://book.hardhacker.com' : undefined, 10 | }); 11 | 12 | // If you have other Next.js configurations, you can pass them as the parameter: 13 | // module.exports = withNextra({ /* other next.js config */ }) 14 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hardhacker-handbook", 3 | "version": "0.2.2", 4 | "author": { 5 | "name": "HardHackerLabs" 6 | }, 7 | "scripts": { 8 | "dev": "next", 9 | "build": "next build", 10 | "start": "next start" 11 | }, 12 | "dependencies": { 13 | "next": "^14.1.2", 14 | "nextra": "^2.13.4", 15 | "nextra-theme-docs": "^2.13.4", 16 | "@next/third-parties": "^14.2.2", 17 | "react": "^18.2.0", 18 | "react-dom": "^18.2.0" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /pages/_meta.json: -------------------------------------------------------------------------------- 1 | { 2 | "index": "简介", 3 | "update": "更新", 4 | "authors": "作者", 5 | "toc": "目录", 6 | "preface": "前言", 7 | "idea": "灵感", 8 | "build": "构建", 9 | "launch": "发布", 10 | "grow": "增长", 11 | "review": "复盘", 12 | "appendix": "附录", 13 | "about": { 14 | "title": "更多", 15 | "type": "menu", 16 | "items": { 17 | "podwise": { 18 | "title": "Podwise AI", 19 | "href": "https://podwise.ai" 20 | }, 21 | "homepage": { 22 | "title": "HardHackerLabs", 23 | "href": "https://hardhacker.com" 24 | } 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # nyc test coverage 18 | .nyc_output 19 | 20 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 21 | .grunt 22 | 23 | # node-waf configuration 24 | .lock-wscript 25 | 26 | # Compiled binary addons (http://nodejs.org/api/addons.html) 27 | build/Release 28 | 29 | # Mac 30 | .DS_Store 31 | 32 | # Dependency directories 33 | node_modules 34 | jspm_packages 35 | 36 | # Optional npm cache directory 37 | .npm 38 | 39 | # Optional REPL history 40 | .node_repl_history 41 | .next 42 | -------------------------------------------------------------------------------- /pages/review/learn.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: 还会坚持干的几件事 3 | --- 4 | 5 | Podwise 到今天,还远没有成功,但我们认为产品应该度过了 0-1 的 PMF 阶段。过程中运气也占了很大的比例,我们在这里也不是在尝试总结,而是在充分讨论后,我们认为:如果还有下一个产品要做,我们会坚持的几件事: 6 | 7 | 首先一定是从社区出发,经过「硬地骇客」播客和 Podwise 的经历,我们对于 Sahil Lavingia  的书 [The Minimalist Entrepreneur](https://www.amazon.com.au/Minimalist-Entrepreneur-Great-Founders-More/dp/0593192397) 认识更加深刻,通过尽可能的与社区成员交流,解决他们的问题,对于产品在第一时间找到客户非常重要。独立开发这件事最忌讳的就是想象用户需求,很多人在不断的想象需求,构建,放弃中逐渐迷失了。所以,深入社区是一定要做的。 8 | 9 | 另外非常重要的一点是快速发布,我们很庆幸在 beta 测试的时候,客户给我们提了计费方式的问题,我们才在这个过程中对方案进行了修改,并且添加了趋势页面,如果再过两个月,我们做的更成熟直接发布,那可能会损失很大一部分流量。所以,确认 MVP,让产品尽快与客户见面,寻求反馈,快速迭代,是我们永远追求的链路。 10 | 11 | 最后,大胆谈钱。产品发布的第一天开始就一定要有付费选项,想好自己的商业模式。如果是 AI 产品一定要充分计算自己的成本,好的 AI 产品毛利要高于 60%。尽量不要贱卖自己的产品,9 元人民币和 9 美元之间有 7 倍的差距,而不同地区的最低时薪之间差距巨大。我们已经作为独立开发者,算是数字游民了,可以赚高时薪地区的钱,做地理套利,那千万不要错过。 12 | -------------------------------------------------------------------------------- /pages/launch/pricing.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: 定价要关注成本 3 | --- 4 | 5 | 在做产品之前,我们一致认为 LLM 带动的 AI 热潮,是 SaaS 软件很好的一个发展契机。因为大家已经习惯 LLM 有成本,所以在产品中叠加 LLM 的能力,可以使以前没有收费能力的软件转上变现的快车道。所以从定价的逻辑上,除了参考友商之外,我们核心还特别从成本角度考虑了定价。 6 | 7 | 在对标产品上,我们选择了两个:[Otter.ai](http://otter.ai/) 和 Snipd 8 | 9 | [Otter.ai](http://otter.ai/) 是一款 AI 语音转文字的工具,并不专注在播客上,它的定价是 $16.99 转录 1200 分钟音频。 10 | Snipd 是一款 AI 播客播放器,它的定价是 $9.9 转录 15 小时,约等于 10 期播客。 11 | 12 | 而在我们核算过使用 AI 的成本后,一期一小时的节目,我发现大概需要 $0.3-$0.5 的成本。Snipd 已经有点压线了,但播客按小时定价其实不是一个很好的定价策略,因为播客的时长不固定,很难预测是否可以转录,如果在时长上出现问题,对用户体验的伤害还是挺大的。所以思考再三,我们决定把定价制定为按期数来定价。按期数定价的好处是,将确定性留给用户,不确定性留给平台。如果按照 Snipd 的时长换算,我们平价的话也就是提供 $9.9 转录 10 期播客。 13 | 14 | 但这里我们想耍个小聪明,假如每个人都转录 10 期内容的话,且用户之间有重复,那因为我们已经转录过了,那我们就可以赚一期。但想了想似乎用户一个月只能看 10 期内容有点少,我们以工作日每天一期为目标,将转录期数定为了 20 期。这样假如用户之间的重复率达到 50%,那么我们还是可以与 Snipd 一样提供 10 期左右的成本,来完成。 15 | -------------------------------------------------------------------------------- /pages/review/future.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: 对远景的一些规划 3 | --- 4 | 5 | Podwise 在当前的主要形态是 Web 版,虽然有 Mobile first 的设计,可以在手机上直接打开,但毕竟不像 app 那样方便。所以移动版的 app 是我们未来首要的目标。另外还有一个重要的原因是,我们发现在平台营销时,大量的用户会问在哪里下载?而且还出现了用户在 App Store 中购买了同名的 app 发现不是我们的产品,过来找官方账号抱怨的。而且移动版可以至少帮我们做两件事,第一是扩大 Podwise 的应用场景,之前主要做笔记,现在也可以听了。第二是扩大产品的曝光范围,多平台的曝光是独立开发产品非常重要的,因为我们只需要 10000 个忠实用户就够了。类似于游戏的全平台发布,如果成本不高的话,强烈建议做多平台发布。 6 | 7 | Podwise 当前的客户组成还是华人为主,包括大陆和台湾地区,海外用户的比例当前还比较低。所以从营销上我们除了继续深耕国内的各大平台之外,我们首要的目标还是在海外建立自己的影响力。其中就包括 X、Reddit、Tiktok 和 Instagram 等平台。除了北美市场是 Podwise 出海的重点之外,东亚也是我们关注的重点,因为儒家文化圈对于学习的重视程度是相同的。在营销上还是有非常大的挑战的,我们也会尝试是用投流的方式来宣传产品,只要投流是正收益,我们就可以持续投流,将客群越滚越大。 8 | 9 | 当然如果要扩展海外市场的话,除了在营销上需要跟进,还有一个很重要的是本地化。尤其是在笔记盛行的日韩地区,我们需要做界面、包括内容的国际化。由于全球 95% 的头部播客都是英文播客,所以东亚语言都需要做英文到当地语言的翻译,这样可以满足很多语言学习者的需求,尤其在东亚英语不是母语的基础上。当前 Podwise 的中文用户会看到中文和英文内容,未来日韩的用户也需要看到他们当地语言和英文的组合。这是对他们最习惯的内容。 10 | -------------------------------------------------------------------------------- /pages/authors.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: 作者 3 | --- 4 | 5 | 本书的作者是「硬地骇客」社区,硬地骇客即 Indie Hacker 的意译。 6 | 7 | 硬地骇客社区核心成员由三位拥有10年+互联网创业者组成,我们关注前沿科技,分享创业故事,打造 “超级个体”,寻找利基市场,构建小而美的生意,同时也希望和广大 Hacker 一起探讨技术、产品和商业之美。我们正在努力探索一条能够同时实现财务、时间和身体自由的道路,希望能够通过 Podcast 将我们的故事分享给大家。 8 | 9 | 硬地骇客是一群追求自由充实的生活并喜欢挑战的 Builder,热爱技术,构建产品,崇尚依靠产品驱动的增长方式构建出自己的小生意。 10 | 11 | 如果你也喜欢这些信条,欢迎加入我们「硬地骇客」的官方星球并参与本书的讨论: 12 | 13 | 我们提供如下服务: 14 | 15 | 1. 全年不少于 24 篇专题分享,内容包括但不限于 灵感 - 构建 - 发布 - 增长 - 复盘 等产品关键环节。 16 | 2. 全年 50 期左右的「硬地骇客」播客文章版内容以及参考资料,通过文章的阅读体验可以在收听节目之后方便快速检索播客内容。 17 | 3. 为支持社群会员的产品发展,社群为每位会员提供「硬地骇客」播客节目 Show Notes 广告位一次,价值 ¥500,可为产品提供高质量的站点外链,提升搜索引擎排名。 18 | 4. 不定期的黑客马拉松赛事,优胜者可以获得包含现金奖励与项目孵化等合作项目支持。 19 | 20 |

21 | 22 | 23 | 24 |

25 | 26 | 我们坚信:“每个人都应该拥有一个小生意”! 27 | -------------------------------------------------------------------------------- /pages/index.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: 两个月 $12000 ARR 实践之路 3 | --- 4 | 5 |

6 | 7 | 8 | 9 |

10 | 11 | ### 关于本书 12 | 13 | 本书是关于 [Podwise](https://podwise.ai) 产品从灵感到变现过程的忠实记录以及复盘。 14 | 15 | [Podwise](https://podwise.ai/) 是一款专为播客听友设计的 AI 知识管理应用。 利用 Podwise 平台,你只需 follow 喜欢的播客,比如「硬地骇客」,当有节目发布后,Podwise 会通过 AI 对播客内容进行 转录、提取、总结、分析 等一系列操作,帮你掰开了揉碎了硬核的播客知识。同时与 Notion、Obsidian、Logseq 和 Readwise 等平台的打通,嵌入你的知识管理工作流,协同在其他渠道的包括新闻,Newsletter,Blog 等内容,帮你完善第二大脑 🧠。 16 | 17 | Podwise 今天的成绩远称不上成功,可能勉强算是找到 PMF (Product-Market Fit),所以这不是一份权威指南,内容仅供参考。 18 | 19 | ### 版权声明 20 | 21 | 本作品采用 CC BY-NC-ND 4.0 进行许可。 22 | 23 | 这意味着您可以自由分享、复制和传播本作品的整体或部分内容,但须遵守下列条件: 24 | 25 | * 署名 — 您必须给出适当的署名,提供指向本许可协议的链接,同时标明是否(对原始作品)作了修改。您可以用任何合理的方式来署名,但不得以任何方式暗示许可人为您或您对本作品的使用提供认可。 26 | * 非商业性使用 — 您不得将本作品用于商业目的。 27 | * 禁止演绎 — 如果您再混合、转换或者基于本作品创作,您不可以分发修改后的作品。 28 | 29 | 无视上述任何条件,都构成违反许可协议,根据法律将受到相应的处罚。 30 | -------------------------------------------------------------------------------- /pages/grow/monetize.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: 意料之外的爆发 3 | --- 4 | 5 | 在黑五到来之前,我们收到了一封来自竞对的 **65% OFF** 的邮件,其实没有思考太多,下意识告诉我们,黑五确实该做一次优惠,而且折扣力度应该是前所未有的,我们的选择是跟随。 6 | 7 | ![campaign](/attachments/grow/campaign.png) 8 | 9 | 结果在即刻 和 X 平台上形成了非常好的传播效应,(小红书因为审核的关系无法透出优惠信息,所以不太利于传播。)在短短三天的时间里面,就卖出将近 150 份订阅,其中绝大部分是年订,一举突破了 $10000 ARR,事后来看这是一次非常成功的活动,但回过头来看确实非常偶然。我们试图找到一些偶然背后的必然,其中非常重要的一项应该是定价: 10 | 11 | Podwise 的定价是 $9.9 一个月,年订的话折算到月 $5.9,按人民币计价的话超过 ¥40 人民币,这对于国内用户来说是有些偏高的,对比国内付费渗透率较高的应用来说,年订 ¥298 已经是很多产品上限,可以注意网盘和视频会员,他们定价大多都在 ¥198-¥298 之间。而 Podwise 的年订价格是超过 ¥500 的,这对于一部分是是有一定疑虑的,当然这是针对国内市场的定价问题,对于国外来说,$9.9 的定价本身不存在问题。 12 | 13 | 通过 3.5 折左右的折扣,我们将 Podwise 的年订价格降到了 ¥290 左右,这样结合用户对于 AI 高成本的预期,比较成功的实现了一次势能转换,为什么说是势能转换,主要是将前期的一些品牌曝光度,美誉度等等在一次活动中的集中变现。 14 | 15 | 在活动结束之后,我们发现 Podwise 的增长曲线比活动之前来的更加陡峭一些,明显是受众增加而带来的订阅增长,我们分析这里面一定有来自活动之后订阅用户的二次传播,这些都是免费的流量。 16 | 17 | 所以我们也认为在产品获得一定曝光后,适当通过活动甚至互动赠送的方式发展种子用户,也不失为一个获得客户的好方法。当然,所有的前提都是产品足够惊艳。 18 | 19 | 可以看到,Podwise 本身订阅用户的爆发式增长其实也不是偶然,前期大量的宣传工作对于品牌的建立和曝光起到了非常关键的作用,当然产品自身的美誉度更是产品能够多次传播的根基,首先是酒香,其次才是让巷子不要那么深。当然现在 Podwise 的订阅量还非常少,后续我们还需要一步一个脚印,让对 Podwise 有需要的用户能更方便的触达产品。 20 | -------------------------------------------------------------------------------- /pages/launch/acquire.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: 最初的 200 个用户 3 | --- 4 | 5 | 经过两个月的开发,我们 MVP 核心功能已经实现,觉得是时候进行一波邀请测试了。在邀请平台的选择上,一方面我们之前已经有 Waitlist 积累的将近 500 多个用户,可以通过邮件召回,另外我们也希望通过平台发声让更多的人了解我们。作为程序员,我们首选的宣发渠道是 V2EX,主要原因是 V2EX 是少数允许新产品进行免费宣发的平台,尤其是通过发送邀请码的方式,参与的热情会特别高。 6 | 7 | [[ Public Beta ] Podwise,专为 Podcast 听友设计的 AI 学习应用 🎁](https://www.v2ex.com/t/965212) 8 | 9 | 在 V2EX 上发了邀请测试的帖子后,很快我们就邀请到了 100 个用户,因为我们不想一下进来这么多人,后来甚至不得不暂停了邀请进程。 10 | 在帖子发布的同时,我们也为 500 个在 Waitlist 的客户邮箱发送了测试邀请码,最终的召回率到 20% 左右,有点低于我们当时的预期,事后分析总结了几点: 11 | 12 | - 一个是邮件的内容可能被当成 Spam 邮件或者被加入营销邮件的列表中,导致流失。 13 | - 另一个是我们对于邮箱召回的期待过高,因为毕竟会经历 收件 - 打开 - 访问 - 注册 四个步骤才能转化一个注册用户,而业界网站的访问-注册转化通常在 15%-30% 之间,举个例子:肯定有人刚看到 Landing Page 的时候觉得特别需要,但看到营销邮件的时候热情已经消退了,所以也就流失了。凑热闹的也肯定会有了。所以邮箱的的转化率肯定是不如 1:1 论坛转化的,从数据上来说也可以解释。 14 | 我们在这次邀请之后,在 Amazon SES 上加入了邮件的追踪,用来计算转化率,更能数据化的分析用户到底在哪个环节流失了,推荐大家在使用邮件系统的时候都开启这个服务。 15 | 16 | 但其实在我们获取用户的过程中还是犯了个错误,我们只关注了渠道的易用性,没关注渠道的契合性。V2EX 作为以开发者,产品经理为主要活跃群体的平台,本身与 Podwise 与播客听众并不是特别契合,我们当初之所以选择 V2EX 只是因为它对产品发布友好,如果重新再来一次的话,我们可能会选择在即刻的“一起听播客”群组和小红书招募用户,这样会用户群会更聚焦一些,然后对产品的帮助也会更大。 17 | 18 | 总之,在 Podwise 在通过 Landing Page 和 V2EX 社区的宣发后,成功获得了最初的 200 个用户,为我们能够持续构建服务开了个好头。 19 | -------------------------------------------------------------------------------- /pages/build/services.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: 能用的服务千万别做 3 | --- 4 | 5 | Podwise 能够快速构建上线,背后用到了很多优秀的三方 SaaS 服务。我们挑选它们的理由最重要的一点是:物美价廉(甚至最好是免费的)。有部分在前面的内容中也有提到,在这里我们再为大家做一下总结。 6 | 7 | - Vercel:一站式的应用部署托管运维平台,主要面向前端技术栈,一方支持 NextJS 。免费计划较为慷慨,在项目启动初期完全够用。 8 | - Zeabur:来自国内创业团队的部署托管运维平台。与 Vercel 不同的地方在于并不基于 AWS lambda 实现,并且支持容器方式部署。我们将 Zeabur 作为补充部署部分无法在 Vercel 上部署的服务。 9 | - Supabase:Firebase 的开源替代,提供 authentication 、database(postgresql)、storage 、edge functions 等一系列能力。SaaS 版本提供了非常慷慨的免费额度。我们仅使用了 Supabase 的 authentication 部分,实现了 Podwise 的登录注册功能。 10 | - PlanetScale:强大易用的 MySQL 云服务,我们的主数据源。PlanetScale 提供几乎 0 维护的在线 MySQL 实例,开发者无需关心备份、扩缩容等问题,只需要专注在业务开发。除了容灾、自动备份、读写分离等基础功能,PlanetScale 还提供像不停机不损失数据的 schema 回滚这样的高级功能。可惜 PlanetScale 曾经慷慨的 Free Plan 已经一去不复返了,现在不再提供免费额度,但我们没有迁走选择继续使用。如果你想寻找免费替代品,那可以考虑一下 Neon 和 Supabase ,它们都基于 PostgreSQL 。 11 | - Clarity:微软出品的站点热力图和用户行为录制重放工具。Clarity 能很好的帮助开发者分析用户行为,发现产品的交互问题和错误。并且它是完全免费的,谢谢微软爸爸。此外 Clarity 还可以 connect 你的 Google Analytics ,在呈现 GA 数据的同时,还能筛选出对应流量背后的用户会话回放。 12 | - Tallyform:体验优秀、简单易用的表单工具。绝大多数功能免费且没有用量限制,用作反馈收集和 bug 报告等场景都非常方便。 13 | - Google Analytics:这个相信就不用过多介绍了,Google 出品的流量分析工具,当然也是免费的。 14 | - BetterStack:监控平台,可以实时监视应用接口或页面,收集日志,并根据配置的规则进行告警。可以非常方便的和 Vercel 整合,从 Vercel 获取日志。不过 BetterStack 的免费用量较少。 15 | - Sentry: 前端监控平台,主要用于监控错误信息,并配套有堆栈、操作回放、性能数据等的监控。可以非常方便的和很多前端框架,包括 NextJS 整合。Sentry 除了错误信息外的免费用量较少,但错误信息的免费用量完全够用。不过我们并不是很推荐持续开启 Sentry ,因为大部分错误信息可能都是无害的,反而会牵扯到你的注意力。我们选择阶段性的开启 Sentry 来观察错误并提升产品质量。 16 | -------------------------------------------------------------------------------- /pages/idea/millions.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: 百万美金的利基市场 3 | --- 4 | 5 | 在选定产品方向之后,我们做了一次市场调研。根据 [eMarketer](https://www.insiderintelligence.com/content/global-podcast-listener-forecast-2021-2025) 和 [PodcastIndex](https://podcastindex.org/) 的数据, 2024 年全球播客市场的听众人数已经超过 5 亿人,但并不是说播客听众都会是 Podwise 的用户,因为播客的核心功能还是陪伴,而当前市面上大部分的播客也都是主打陪伴属性的,Podwise 所服务的硬核知识类的播客主要集中在 创业、投资、商业、历史和健康等类目。 6 | 7 | 我们假设全球 5 亿的播客受众有 10% 左右的重度用户,而这 10% 的重度用户中又有 10% 会喜欢收听 “硬核知识” 播客,那就是 500 万左右的受众。那么 Podwise 只要能够转化 1% 左右的核心用户,就可以拿到 5 万个订阅用户,按年算 ARR 也有 350 万美金左右的市场,按毛利 60% 计算市场回报也超过 200 万美金。当然,上面所有的假设都是最保守的数字,对我们来说,粗算下来这是一个能支撑小团队的市场规模,那我们就可以干。 8 | 9 | 我们回过头来看看播客市场的供给,根据 [PodcastIndex](https://podcastindex.org/) 的数据,当前还在活跃的播客有 400 万以上。 10 | 11 | ![podcastIndex](/attachments/idea/podcastindex.png) 12 | 13 | 而平均一个人收听播客的时长为 6-7 小时,所以大量的播客其实是无法被用户收听到的。同时我们也注意到一个现象,像 Joe Rogan、Huberman、Lex Fridman 等头部播客的时长越来越长,动辄 3-4 个小时。所以注定有海量的播客内容其实是无法被广泛消费的。那么在 400 万活跃播客 和 每人最多 6-7 小时的收听时长之间就存在一个巨大的供给与消费的鸿沟,所以这里存在机会。而 Podwise 为这种鸿沟提供了解决方案,就是加速消费播客内容。当然方式也不止这一种,在这种情况下,在供给端,播客主理人们也会想尽办法来让自己的内容更多的触达给用户,那这里面一定也存在机会。 14 | 15 | 另外我们也横向对市场上 Podwise 的上下游产品做了些调研,其中就包括 Readwise,Readwise 作为当前最火的第二大脑中枢型产品,在创始人 2022 年的[分享](https://twitter.com/deadly_onion/status/1578831165561049089)中我们得知 Readwise 的 ARR 为 350 万美金,合理推测现在应该已经突破 500 万美金 ARR,用了将近 7 年的时间。如果我们也能像 Readwise 一样 Scale 顺利的话,合理推测,我们会在 3 年左右的时间达到 100 万美金 ARR。这对小团队来说也会是一个比较舒服的状态。 16 | 17 | 在经过这番调研过后,我们认为这个市场规模是足够支撑我们团队发展的,并且这个规模可能还是独立开发者们选择市场的一个甜蜜点。再小的话不足以养活一个团队,大一些的话将来一定会面临巨头的竞争,因为我们缺乏团队和技术的优势,所以有极大可能在竞争中落败。 18 | -------------------------------------------------------------------------------- /pages/toc.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: 目录 3 | --- 4 | 5 | 全书以产品的全生命周期划分为 5 个部分: 6 | 7 |
8 | 💡 灵感 9 | 10 | * [从社区出发](https://book.hardhacker.com/idea/community) 11 | * [百万美金的利基市场](https://book.hardhacker.com/idea/millions) 12 | * [MVP MVP MVP](https://book.hardhacker.com/idea/mvp) 13 |
14 |
15 | 🛠 构建 16 | 17 | * [设计与开发一体](https://book.hardhacker.com/build/design) 18 | * [用方便且擅长的技术栈](https://book.hardhacker.com/build/buildstack) 19 | * [关于大模型必须知道的事](https://book.hardhacker.com/build/llm) 20 | * [省下的都是赚的](https://book.hardhacker.com/build/saving) 21 | * [能用的服务千万别做](https://book.hardhacker.com/build/services) 22 |
23 |
24 | 🚀 发布 25 | 26 | * [最初的 200 个用户](https://book.hardhacker.com/launch/acquire) 27 | * [永远跟用户站在一起](https://book.hardhacker.com/launch/feedback) 28 | * [定价要关注成本](https://book.hardhacker.com/launch/pricing) 29 |
30 |
31 | 💸 增长 32 | 33 | * [到杠杆高的地方去做宣传](https://book.hardhacker.com/grow/strategy) 34 | * [互动、回复、关注长尾](https://book.hardhacker.com/grow/tactics) 35 | * [意料之外的爆发](https://book.hardhacker.com/grow/monetize) 36 |
37 |
38 | 🤔 复盘 39 | 40 | * [对远景的一些规划](https://book.hardhacker.com/review/future) 41 | * [最不应该犯的错误](https://book.hardhacker.com/review/fault) 42 | * [还会坚持干的几件事](https://book.hardhacker.com/review/learn) 43 |
44 | -------------------------------------------------------------------------------- /pages/launch/feedback.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: 永远跟用户站在一起 3 | --- 4 | 5 | 建立用户反馈一般有三种渠道,邮件反馈、论坛反馈和在线聊天反馈。从实时程度上排序:在线聊天> 论坛 > 邮件,但从效率上来说却是反过来的:在线聊天 < 论坛 < 邮件。原因是,在越异步的平台上,人们越倾向于在一次交互中提供尽可能多的信息,在这种背景下,邮件是期望交互次数最少的平台。这对于独立开发者们很重要,因为越少的交互次数则意味着越少的上下文切换,这样效率会提高很多。且在一天的时间分配上,还需要有很多时间在营销和开发中。 6 | 7 | Podwise 在一开始的社区反馈上选择了 Discord 社群的方式,Discord 是一个兼具论坛与在线聊天的用户反馈渠道。因为之前是从游戏社群发展而来的,所以它的语音和聊天能力很强,但在后续的发展中,Discord 逐渐开始支持用户在其平台上打造社区,其中最重要的特性是可以将 Channel 建成一个 Forum,这样就可以避免重要的信息被海量的聊天记录覆盖。在社区开始时,Podwise 就建立了几个基本的 Forum,包括 feature-request、bugs、help 还有 integration 等。从结果看,Discord 的 Forums 极大的促进了我们产品的正向循环,让我们与用户离得更近。 8 | 9 | ![Discord](/attachments/launch/discord.png) 10 | 11 | 但在执行了一阵之后,我们发现大家对于功能的相关信息会反馈回 Discord,但是对于播客和节目的内容侧,却鲜有反馈,但从我们自己人工检测来看,有些节目的逐字稿和脑图等还是会有 bad case 的情况存在,甚至在前期有完全不可用的节目出现,但只能靠我们自己人工发现了。 12 | 我们分析后发现,用户发现问题和到 Discord 反馈还是存在诸多不便,首先不是每个人都安装了 Discord。其次,每个人都有惰性,如果出现问题,大部分人都是不愿意说的,尤其是还需要操作的情况下。 13 | 14 | 针对这样的情况,我们在 Podwise 平台播客节目的详情页添加了一个反馈按钮,让用户可以没有任何心理负担的反馈他对于节目的真实感受。果然上线之后立竿见影,就收到了用户的评价。后续为了继续提升反馈率,我们又让反馈按钮偶尔抖动一下以引起用户的注意,这个改动过后,我们收到的反馈确实显著提升,而且反馈是多样化的,有 5 星也有 1 星,这个执行又让我们发现了很多 corner case,显著提升了产品的可用性。 15 | 16 | 播客节目的反馈背后我们使用 Tallyform 来作为支撑: 17 | 18 |

19 | Tallyform 20 |

21 | 22 | 当用户点击反馈按钮后,我们会弹出一个菜单让用户选择本期转录的质量以及他的建议,建议可以为空,所以在最短路径下,用户只需要给出星级评价就可以了。用户在提交表单时,我们会默认带上本期节目的 id 以及用户的邮箱。这样我们可以在用户反馈后,可以通过邮件的形式来给用户反馈我们后续的改进,以提高用户的忠诚度,甚至是挽回用户。 23 | 24 | 总的来说,通过 Discord 社群和邮件方式,当前用户的反馈通路是比较顺畅的,对我们产品的迭代很有帮助。 25 | 26 | 可能会有朋友会问,为什么不建微信群和 Telegram,除了我们刚才聊到的效率问题之外,还有一个比较大的运营问题,因为用户群是一个所有人都可以发言的地方,如果你没有十足的运营经验,群的氛围会比较难以把控,虽然他非常容易触达,但作为产品方也可能被反噬,所以做好万全的准备再考虑群这种形式的运营,否则先用异步沟通为主。 27 | -------------------------------------------------------------------------------- /pages/preface.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: 前言 3 | --- 4 | 5 | 我们写这篇小册子主要是为了记录 Podwise 构建的完整经过,我们尽量忠实记录。虽说成功不可复制,但我们会尽量还原当时选择的依据以及事后的结果,所以这更像是一个复盘故事,而不是成功教科书。 6 | 7 | ### Podwise 是什么? 8 | 9 | [Podwise](https://podwise.ai/) 专为播客听友设计的 AI 知识管理应用。 在 Podwise 平台,只需 follow 你喜欢的播客,比如 「纵横四海」,当有节目发布后,Podwise 会通过 AI 对播客内容进行 转录、提取、总结、分析 等一系列操作,帮你掰开了揉碎了硬核的播客知识。同时与 Notion、Readwise 等平台的打通,嵌入你的知识管理工作流,协同在其他渠道的包括新闻,Newsletter,Blog 等内容,帮你完善第二大脑 🧠。 10 | 11 | ### 我们为什么要做 Podwise? 12 | 13 | 在经历了 10 年企业服务创业的洗礼后,我们决定从 to B 的生意中跳脱出来。也是因为机缘巧合的关系,有幸拜读了 Gumroad 创始人 Sahil Lavingia  的书 [The Minimalist Entrepreneur](https://www.amazon.com.au/Minimalist-Entrepreneur-Great-Founders-More/dp/0593192397),让我们觉得,做独立开发者似乎也是一条不错的路。 14 | 15 | 在经历几个月的 Gap 之后,我们以播客的形式开始重新切入市场,制作了一款针对 Indie Hacker 的播客「硬地骇客」,名字取自 Indie Hacker 的中文音译,“硬地”也充分表达了独立开发本身的难度极高。 16 | 17 | 当然为什么制作「硬地骇客」,与这两年互联网、科技行业所面临的业绩下滑和裁员的大环境也有很大的关系,我们预计 Indie Hackers 群体会因为这些因素大幅增加。再叠加 AI 产业爆发,尤其是 ChatGPT 对于开发者群体效率的大幅加成,好像让这件事更成为了一种趋势。我们的目标很简单,希望在这波大潮中,让独立开发者变成生意人,不再用爱发电。当然也包括我们自己。 18 | 19 | 所以「硬地骇客」播客的成长其实是伴随着我们团队 Learn in Public 一起的,在经过三个月的播客节目中,我们制作了几期爆款内容: 20 | 21 | [EP2 从个人兴趣到财务自由:独立开发者也可以这样做开源](https://www.xiaoyuzhoufm.com/episodes/6418304073768bea35ee47d2) 22 | 23 | [EP7 大厂程序员构建 “小生意”,更加从容应对裁员潮](https://www.xiaoyuzhoufm.com/episodes/6446499294d78eb3f74a01d8) 24 | 25 | [EP9 提供一点搞钱小思路:三个独立开发者的创业故事](https://www.xiaoyuzhoufm.com/episodes/6458e5ba7d934b8505efa2cf) 26 | 27 | 小宇宙的订阅数很快突破了 3000,我们也开始思考下一步的计划,利他的同时也需要利己,我们一直认为用爱发电一定是不持久的,做服务需要大胆谈钱。由于播客的整体体量并不是很大,所以单靠广告收益是不现实的。所以我们目标停留在两条: 28 | 29 | 1. 知识付费-出教程如何独立开发 30 | 2. 自己做产品出海赚钱 31 | 32 | 但第一条很快被我们否掉了,因为我们自己也没有独立开发的作品,虽然我们在企业服务创业 10 年,有很多大型的企业级软件作品,也有很多产品的经验,但独立开发且 to C 的产品确实没有。这似乎不是一个对口的背书。再加上知识付费赛道因为经济原因整体大盘都下滑,在当前时段似乎不是一个很好的时间点。所以思考再三,我们还是选择了做产品这个明显更难的路,希望借此机会也给自己留下一个作品。 33 | -------------------------------------------------------------------------------- /pages/idea/mvp.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: MVP MVP MVP 3 | --- 4 | 5 | 产品目标确定之后,我们优先需要确认用户故事。即我们需要给有听播客、记笔记需求的用户,设计一个路径,能够让他们达成目标。 6 | 7 | 在这里我们使用了一个 User-Story Mapping 的图表,它是敏捷团队在长期规划用户故事中所创造出来的一个工具,能够帮助我们更好的思考用户的使用路径。这个工具要使用起来非常简单,它把用户的行为划分为三个层级,即:活动、步骤和细节,他们三者是依次拆解的关系。即通过活动来拆解具体的步骤,再根据每个步骤来确认细节。当然工具只是辅助我们进行思考的工具,核心还是业务,用户到底是如何使用产品的? 8 | 9 | 我们尝试描绘一下用户使用的过程: 10 | 用户被 landing page 的展示内容所吸引,通过 Google 直接登陆系统,但这时候因为用户没有处理过任何节目,所以无法查看 AI 处理过的播客节目。所以系统需要引导用户来转录内容,用户可以直接搜索节目来处理,或者用户通过搜索 Podcast,来找节目并处理。因为节目的处理需要 3 分钟起步的时间,所以这里需要异步通知用户。通知之后用户进入平台看到 AI 处理的内容,包括 总结、大纲、脑图、金句等,当然最重要的是逐字稿,这个是所有内容能产出的基石。在 AI 产出笔记之后,用户可以选择将 AI 笔记推送至自己的笔记本中,比如 Notion 和 Readwise 等。这样就完成了一次完整的用户旅程。当然后续用户可以通过订阅 Podcast 的形式,即时收到新的节目中已处理的笔记。但这属于优化体验了,我们上面描述的这个过程是一个用户要完成 AI 处理播客的必经之路,不能缺乏其中任何一项。 11 | 12 | 做完用户故事的描述,我们尝试将故事整理进 User-Story Mapping 表中,在活动层级中,我们将用户划分为三大主要的活动:Choose a Plan -> Subscribe Podcast -> Share Knowledge。同时也为细节设置了优先级,比如在 Be User 中,我们可以让用户用 Google 和 Email 注册,但用 Apple 的话就可以放在后续有时间再实现。根据我们讨论过后,就形成了下面这张 Mapping: 13 | 14 | ![userstory](/attachments/idea/userstory.png) 15 | 16 | 大家可以发现,我们在思考用户活动的时候用的都是必备路径,而不是事无巨细的全量路径。那是因为,我们不可能在一开始就想清楚产品的全貌,好产品都是与用户磨合出来的。而我们在产品初期,最重要的事情就是快速交付 MVP(Minimum viable product) 最简可行产品,因为在这时候,所有的东西都是我们脑子构建出来的,没有任何用户参与。所以我们需要尽快让用户能够参与进来,来帮助我们验证产品是否真的可行。而构建 MVP 的核心,则需要我们高度提炼用户故事的必备路径,这里有张经典的 MVP 图: 17 | 18 | ![mvp](/attachments/idea/mvp.png) 19 | 20 | 我们的目标是制造一个载具,所以在迭代的过程中每个版本都要交付能载人的工具,而不是每个版本都制造一个零件,等到最后再拼起来。因为长时间的交付,极有可能出现我们规划的是给用户造辆车,但其实用户真正想要的是船,那整个产品可能就需要在 6 个月后完全从头转型,那就意味着要完全失败了。 21 | 22 | 另外如果是工具类的产品,比较忌讳的就是一上来就大而全。大而全的产品本质上还是没找到产品的卖点,而大而全就是最偷懒的做法,但往往海外用户不喜欢大而全。 23 | 24 | 如果你在用户故事精简过后,在第一个 MVP 中没有找到太多产品亮点,那么可能要思考一下这个产品是否值得做?产品在市场上到底有什么差异性?没有差异性就很难让用户产生记忆点,而记忆点就没有品牌,最终就很容易被遗忘。所以模仿并不是一个好的方法,可以借鉴但不要抄袭。品牌本身是有先发优势的。 25 | 26 | 最后由于我们的目标客户是听播客人群的细分,所以我们在产品形态上选择了用网站的形态来做。而并没有选择很多 AI 产品通过做浏览器插件的形式,我们觉得这有些违背笔记的使用习惯。并且因为在前期做了一些市场调研,我们的竞品因为只有手机客户端,所以也有用户抱怨的声音存在,这也是我们选择从网站开始的原因之一。 27 | -------------------------------------------------------------------------------- /pages/review/fault.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: 最不应该犯的错误 3 | --- 4 | 5 | 如果有什么是我们最不应该犯的错误,那就是我们在定价的时候期望用信息差来赚钱。 6 | 7 | 由于 Podwise 与 [Otter.ai](http://Otter.ai) 之类的产品有个很大的区别,Otter 是服务于会议场景,在会议的转录中,基本不太可能出现重复音频,所以 Otter 的转录时间是肯定会被用满的。但 Podwise 不一样,Podcast 的有很强的头部效应,所以热门节目只需要转录一次,就可以永远服务其他用户,而 Podwise 服务只需要转录一次,也就是利用一份资源消耗,赚取多份利润,这是 Podwise 规划盈利模型的基本逻辑。结果,在第一次测试的时候,多为用户直接提问,是不是你们会缓存节目的转录结果,下个用户来的时候就不用再次转录了。这里面产生了两个逻辑:有用户提说,如果是我转的,那我能不能拿后续的阅读分润?还有用户提另外一端,如果已经被转过的节目,我在学习的时候能不能只扣我 0.5 次。到这里我们才恍然大悟,做产品永远不能把用户当傻瓜,这是我们最大的错误。 8 | 9 | 在接到用户的灵魂两问之后,我们也紧急讨论,最后决定将计费方式变更为:主动使用 AI 处理播客会扣除次数,但查看已处理过的节目则不扣除次数,并且 Podwise 平台会帮大家默认转平台上订阅 Top 100 的节目。从这个决定看,我们付出了巨大的代价,不仅无法利用播客的头部效应来降低成本,而且平台还默认转录内容当成大家免费的资源。在这之后,考虑到利润,我们只能寄希望于 ChatGPT 规模化降价以及 Podwise 自己的规模化盈利了。但没办法,我们不能通过惩罚客户来弥补自己的错误。 10 | 11 | 在最初的计划中,我们是希望通过邮件的形式来与客户沟通的,通过周报与新功能发布在邮件中跟客户沟通,但通过植入邮件的监测后我们发现,邮件的打开率非常低。我们才开发反思是否应该有更直接的方式?经过讨论过后我们优先尝试去建立即刻的 Podwise 官方账号,并开始在一起听播客群组中营销,在这之后发现这种模式惊人的有效。因为社区的强互动性让大家对新产品非常热情,之后我们又开拓了包括小红书和 X 的官方账号,当前正在做 reddit 账号的尝试。如果有什么事是可以尽早做的,那应该是在用户聚集场所建立官方账号,你会收到很多有效的反馈,也可以帮你拓展新用户。 12 | 13 | 虽然 Podwise 是一个订阅制的产品,而海外的订阅制一般只支持 Apple、Google、Paypal 和银行卡订阅这四种,但假如不订阅的话,包括微信和支付宝等方式也是可以支付的。在开始我们只支持了订阅的方式,在黑五大促的事后,因为销售量飙升,我们在各个渠道突然接到了大量无法付款的反馈,这才让我们反应过来,之前可能大量购买都被挡在了支付环节。考虑到这一点,我们用最快速的方式支持了 One-time 也就是一次性支付,也因为一次性支付的支持,顺道我们也支持了支付宝和微信支付。但即使最快,这次改动也只赶上了我们黑五活动的尾巴,当然我们损失了部分用户。后续我们紧急补了一次 Cyber Monday 也就是网络星期一的活动,并告知我们已支持更多的支付方式,在这次 Cyber Monday 的活动中也挽回了部分用户。知道今天,One-time Payment 在我们所有支付的比例中仍然占据 30% 的份额。所以,关注用户的支付习惯,不用教条的追求“订阅”,降低用户付款的难度,一定要重点关注。 14 | 15 | 自从 Podwise 发布过后,突然就出现了十几二十家投资机构对产品感兴趣,我们开始还有点沾沾自喜,觉得自己做的产品有价值所以很多人关注,证明自己做对了。但与众多投资人聊过之后,我们才发现不是的。在当前 AI 创业火爆的大背景下,很多投资人也陷入了 FOMO 的情绪中,开始频繁找项目聊,但因为当前国际局势的关系,美元基金已经全面退潮了,人民币基金背后的 LP 又多为政府,所以大家出手都非常谨慎。造成的结果是,大量的投资人只是找项目来聊确保自己能跟上时代而已,说白了,只是利用自己投资人的光环来找项目学习。为什么如此确定?因为在跟我们聊的过程中很多人根本没有用过 Podwise,这是很令人沮丧的。尤其还有一个国内大厂的战投基金,竟然拉着公司的部门轮番跟我们交流,在交流完一个后,我们果断拒绝了后续的沟通。 16 | 17 | 在自从认识到这一点之后,我们内部制定了一条规则,我们只跟 Podwise 的年订投资人聊,我们将沟通当成用户访谈。这样对我们来说也公平了。最后,我们建议大家不要浪费太多时间在投资人身上,有这个时间,不如多去关心一下自己的客户。 18 | -------------------------------------------------------------------------------- /pages/grow/strategy.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: 到杠杆高的地方去做宣传 3 | --- 4 | 5 | Podwise 遵循 0 成本宣传策略,毕竟是白手起家,本身也没什么钱去投入到营销中。在这种情况下我们就得特别注意方法,不然很有可能事倍功半。 6 | 7 | 首先我们认为,营销一定要去杠杆高的地方,而且有很好的内容发现机制的平台。 8 | 9 | 在传统的社交媒体上,你的影响力辐射半径就是关注自己的粉丝,如果没有粉丝,那么内容就无法被传播。在项目前期我们肯定没有粉丝,这就会让宣传陷入僵局。所以对于独立团队来说,最适合我们的其实是类似于论坛的地方,因为论坛对于所有发帖人来说是公平的,都有被关注到的机会。比如小红书,有标签机制可以被发现;再比如即刻,它虽然是粉丝驱动,但是可以将内容发送至小组,这样也可以被发现;还有类似 Reddit,天然就是一个论坛,只要不违规,所有人的帖子都可以被看到。有什么不太适合我们一开始投入太多精力的地方,比如微博,他的内容发现机制很弱,还有 Twitter,没有粉丝内容几乎很难传播,视频内容包括抖音,虽然给新号一定的发现机会,但整体来说几率还是太低。这些平台适合我们做品牌营销,通过官方网站来引导用户关注,来巩固自己的客群。 10 | 11 | 那什么是高杠杆的地方,高杠杆其实是指 KOL/KOC 比例高的地方,如果要宣传播客,那么最好的渠道应该是即刻的「一起听播客」小组了,这里面有播客听友,同时还有大量的播客节目主理人,他们中的很多人就是播客领域的 KOL,所以影响他们就可以顺带影响一批人。另外还有一些平台虽然他们不是 KOL,但是因为平台的流量机制,导致每个人的帖子都有可能形成广泛的传播,在这种平台去做宣传也是一个很好的渠道。即虽然受众不是 KOL,但形成的二次传播都可能等同于一次 KOL 的传播。 12 | 13 | 而低杠杆的平台比较适合做品牌和用户反馈,比如用户会下意识的去 Twitter、微博等平台找产品的官方账号,一方面看产品有什么最新的进展,更重要的是反馈问题等,而这些平台因为私信等功能其实还是比较适合做这样的功能的。所以对 Podwise 来说低杠杆的平台我们会主要做官方内容的宣发以及及时的问题反馈。 14 | 15 | 总结一下,我们认为在前期我们最需要是通过论坛来做内容营销转化,有一定用户基础之后,再发力做品牌营销来巩固用户。 16 | 17 | 我们再聊一下 ProductHunt 的 Launch,很多人把在 ProductHunt 的发布看得比较重,觉得能从这次发布中直接获取大量的用户,并借此获得不菲的营收。但其实往往是事与愿违的。因为 ProductHunt 网站的核心用户其实是产品经理和开发者,如果你的产品面向的用户是他们,那有可能,但大部分的产品其实是面向普通用户的,所以客群不匹配结果可想而知。而且现在 ProductHunt 因为这次 AI 产品的爆发,每天有大量的产品发布,而排行榜的日榜前三往往需要 800 票以上,这以自然流量转化到 upvote 上几乎是不可能的。所以最终发布在拼的其实不是产品力,而是人脉。对,ProductHunt 有很多 Telegram,Facebook,Linkedin 甚至包括微信的 upvote 群,如果你的发布没有这些群的帮助,那可能非常难排到前三。 18 | 19 | 说了这么多 ProduntHunt 的问题,那为什么还有这么多人冲着去发布。那是因为有很多 KOL,尤其是科技产品领域,因为制作内容的关系,他们会关注 ProductHunt 的日榜和周榜,并制作相关内容来获取粉丝关注,这就是 Producthunt 的意义。如果你之前发布过 ProductHunt,但却觉得没什么效果,那可能是因为你没有被他们说关注到。所以 ProductHunt 的意义并不是直接获取用户,而是通过日榜和周榜前三来获取 KOL 的关注,形成二次传播。这其实也是高杠杆的意义所在。 20 | 21 | 最后我们还认为,对于独立开发者来说,除了用产品的方式来做内容营销,还可以通过 build in public 的方式来做人设营销,build in public 是一种将自己打造产品的过程全程公开的一种营销方式,它是我们 MVP 在营销领域的延伸。之前我们在强调 idea 的时候都是自己在构建用户故事,其实我们也可以用 build in public 的方式在自己的社交平台将这部分内容公开出来,这样会吸引对产品有兴趣的朋友的关注,也可以得到他们尽快的反馈。而 build in public 作为一种新兴的营销方式,为什么会成为很多独立开发者说践行的策略,其中很重要的一个原因是大众对于这部分内容天然的好奇,尤其是有独立开发者在分享自己的营收时,公众会格外关心。所以 build in public 最热门的内容,就是当前产品产生了多少 MRR / ARR。而 build in public 不光会对当前的产品有积极的影响,更重要的是积累了自己的粉丝之后,可以通过个人影响力去打造下一个产品,因为毕竟一个产品就直接成功肯定是少数。而一旦积累了个人影响力,就可以不断通过产品来变现,这也是更长久的生意。我们看到,很多海外 Twitter 上的独立开发者,他们本身就已经是网红了。1000 个铁杆粉丝就可以养活自己,假如你有 2万粉丝,5% 铁杆就足够了。 22 | 23 | 我们比较建议如果要为“独立”做长期打算的开发者,可以试试用 build in public 的方式。 24 | -------------------------------------------------------------------------------- /pages/idea/community.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: 从社区出发 3 | --- 4 | 5 | 在产品这条路上我们为什么看中 Podwise 所面向的播客市场,这个的思路来源完全践行了 [The Minimalist Entrepreneur](https://www.amazon.com.au/Minimalist-Entrepreneur-Great-Founders-More/dp/0593192397) 的强调的从社区出发,看到播客积累的 3000 多订阅用户,我们想是否能给他们做些产品?如果不能,那是否可以在播客领域做些什么?从自身和身边出发。 6 | 7 | 所以我们团队脑暴了一个点子库: 8 | 9 | ![brainstorm](/attachments/idea/brainstorm.png) 10 | 11 | 点子库的内容大家可以看到,要不然是跟独立开发者相关,要不然跟播客相关。这些是我们分析过后可以横跨的两个圈子。我们认为基于自己的圈子,才有可能做出长久且优质的产品。 12 | 13 | 其实播客剪辑也是来自我们自己很大的一个痛点,因为要制作「硬地骇客」这款播客,所以我们在播客设备和剪辑上都下了很大的功夫,传统的剪辑工具是基于音轨的剪辑方法,在剪辑的过程中一定边听边剪,所以剪辑时长与播客的时长一般有一个比例关系,大概要花费在 2~3 倍的时间。我们在准备这个 idea 的时候市面上已经有 descript 了,他们主打做 AI 播客剪辑,但可惜的是没有中文,同时也没有录制环节。我们在规划的时候希望从录制开始,到制作完成发布,完成整个流程,因为只有这样对用户来说才是效率最高的。但你可能也会发现,这样的产品会带来一个问题,你只有打通整个链路之后效率的提升才有体感,以我们团队的人力如果要做到这样的程度需要花费至少 6 个月的开发时间才有可能达成 MVP,这显然会有极大的风险。另外播客剪辑在原有赛道的选手,比如 Adobe 已经在跃跃欲试,并且在尝试 AI 剪辑的部分,这也是其中的一个风险。最后海外除了 descript 之外,还有包括 Podcastle、Riverside、Zencastr 等之前主要关注录制的创业公司,也因为这轮 AI 的发展把自己的触角往 AI 制作方向延伸了,尤其是上面提到的这三家在最近都完成了一轮融资,我们如果要跑的更快如果我们不走融资渠道的话,基本很难追赶了。基于这三点的原因,我们还是放弃了这个 idea。这可能是个很好的 idea,但是不适合我们这样的团队。 14 | 15 | 关于开发者这个群体的产品我们为什么没选,主要还是因为 女人、孩子、老人、宠物到男人的排序中,开发者核心群体的男性的消费能力最低,并且冲动消费最少,买东西都要货比三家。同样是笔记,可以看到知乎和小红书现在的发展差距越拉越大。所以,虽然我们所在的社区是开发者,但本身我们并不想赚开发者的钱,或者换个说法,我们觉得自己没有这样的能力。所以也就暂时放弃了这些相关的 idea。 16 | 17 | 至于为什么最终选择了 AI 播客的内容,首先来源于现实的需求,例如:「纵横四海」「Huberman lab」「Lex fridman podcast」等内容的听众的笔记需求。这些需求是怎么被发掘的,我们发现在即刻和小红书经常会有人发播客笔记,因为播客是音频产品,无法搜索和快速通览,所以在这些平台就出现了很多课代表,其实课代表就是代总结,代提炼的过程,而这个提炼的结果中尤其受欢迎的内容就是脑图。那么既然有这么多人工课代表,那我们 AI 课代表一定有需求,只是我们到底能达到人工的几成而已。另外我们还发现,大部分发布总结的人群都为女性高知用户,可能女性用户对于知识的整理有天然的冲动,而这样的人群画像的购买力也是最强的。其次是 AI 相关,风口上的猪也能飞起来,这句话绝对不是空话。因为 ChatGPT 所带来的这个风潮绝对是一个历史级别的机遇。而且 ChatGPT 还给大家带来一个认知,AI 是有成本的。之前的 SaaS 在用户付费的时候可能会存在一些价值困境,用户会觉得你扩展的成本几乎为 0,所以费用上就会觉得贵。但今天一旦用户知道产品背后有 AI 的支持,那 20 美元的 ChatGPT 对用户就有很强的心理锚定效应。AI 产品等于需要付费这个概念根深蒂固之后,对于产品是非常好的机会,我们要做的只是大胆收费就可以了。最后,跟我们最相关的,我们还有一个 3000 订阅的播客节目,我们可以用自己播客节目来宣传 AI 播客,虽然我们播客的用户画像与播客笔记的画像有所不同,但至少我们不会是从 0 开始。所以我们分析过后一致选择了 AI 播客赛道。 18 | 19 | 当然也许现在也算是证明了我们当时的判断,但回看的话主要还是要归功于运气。 20 | 21 | 这里面还有一个小插曲,为什么 AI 播客产品的名字最后取名叫 Podwise。其实我们当时有两个名字备选:HardPod 和 Podwise。HardPod 是因为有笔记需求的播客本身都是知识型的硬核播客,硬核也就是 Hardcore,HardPod 很自然就是硬核播客的意思。而 Podwise 的名字的灵感来源其实是 Readwise,Readwise 是现今知识管理的中枢型产品,Readwise 这个名字起的也很好,仿佛用了产品之后人就可以智慧起来。那我们是 Podcast 产品,所以就拟定了一个 Podwise 的名字。 22 | 23 | 我们其实一开始都倾向于 HardPod 这个名字,以至于 Github 的仓库很长的时间都叫 hardpod-website,而且 HardPod 还与 HardHacker 有个呼应关系。但又觉得 Podwise 对于出海用户更直觉一些,大家一下就能想到这个产品的用途。最后为了确认最终的名字,我们决定针对 native speaker 做一个产品名的测试,把 HardPod 和 Podwise 给他们,让他们猜一下这个产品是干嘛的?直到一个朋友告诉我们 HardPod 这个名字一听就非常的“性感”,问完我们都长舒一口气,还好问了一下,至此,Podwise 的名字才被确定。 24 | -------------------------------------------------------------------------------- /pages/build/design.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: 设计与开发一体 3 | --- 4 | 5 | 对很多独立开发者来说,设计可能是除了营销推广之外第二难的事情。我们也一样,没有专业设计师,不说 Photoshop 了,连 Figma 都用不明白。但 Podwise 的 UI 设计还是收到了很多用户的好评。我们会分享我们是如何来设计 Podwise 的界面和交互,还包括一些营销内容的设计。 6 | 7 | **设计开发一体。** 8 | 9 | 对于独立开发者或小团队,设计和开发一体是比较高效的方式。这并不只是说由同一个人来负责设计和前端开发工作,而是直接将设计和开发工作一次性完成。得益于现代的开发过程和工具,直接编写 Web 代码并预览效果的效率并不低。哪怕是对能熟练使用 Figma 等设计工具的开发者来说,直接编码并预览的效率也会高于先进行设计再编码实现的方式。也许在进行一些微小调整时,反复的修改代码并预览并不如在设计工具中直接修改来的直观高效。但实际上当设计完成的时候,我们的开发动作也同时完成了。0.6(设计)+ 0.6(开发)> 1(设计开发一体),很简单的道理。 10 | 11 | 与我个人而言,我会选择可以快速调整(例如 TailwindCSS)并实时预览(可以 hotreload)的技术栈。同时我会使用两个显示器,或左右分屏来提高这个阶段 修改->预览->再修改 的效率。 12 | 13 | **如何设计功能性界面?** 14 | 15 | 对于产品的功能性界面,我们需要着重设计的是布局和流程,而非视觉效果。原本视觉效果就不是我们擅长的领域,因此我们完全可以将视觉效果交给 UI 组件库来确保下限。前面提到过的 shadcn/ui 、radix-ui themes 和 NextUI 都是非常优秀的带样式的组件库。这些组件库已经基本涵盖了大部分交互形式,我们只需要将它们组合起来。因此留给我们的主要工作就是设计布局和流程了。 16 | 17 | 设计布局和流程其实是 PD 的工作,而 PD 最重要的就是去理解用户。因此我们的策略就是将自己作为忠实用户,去从目标用户的角度思考这个产品怎么用起来才是最方便的。我会尝试举几个例子来说明。 18 | 19 | - 播放器的设计:Podwise 强调阅读,或将阅读作为听播客时的重要辅助手段。因此可阅读区域就显得非常重要,而播放器本身则会被弱化。传统的播客应用往往会将播放器做的非常显眼好看,甚至做成全屏播放器。但 Podwise 选择尽可能减小播放器的大小,将更多屏幕空间留给文字内容。所以不论在桌面视图还是移动端视图下,我们都将播放器设计成了平时收起,但可通过 hover 或点击方式展开的形式。 20 | - 思维导图样式:为播客节目提取思维导图是 Podwise 的亮点功能之一。但标准的思维导图在移动端视图下并不好用,用户必须横置手机并将思维导图全屏才勉强能阅读。为此我们专门为移动端设计了一个树形结构的纵向思维导图样式,提升用户在移动端的阅读体验。在移动端,默认展示的是这个树形结构的思维导图,而在桌面端则默认展示标准的思维导图。 21 | 22 | **如何设计 landing page ?** 23 | 24 | 相比功能界面,landing page 会是更难以设计的部分。Landing page 的主要目的是将产品能力和优势展示清楚。于此同时,一个设计炫酷的 landing page 对产品传播和使用转化会带来很大的帮助。因此,相较功能界面重在交互体验,视觉样式绝大部分由组件库完成的情况不同,landing page 需要我们自己来完成视觉样式的设计。 25 | 26 | 在缺乏专业设计能力和设计经验的情况下独立完成 landing page 的设计不是一件现实的事情。但我们可以选择多观摩并借鉴别人的设计,拿来和自己的想法进行组合改良。 27 | 28 | 除了在平时留意好看的网站设计之外,有不少在线的资源可以帮助我们。 29 | 30 | - Tailwind UI:Tailwind UI 是 TailwindCSS 的一个衍生项目,提供了非常多通过 TailwindCSS 实现的复杂组件、页面块或完整页面的模板。特别是在 Marketing 分类下,有很多适用于 landing page 的设计。你可以从里面汲取灵感,复刻你喜欢的部分,或者干脆直接购买源码。 31 | - Flowbite:和 Tailwind UI 类似,Flowbite 也是一个基于 TailwindCSS 实现的组件库。在 Flowbite 的 blocks 菜单下,也有很多适用于 landing page 的页面块可当做参考。 32 | - OnePageLove:[onepagelove.com](http://onepagelove.com/) 是一个收集精美设计网站的站点。OnePageLove 收集的基本全都是 landing page ,你可以翻上一整天,从中找出你喜欢的设计。 33 | - Webflow:Webflow 是一个 No-code 网站开发工具,功能强大的同时也有很多模板可供直接套用和修改。No-code 也是一种设计开发一体的方式,如果你倾向于所见即所得的方式,可以考虑。Webflow 本身功能强大但也相对复杂,除了 Webflow 之外也有很多别的产品提供 landing page 的开发和托管,例如 UnicornPlatform 。 34 | 35 | **用 Keynote / PowerPoint 设计营销图片。** 36 | 37 | 产品的视觉和交互设计可以适用设计开发一体的方式,使用开发者更熟悉的方式,也就是写代码来进行设计。但用作营销的图片就没办法用这种方式了。相比起专业的图片编辑软件,我会更推荐使用 Keynote / PowerPoint 来进行这项工作。Keynote / PowerPoint 的上手难度比专业的图片编辑软件要简单的多,且我们大多数人或多或少都使用过它们(例如写晋升答辩 PPT)。我使用 Keynote 来设计活动头图(例如黑五促销)、社媒广告图(例如小红书封面)、产品介绍图(例如用作 ProductHunt 发布)等。 38 | -------------------------------------------------------------------------------- /pages/grow/tactics.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: 互动、回复、关注长尾 3 | --- 4 | 5 | Podwise 正式的宣发是从「硬地骇客」的播客开始的,我们用自己的播客节目,宣传 Podwise 这个播客相关的产品,主打一个免费精准: 6 | 7 | [EP29 三个人三个月的故事:我们的第一个作品 Podwise!](https://www.xiaoyuzhoufm.com/episodes/651188ea19dde7bf6a86a57f) 8 | 9 | 经过本期节目,我们的营销历程算是正式开始了,并且在上线当天我们就完成了销售,算是开门红。 10 | 11 | 因为 Podwise 是一个播客产品,所以从我们的角度,我们需要找到听播客人群最多的地方,如果有主播就更好了。在国内,最好的播客平台是小宇宙,而最好的播客交流平台是即刻的 “一起听播客” 群组,所以自然而然,即刻就是我们在国内宣传的主要渠道。另外小红书是一个人人都能🔥一次的平台,虽然小红书没有那么容易涨粉,但是流量机制会倾向于给新人机会,如果账号做到一定粉丝数反而倾向于让你投流才能获得流量。 12 | 13 | 因为即刻是个小众平台,所以涨粉相对困难,但因为 KOL / KOC 的比例很高,所以在设计即刻起号的过程中我们强调了互动的重要性,因为免费用户不能转录,只能查看会员和平台已转录的播客,每个月 4 期,所以我们提出了免费转录的方案,当然方案由即刻动态与 @Podwise 账号达成,这样就比较容易的达成了二次传播,这个活动在后续一个月的时间里,持续的为 Podwise 即刻账号带来了关注和互动。回看还是个非常不错的营销策略。 14 | 15 | ![engagement](/attachments/grow/engagement.png) 16 | 17 | 回复 KOL 的播客相关帖子,因为即刻的马太效应还是挺严重的,所以 KOL 的帖子往往得到的关注度会很高,同时即刻有回复的点赞机制,这样高质量的回复就很容易显现出来。基于此,我们会选择在 KOL 的帖子下面回复播客的总结和脑图等相关信息,因为这是快速了解一个播客内容的捷径,所以就会吸引用户来了解这是一个什么服务?进而转化为用户和会员。 18 | 19 | 在经过互动和回复的两招后,Podwise 在即刻 “一起听播客” 圈子已经小有名气,但要爆发还缺少一环,真实 KOL 的硬核推荐。KOL 的推荐一定来自广泛的受众覆盖,这样才有可能更大概率击中 KOL。更重要的是需要产品体验真正打动用户,这样才会自发的给产品代言。酒香也怕巷子深,但前提还是酒香,所以,好产品和高覆盖宣传缺一不可。 20 | 21 | ![trending](/attachments/grow/trending.png) 22 | 23 | 经过互动、回复和 KOL 传播之后,在即刻平台播客相关的意见领袖对于 Podwise 的产品应该已经有一定的认知了。 24 | 25 | 经过即刻的宣传过后,我们意外的发现了在小红书出现了 Podwise 的热帖,而且曝光量超过想象:[🔥播客教程|我是如何有效获取播客知识点的?](https://www.xiaohongshu.com/explore/655ddccb000000001701f4ae) 26 | 27 | 考虑到我们本身并没有在别的平台做过宣发,所以这次小红书 KOL 自主宣传也很有可能来自于即刻本身的社区覆盖,在热帖下面我们发现,很多用户不知道 Podwise 如何访问,还有使用方法等不太清楚,我们想说,是时候可以做做小红书了。 28 | 29 | 如果说即刻是一个 KOL 主导的社区,那在小红书,它的流量策略可以让每个人都火一次。所以在小红书 标题党 + 解决方案 是一个非常好的流量获取策略,比如:哭死,姐妹们,家人们,谁懂啊 等等,当然如果你的标题内容主打帮大家解决一个问题,那这篇内容是有可能迎来传播的,当然是有可能,不是绝对,毕竟小红书的推流策略是一个黑盒。 30 | 31 | Podwise 在小红书的起号是通过一篇:[我真的哭死,这么用心做的 app 值得被看见](https://www.xiaohongshu.com/explore/6582dc6a000000003403db9a) 32 | 33 | 起号的,这也是一篇典型的 标题党 + 解决方案的文章,因为这篇笔记的图上文字是:“听播客做笔记,终于等到你”,所以属于内容标题党 + 封面解决方案的组合,如果你也想要尝试在小红书做账号,那也可以试试看。 34 | 35 | 另外小红书也是 Gen Z 时代用户的搜索引擎,所以如果你要做小红书,一定要注意长尾流量,因为在你不是 KOL 的情况下,你的内容被刷到的概率不会很大,但是假如用户对于某件事寻求解决方案,那么如果你有一篇相关的笔记,那这篇笔记会持续不断的获得流量,进而产生转化。类似于你做搜索引擎的 SEO,所以在小红书发笔记时候,需要时刻思考 SEO,我这篇笔记到底要占领哪个关键词,帮用户解决什么问题?而如何发现用户到底在搜索什么?可以从小红书本身的搜索框提示出发,看看用户到底在搜什么,进而写一篇相关的笔记。 36 | 37 | ![longtail](/attachments/grow/longtail.png) 38 | 39 | 所以当我们发现用户在小红书上排名第一的竟然是 Podwise 怎么用的时候,我们就立刻决定写一篇 Podwise 使用教程的内容:[人人都能学会!🪄手把手教你使用Podwise.](https://www.xiaohongshu.com/explore/6588cbcc0000000038022b88),至今还在不断的收获点赞和转化。 40 | 41 | 另外不论是即刻还是小红书,回复相关热帖都是一个非常好的流量获取手段,小红书甚至有个功能,在你的帖子被其他网友点赞超过一定次数之后,小红书会提示你将回帖直接发成一篇笔记,这也是一个非常好的曝光手段。 42 | 43 | 这些是我们当前在获客的一些方法,这里也推荐播客节目「乱翻书」的一个传播金句:“用户最爱转发的内容有三种:一种叫喜闻乐见,一种叫感同身受,第三种叫对我有用,它们的共通点都是与我有关。” 44 | 45 | ![highlight](/attachments/grow/highlight.png) 46 | 47 | 我们认为这是一个非常好的底层逻辑,适用于各种社交媒体,如果你的内容还做不到“喜闻乐见”和“感同身受”的话,那至少保证“对我有用”。这可能是成本最低的获客方式了。 48 | 49 | 最后营销这件事,重点在如果 10 万行代码可以成就一个优秀的程序员,那我们可以问问自己是否有发过哪怕 1 万字的营销文案。如果没有的话,请坚持下来。 50 | -------------------------------------------------------------------------------- /pages/build/saving.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: 省下的都是赚的 3 | --- 4 | 5 | 得益于近些年云计算的快速发展,不只是 IaaS,更多的 PaaS 产品横空出世,现在构建产品的成本越来越低了,甚至可以做到除了域名之外 0 成本购买。Podwise 在构建产品的过程中当然也完全践行了能省则省的原则。 6 | Podwise 在最终的选型上,使用了 Vercel、Supabase、Amazon SES 和 Lemon Squeezy,几乎做到了零成本。 7 | 8 | 在为什么选择 Lemon Squeezy 上最核心的原因是因为 Stripe 对于注册要求门槛比较高,需要有相关的税务资质。虽然 Stripe 相对来说是综合费率最低的,但在当时的环境下还是选择了对于独立开发者相对友好的 Lemon Squeezy 来使用。虽然 Lemon Squeezy 的综合费率较高,但相应的也提供了一个额外的 affiliate 服务,可以让开发者利用 Lemon Squeezy 平台来发展代销网络以及分润,这对开发者来说也相当友好了。另外如果你的年度营收超过 10 万美金,也可以尝试向 Lemon Squeezy 发邮件申请降低费率,这也是他们平台的一项扶持政策。 9 | 10 | > 2024/05/31 Update: Lemon Squeezy 在最近修改了用户协议,因为制裁的缘故不再接受来自中国大陆用户的付款。如果你预期的国内用户很少的话倒是没关系,否则就需要考虑其他的支付解决方案了。或者针对国内用户提供其它的支付方式。 11 | 12 | 在成本控制方面,通用服务相关几乎 0 成本了,但对于 AI 产品来说,想做到零成本就几乎不可能了,而且 AI 产品一定要计算销售毛利,免费策略要谨慎使用,随意定价的结果很可能会血本无归。 13 | 14 | Podwise 是一个播客语音转文字的服务,我们在 AI 领域会用到两个最基础的服务: 15 | 16 | - Whisper:用于语音转文字,由 OpenAI 开源,并且也有商用服务提供 17 | - ChatGPT:LLM 服务,由 OpenAI 提供,用于总结、大纲、摘要、名词解释等用途 18 | 19 | Whisper 的商用服务价格为:$0.006/m。 换算为一小时的播客节目的话费用为 $0.36,接近 ¥2.6 一集,这个成本可想而知。 20 | 21 | ChatGPT 经历过几次降价,并且区分 GPT4 和 GPT 3.5。价格如下表: 22 | 23 | | | Older Models | New Models | 24 | | --- | --- | --- | 25 | | GPT-4 Turbo | GPT-4 8K
Input: $0.03
Output: $0.06 | GPT-4 Turbo 128K
Input: $0.01
Output: $0.03 | 26 | | GPT-3.5 Turbo | GPT-3.5 Turbo 16K
Input: $0.003
Output: $0.004 | GPT-3.5 Turbo 16K
Input: $0.001
Output: $0.002 | 27 | 28 | 如果我们以 1 小时英文播客计算的话,语速一分钟 180 单词左右,那么一期播客会有大概 10000 单词,换算到 OpenAI 的 Token 计数,同时叠加 Prompt 以及输出的 Token 计算,那么会消耗 20000 左右的 Tokens。 29 | 30 | 按 Tokens 计算,未降价前 GPT-3.5 和 GPT-4 成本分别为:$0.07 和 $0.8,降价后成本为:$0.025 和 $0.3。 31 | 32 | 由于在 Podwise 构建当时,OpenAI 还未降价,所以以当时的成本计算,1 小时的英文播客,如果完全使用商业服务,GPT-4 版本需要 $1.16 一期,而 GPT-3.5 版本也需要 $0.43 一期。如果订阅版本中提供 20 期的节目转录,平均一期一个半小时,GPT-3.5 的成本需要 $12.9 左右,GPT-4 版本那就更是天价了。所以如何降低成本就变成非常重要的一件事。 33 | 34 | 首先是 Whisper,由于 Whisper 是开源的,而且业界有不少针对 Whisper 的优化版本,所以优先思考的就是是否可以将 Whisper 跑在自己的服务器上,这样每小时节目就可以节省 $0.3,这是一笔不菲的费用。所以开始阶段的 Podwise 节目背后其实都是用 Macbook 与 Mac Studio 在背后支撑的,得益于开源项目对于 Mac M 系列芯片的不断优化,一小时的音频在 M1 Max 机器上从 20 分钟提升到了 10 分钟以内,感谢开源。这样我们就把费用顺利的转化为 M 系列芯片的电费,众所周知,M 系列芯片能耗比非常好,100瓦足以满载工作。满载工作 10 个小时消耗 1 度电,按 ¥1 计算的话成本简直不要太低。 35 | 36 | 在我们利用高能耗比的 M 系列芯片节约成本之时,突然有 AWS Startup Program 的同学找到我们,问我们是否愿意申请 AWS 的 Startup Program,成功的话最少有 $25000 的 Credit。这对我们来说简直的天上掉下来的馅饼,由于是 AI 创业项目,申请的过程格外的顺利,最终我们拿到了 $10000 的额度可以用于购买 ECS 服务器,但 Credit 有使用时间限制,周期为 6 个月,但因为是免费额度,我们很快将家用机切换为高性能 v100 服务器,服务的效率也快速提升,后来我才知道,其实 AWS、Azure 和 Google 都有针对 Startup 的 Program,且补贴都不低。尤其是 Microsoft for Startups,额外赠送 $2500 OpenAI 额度,这对于 AI 创业者来说简直是白捡的,而且一定会用到的福利。当我们注意到 Microsoft for Startups 的 OpenAI 额度的时候,我们已经花了 $1000 左右了,这些钱本可以省下来变成利润的,所以在这里强烈建议 AI 创业者先去申请 Microsoft for Startups。以下是三个 Startup Program,有需求的同学强烈建议按需申请,注意 Credit 一般都有使用时间的限制,需要在 6 个月到 1 年的时间内消耗完毕,所以建议错开时间申请,这样可以最大化利用云厂商的免费额度。 37 | 38 | - AWS:https://aws.amazon.com/startups 39 | - Azure:https://www.microsoft.com/en-us/startups 40 | - Google:https://startup.google.com/programs/ 41 | 42 | 2023年12月14日,Google 终于在 LLM 领域发力,推出了与 ChatGPT 能力接近的模型 Gemini,甚至在自己的评测中,Ultra 版本能力全面超越 GPT-4。其实对于新模型来说,如果能力只是与 GPT-4 接近,即使价格便宜一些,可能也不会引起太大的波澜,但 Gemini 推出了一个针对所有人免费的 Gemini Pro 版本,重点是完全免费,且 60 QPM,这对于 Podwise 这类应用来说完全够用,在经过长时间的测试后,我们判断 Gemini Pro 的能力确实与 GPT-3.5 有一战之力,我们做出了一个 Gemini Pro 与 GPT-3.5 并行支持 Podwise 的决定,又节省了一半的成本。 43 | -------------------------------------------------------------------------------- /theme.config.jsx: -------------------------------------------------------------------------------- 1 | import { useConfig } from "nextra-theme-docs"; 2 | import { useRouter } from 'next/router' 3 | import Image from 'next/image' 4 | 5 | 6 | 7 | export default { 8 | logo: 硬地骇客 - 两个月 $12000 ARR 实践之路, 9 | project: { 10 | link: "https://github.com/hardhackerlabs/book", 11 | }, 12 | chat: { 13 | link: "https://book.hardhacker.com/authors", 14 | icon: ( 15 | 16 | 17 | 21 | 25 | 26 | 27 | ) 28 | }, 29 | docsRepositoryBase: "https://github.com/hardhackerlabs/book/tree/main/", 30 | banner: { 31 | key: 'debut', 32 | text: ( 33 | 34 | 🥳 加入「硬地骇客」知识星球,与作者和读者一起交流探讨!点击了解更多 → 35 | 36 | ) 37 | }, 38 | head: () => { 39 | const { asPath, defaultLocale, locale } = useRouter() 40 | const { frontMatter } = useConfig() 41 | const url = 42 | 'https://book.hardhacker.com' + 43 | (defaultLocale === locale ? asPath : `/${locale}${asPath}`) 44 | 45 | return ( 46 | <> 47 | 48 | 49 | 53 | 54 | 55 | ) 56 | }, 57 | 58 | useNextSeoProps() { 59 | const { frontMatter } = useConfig(); 60 | 61 | return { 62 | titleTemplate: frontMatter.title + " – 硬地骇客", 63 | }; 64 | }, 65 | toc: { 66 | title: "目录", 67 | extraContent: <> 68 | 🥳 欢迎加入「硬地骇客」社群! 69 | knowledge planet 75 | 与作者和广大读者一起交流。 💬 76 | , 77 | }, 78 | editLink: { 79 | text: "" 80 | }, 81 | // ... other theme options 82 | footer: { 83 | text: ( 84 | 85 | Made with 💖 by ©{" "} 86 | 87 | 硬地骇客 88 | 89 | 90 | ), 91 | }, 92 | faviconGlyph: "🏂", 93 | }; 94 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | 4 | 5 |

6 | 7 |

8 | 9 | 10 | 11 |

12 | 13 |

14 | 15 | 16 | 17 |

18 | 19 | ## 参与讨论 20 | 21 | 欢迎大家加入「硬地骇客」的官方星球并参与本书的讨论。 22 | 23 |

24 | 25 | 26 | 27 |

28 | 29 | 社群提供如下服务: 30 | 31 | 1. 全年不少于 24 篇专题分享,内容包括但不限于 灵感 - 构建 - 发布 - 增长 等产品关键环节。 32 | 2. 全年 50 期左右的「硬地骇客」播客文章版内容以及参考资料,通过文章的阅读体验可以在收听节目之后方便快速检索播客内容。 33 | 3. 为支持社群会员的产品发展,社群为每位会员提供「硬地骇客」播客节目 Show Notes 广告位一次,价值 ¥500,可为产品提供高质量的站点外链,提升搜索引擎排名。 34 | 4. 不定期的黑客马拉松赛事,优胜者可以获得包含现金奖励与项目孵化等合作项目支持。 35 | 36 | 关注前沿科技,分享创业故事,打造 “超级个体”,寻找利基市场,构建小而美的生意,同时也希望和广大 Hacker 一起探讨技术、产品和商业之美。 37 | 38 | ## 关于本书 39 | 40 | 本书是关于 [Podwise](https://podwise.ai) 产品从灵感到变现过程的忠实记录以及复盘。 41 | 42 | [Podwise](https://podwise.ai/) 是一款专为播客听友设计的 AI 知识管理应用。 利用 Podwise 平台,你只需 follow 喜欢的播客,比如「硬地骇客」,当有节目发布后,Podwise 会通过 AI 对播客内容进行 转录、提取、总结、分析 等一系列操作,帮你掰开了揉碎了硬核的播客知识。同时与 Notion、Obsidian、Logseq 和 Readwise 等平台的打通,嵌入你的知识管理工作流,协同在其他渠道的包括新闻,Newsletter,Blog 等内容,帮你完善第二大脑 🧠。 43 | 44 | Podwise 今天的成绩远称不上成功,可能勉强算是找到 PMF (Product-Market Fit),所以这不是一份权威指南,内容仅供参考。 45 | 46 | 全书以产品的全生命周期划分为 5 个部分: 47 | 48 |
49 | 💡 灵感 50 | 51 | * [从社区出发](https://book.hardhacker.com/idea/community) 52 | * [百万美金的利基市场](https://book.hardhacker.com/idea/millions) 53 | * [MVP MVP MVP](https://book.hardhacker.com/idea/mvp) 54 |
55 |
56 | 🛠 构建 57 | 58 | * [设计与开发一体](https://book.hardhacker.com/build/design) 59 | * [用方便且擅长的技术栈](https://book.hardhacker.com/build/buildstack) 60 | * [关于大模型必须知道的事](https://book.hardhacker.com/build/llm) 61 | * [省下的都是赚的](https://book.hardhacker.com/build/saving) 62 | * [能用的服务千万别做](https://book.hardhacker.com/build/services) 63 |
64 |
65 | 🚀 发布 66 | 67 | * [最初的 200 个用户](https://book.hardhacker.com/launch/acquire) 68 | * [永远跟用户站在一起](https://book.hardhacker.com/launch/feedback) 69 | * [定价要关注成本](https://book.hardhacker.com/launch/pricing) 70 |
71 |
72 | 💸 增长 73 | 74 | * [到杠杆高的地方去做宣传](https://book.hardhacker.com/grow/strategy) 75 | * [互动、回复、关注长尾](https://book.hardhacker.com/grow/tactics) 76 | * [意料之外的爆发](https://book.hardhacker.com/grow/monetize) 77 |
78 |
79 | 🤔 复盘 80 | 81 | * [对远景的一些规划](https://book.hardhacker.com/review/future) 82 | * [最不应该犯的错误](https://book.hardhacker.com/review/fault) 83 | * [还会坚持干的几件事](https://book.hardhacker.com/review/learn) 84 |
85 | 86 | ## 关于作者 87 | 88 | 本书的作者是「硬地骇客」社区,硬地骇客即 Indie Hacker 的意译。 89 | 90 | 硬地骇客社区核心成员由三位拥有10年+互联网创业者组成,我们关注前沿科技,分享创业故事,打造 “超级个体”,寻找利基市场,构建小而美的生意,同时也希望和广大 Hacker 一起探讨技术、产品和商业之美。我们正在努力探索一条能够同时实现财务、时间和身体自由的道路,希望能够通过 Podcast 将我们的故事分享给大家。 91 | 92 | 硬地骇客是一群追求自由充实的生活并喜欢挑战的 Builder,热爱技术,构建产品,崇尚依靠产品驱动的增长方式构建出自己的小生意。 如果你也喜欢这些信条,欢迎加入我们硬地骇客社区。 93 | 94 | 我们坚信:“每个人都应该拥有一个小生意”。 95 | 96 | ## 版权声明 97 | 98 | 本作品采用 CC BY-NC-ND 4.0 进行许可。 99 | 100 | 这意味着您可以自由分享、复制和传播本作品的整体或部分内容,但须遵守下列条件: 101 | 102 | * 署名 — 您必须给出适当的署名,提供指向本许可协议的链接,同时标明是否(对原始作品)作了修改。您可以用任何合理的方式来署名,但不得以任何方式暗示许可人为您或您对本作品的使用提供认可。 103 | * 非商业性使用 — 您不得将本作品用于商业目的。 104 | * 禁止演绎 — 如果您再混合、转换或者基于本作品创作,您不可以分发修改后的作品。 105 | 106 | 无视上述任何条件,都构成违反许可协议,根据法律将受到相应的处罚。 107 | -------------------------------------------------------------------------------- /pages/build/llm.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: 关于大模型必须知道的事 3 | --- 4 | 5 | **内容生成类 AI 产品的后台服务大有不同** 6 | 7 | 2023 年 ChatGPT 的出现,带动了一大批的 AI 产品的诞生,所有的这些 AI 产品都集中在文字、图片和视频的生成领域。这些 AI 产品和传统的 2C SaaS 产品有一个巨大的区别(除了 AI): 8 | 9 | - 构建传统的 2C SaaS 产品从技术角度会着重考虑 QPS 性能容量,你永远也不想开发一款互联网 SaaS 产品最多只能提供 10 个并发任务。 10 | - AI 生成式产品在处理 AI 任务的时候,需要耗费大量的算力,同时也带来了巨大的延迟,一个图片、音视频任务都可能需要分钟级以上的时间去处理。AI 任务的排队基本是铁定的事实,区别只在于长短上。 11 | 12 | 所以,传统的 2C 互联网产品很流行使用 QPS 这个技术指标,而今天的 AI SaaS 产品,很难再使用 QPS 来度量 (或许,可以升级到 QPH 了)。 13 | 14 | 针对 AI 任务的大延迟,消费者端基本也具备一致的共识,所以这类 AI 应用的后台设计需要重点考虑任务调度,任务排队,任务优先级,任务限流,任务延迟后的用户体验等等。 15 | 16 | **理解 AI 模型的能力边界** 17 | 18 | 在具体实现 AI 产品之前,我们应该深度使用每一款大模型,确保自己对模型有比较深刻、完整的理解。除了知道模型能够做什么以外,更重要的是知道模型能做到什么程度。 19 | 20 | 搞清楚模型的硬性指标有哪些,比如: chatgpt 3.5 有 16k 的 token 支持,这个 16k 是 input 和 output 的 token 总和。然而,gemini 1.0 有 32k 的 token 支持,但是 32k 是 input 占用了 30k,output 只有 2k。看出区别了吗?gemini 1.0 2k 的 output 限制,已经决定了你不能让它一次给你输出很长的一段文字,比如用它写小说等长输出场景。 21 | 22 | 相对于模型的硬性指标,我们更应该关注模型那些看不见的能力,比如稳定性,比如细节处理能力。这些能力都是需要在大量的深度使用上,才能真正体会得到的,我的建议是从一开始用比较保守的态度去对待 LLM ,不要试图把所有的任务都扔给 LLM 处理,在你的业务流程中,能用写代码解决的就老实写代码,把真正需要 LLM 的任务交给 LLM。在 Podwise 开发中,花了非常多的时间打磨 LLM 的输出稳定性。 23 | 24 | 自己写代码还是调用 LLM ?这一刀从哪里切下去,就是我们理解 AI 模型的边界。 25 | 26 | **模型选择** 27 | 28 | 如果使用 langchain 这种框架,那基本可以选择调用任何一个模型,它已经帮你做好了封装和模型的对接。当然,如果像我们一样,使用了其他编程语言,或者不想用 langchain,那恭喜你,你可以非常自由的使用相关模型提供的 api or sdk。但要告诉你的是,永远不应该只锁定在一个模型上,比如 chatgpt。所以,从一开始就可以留出未来多模型的位置和空间。 29 | 30 | Podwise 目前是同时支持 chatgpt 和 gemini 的,也可以非常方便的再扩展到一个新的模型上。具体使用哪个模型,哪个版本,完全根据模型的效果和成本进行综合评估。在一些特殊的地方,我们甚至使用了动态调用多模型来获取结果,比如 gemini 安全限制严格,导致一些播客内容无法得到期望的结果,那我们会根据 gemini 的结果动态决策是否调用 chatgpt ,等等。 31 | 32 | 另外,业务流程中不应该出现模型相关的信息和选项,除非你是在做模型代理之类的产品。用户关心的永远是结果和质量,而不是你的技术实现。但是技术实现有时候可以增强用户信心,这个可以在营销环节去呈现。 33 | 34 | 不建议一开始选择开源模型,虽然成本可能更低,但不可控性太高,运维管理的投入精力也很大。这不是一个独立开发者应该投入的方向。如果有时间,有人力,有技术投入在开源模型上,也建议是在 mvp 推出,验证市场需求之后,再考虑。 35 | 36 | **AI 工作流** 37 | 38 | Podwise 的整个 AI 后台服务主要由两条工作流驱动完成: 39 | 40 | 第一条工作流负责从任务队列获取播客 episode 进行转录成最终的 transcript 文本。这个工作流具体就会涉及到: 下载音频文件 -> 探测语言 -> 预采样音频内容 -> whisper 转录音频 -> 生成分段 -> 优化 transcript -> 写入 db。 41 | 42 | 第二条工作流负责从任务队列获取被第一条工作流处理完的 episode,然后执行 LLM 总结任务。具体流程: load transcript -> 分段(split)-> 总结章节,抽取 highlights和关键词 -> 基于章节生成全文总结 -> 解释关键词 -> 基于章节生成 mindmap -> 写入 db。 43 | 44 | 为什么是两条工作流,而不是合二为一的一条?开发成一条明显是很自然的选择,一开始我们也是跑在一条工作流上。我们在 mvp 阶段,整个工作流服务都跑在我们本地电脑上,从下载音频到whisper 转录,再到 chatgpt 总结。然后就遇到一个很大的问题,由于网络问题,本地电脑调用 chatgpt 很容易出错,所以需要把第二部分拆分到 aws 免费的 1核1g ec2 上(这也是为什么有了在 Go 的技术选型章节里强调的资源节省问题)。现在,我们已经没有本地电脑运行了,全部在云上。 45 | 46 | 历史故事分享完了,再认真分享一下两条工作流的真正好处是什么。最大、最重要的好处就是可以轻松、方便的重跑 transcript 或者是总结。Podwise 做到 "能够在任何时候对 AI 生成结果进行低成本的重跑,重新处理" 是一个非常重要的设计,任何功能的加入都不能破坏这个设计。 47 | 48 | 针对工作流内部再介绍几个关键点: 49 | 50 | 一:podwise 除了使用 whisper 将音频转成文字外,在这个过程中,还加入了识别 speaker,并且将 speaker 的声音 embeding 成向量,再写入向量数据库 qdrant。借此,我们才做到了在节目的 transcript 中直接显示 speaker 的名字。或许有人会认为我们是采用 llm 从对话中分析出来的 speaker 名字,实际并不是。 51 | 52 | 二:whisper 转录出来的 transcript 很长,需要分段后,才能被 chatgpt,gemini 这样的大模型处理。对长文进行分段处理,基本是总结类产品的必备操作。podwise 采用了传统 NLP 的社区算法对文本进行向量相似度计算,从而更好的实现按对话逻辑和内容进行分段。 53 | 54 | 三:LLM 总结流程中的并发设计,涉及到了这样的几种模式(在 langchain 中也有这样的概念): 55 | 56 | - 无序并发:就是可以将多个任务提交给 llm 后,返回的结果不需要任何顺序。 57 | - 有序并发:将多个任务提交给 llm 后,返回的结果必须根据业务需要排序的。 58 | - 完全串行:就是多个任务只能串行,一个一个的执行,往往前一个任务是后一个任务的输入。 59 | Podwise 将第三种串行执行这种模式提升到了工作流上完成控制,所以工作流是先做章节总结,然后做全文总结,最后是 mindmap。在工作流一个节点内部,大多数对 llm 的调用都是由无序或者有序并发模式组成的。 60 | 61 | **Prompt** 62 | 63 | 做 AI 产品,除了要设计好的 AI 任务流程,合理的拆分业务以外,最重要的就是写好 prompt,管理好 prompt,持续迭代 prompt。 64 | 65 | prompt 一般有两种形式:结构化 prompt 和对话式 prompt。 66 | 67 | 结构化 prompt 的优点是通过规范的结构把任务介绍得很清楚,缺点就是往往很长,比较复杂。而对话式 prompt 更加简单,更符合日常的说话习惯,缺点是难以一句话描述清楚任务,最后得不到满意的结果,需要进行多轮对话才能获得最终结果。两种 prompt 都有自己的适合场景,结构化的 prompt 更合适用来内置到产品工作流中,由开发者编写、维护,podwise 采用的就是这种复杂的 prompt 形式。对话式 prompt 就合适用在 chat bot 场景,直接由用户发出。 68 | 69 | 1. **结构化 prompt** 70 | 71 | podwise 的结构化 prompt 框架覆盖了如下一些内容: 72 | 73 | - 定义角色 74 | - 介绍背景和输入的数据格式 75 | - 提出任务 (可能会有多个任务) 76 | - 执行所有任务的步骤 77 | - 定义输出格式 78 | - 给定输出例子 79 | 80 | 这是一个结构化 prompt 的大概框架,这个框架可以采用 markdown 来描述。podwise 的 prompt 有两个很关键的地方。第一个是多任务,第二个就是输出格式的控制。 81 | 82 | - 关于多任务: 83 | 首先,一定要明白在一个 prompt 里面内置多个任务,绝对不是一个好的选择,除非你有强烈的这样做的理由。podwise 选择做多任务的理由就是 “降低成本”,这对我们来讲是非常重要的事情,我相信对大多数独立开发者来说,都是重要的事情。 84 | 85 | podwise 在一个 prompt 同时执行 “总结章节”,“抽取highlight”,“抽取关键字” 等任务,就是为了让这些事情只需要输入一遍 transcript 就可以同时获取这些结果。如果单独执行每一个任务,那就需要把相同的 transcript 数据输入 llm 多次,这将会多消耗数倍的成本。 86 | 87 | 但一定要明白,多任务无疑增加了 llm 执行的复杂度,这并不符合 “尽量给 LLM 简单、明确、较小的任务的原则“。经过测试,多任务执行的结果质量赶单任务是有差距的,这就需要不断的打磨和权衡吧。 88 | 89 | - 关于输出格式控制: 90 | 由于 podwise 的 llm 总结结果是需要在产品页面上进行结构化展示,并不是 chat bot 那样直接输出给用户(人),所以像 podwise 这类产品对 llm 的输出格式就需要严格定义,并且希望 llm 能够稳定且正确的输出。对于程序员来说,一般会选择 json 作为输出。但 podwise 并没有选择 json,而是选择了 markdown。原因是因为 llm 并不是绝对稳定,再规模上来后,总是会有不稳定输出的情况,偶尔输出的 json 都是非法的,这种情况只能重试 llm,浪费 token,增加成本。所以,podwise 实践的做法有这样的几点来控制输出,减少重试 llm。 91 | - prompt 中提供输出示例 92 | - 输出格式使用简单的 markdown 语法,自己解析 markdown 93 | - 借助编程做好容错处理 94 | 95 | 2. **prompt 管理** 96 | 97 | 我们采用模板技术来定义 prompt,然后通过模板变量去控制 prompt ,比如多语言等。使用模板来管理 prompt 后,就不需要为不同的情况都写一份 prompt,只需要抽象好 prompt 模板 + 模板变量即可。 98 | 99 | 3. **prompt 测试** 100 | 101 | - OpenAi playground 102 | - Google AI Studio 103 | - https://promptknit.com/ 104 | 105 | 在调试 prompt 的时候,温度(temperature)应该是最常用的一个选项。也就是设置不同的温度,可能会得到不同的效果。像总结文章这种需求,需要基于原文的事实,那最好是温度设置低一些,倾向 0 都可以。温度设置得很高,大于 1 ,LLM 就会更大概率做自由发挥了。还是看自己的业务场景,以及更多的测试。 106 | 107 | 4. **prompt 迭代** 108 | 109 | 在开发 AI 产品的时候,不要纠结一步到位写好 prompt ,还是需要将重心放到完成整个业务流程和功能上。prompt 的编写也和代码一样,需要持续的迭代、优化。所以,需要好的 prompt 管理方式,方便持续的迭代、测试改进。 110 | 111 | 对 prompt 不断地打磨,调试,并不是一件 roi 很高的事情,但有时候你又不得不做。 112 | -------------------------------------------------------------------------------- /pages/appendix/lemonsqueezy.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Lemonsqueezy 计费场景支持和常见问题 3 | --- 4 | 5 | Lemonsqueezy 是国内独立开发出海比较常用的收款产品。相比 Stripe 来说,Lemonsqueezy 的开通门槛可谓是非常的低,风控也没那么严格,所以被很多国内独立开发者选择。当然如果你有条件的话,我建议还是使用 Stripe ,因为 Lemonsqueezy 在产品能力和稳定性上确实做的不太好。 6 | 7 | 这篇分享会列举一下较常见的收款、支付、计费、营销方式是否被 Lemonsqueezy 支持,特别是哪些地方和我们看文档所设想的不同。此外也会分享一些我们在使用 Lemonsqueezy 的过程中遇到的问题和解决方法。 8 | 9 | > ⚠️ 需要注意的是目前 Lemonsqueezy 不支持国内用户的支付。用户反馈无法使用支付宝、微信付款,或者在使用信用卡支付时地址如果填写 China 也无法付款。如果你的主要客群是国内用户,需要考虑 Lemonsqueezy 是否适合。 10 | 11 | ### 收款 12 | 13 | Lemonsqueezy 支持多种收款方式和支付方式。 14 | - 订阅制收款,即每个周期自动扣款。Lemonsqueezy 支持日、周、月、年粒度的扣费周期。我们常用的一般就是月度订阅和年度订阅。Lemonsqueezy 支持在一个订阅制产品下创建多个不同的变体(可以理解为不同 Plan),赋予不同的价格。用户可以在这些变体间快速切换,Lemonsqueezy 会负责**计算价差并在下个扣费时间点抹平价差**。 15 | - 一次性收款。Lemonsqueezy 也支持非订阅制的交易,只会按当前交易扣款一次。我们在两个场景使用了一次性收款:出售额外转录次数;通过支付宝/微信支付出售一次性会员权益。 16 | 17 | 支付方式: 18 | - 信用卡 - 最常用的支付方式,常见的发卡方例如 Visa 、Mastercard 、AE 、Discover 、DC 、JCB 等都支持,也支持 UnionPay(银联)。但实际尝试发现**国内发行的单币信用卡很多都不能完成支付**。 19 | - Google Pay / Apple Pay - **仅在对应浏览器中打开支付界面时会出现**。Google Pay 仅在 Chrome 中支持,Apple Pay 仅在 Safari 中支持。支持订阅制。 20 | - PayPal - 支付订阅制。 21 | - 支付宝 / 微信支付 - **仅在一次性收款的情况下支持**,订阅制不支持。之前 Lemonsqueezy 曾短暂在订阅制付款界面下透出了支付宝 / 微信支付的支付方式但实际不可用,后重新被隐藏。截止成文为止目前还不支持支付宝 / 微信支付的订阅制收款。 22 | - Bank debits - 不太了解这种支付方式,但在马来西亚等地区比较流行。 23 | - 其它一些支付 App 或数字钱包 - 视付款者所在地区不同,可能出现不同的选项。 24 | 25 | Lemonsqueezy 会**尽可能展示所有可用的支付方式给用户**。这些支付方式中,目前仅信用卡、Apple Pay 、Google Pay 和 PayPal 支持订阅制收款。 26 | 27 | ### 计费 28 | 29 | - 固定价格 - 这是最基础的方式,不论一次性收款还是订阅制收款都支持这种定价。 30 | - 初始化费用 - 在订阅制收款下,除了每个订阅周期的订阅费之外,Lemonsqueezy 允许你设置一个初始化费用(setup fee)。这是一笔一次性的费用,会**在首次订阅时收取**。假如你的服务在用户首次开通时有额外成本(例如你需要指派一位培训专员为用户进行培训)而你想要向用户透出这部分成本,你就可以考虑启用 setup fee 。 31 | - 用户自选价格 - **类似 sponsor 的模式**,由用户在下单时自己选择价格。不过你可以设置最低价格和建议价格。我们非常建议设置一个合理的最低价格,因为 Lemonsqueezy 会为每一笔交易扣除固定的手续费。 32 | - 按量计费 - 仅订阅制收款支持按量计费的模式。我们将用户的使用量通过 API 发送给 Lemonsqueezy ,然后 Lemonsqueezy 提供了好几种方式将用户实际用量映射到最终要支付的价格上。 33 | - 标准价(Standard pricing):就是普通的单价。消耗多少用量就以用量乘以单价计算得到最终用户会被收取的费用。 34 | - 打包价(Package pricing):价格不以单个用量决定,而是按需要消耗多少个 Package 来决定。换个说法就是不单卖,你要买就得整件的(山姆会员店?)。比如你定价 $10 per 10 units ,虽然看起来每 1 unit 价格是 $1 ,但用户最终只会被按 $10 的整数倍 charge 。用户用量是 1-10 ,被收取 $10 。用户用量为 11-20 ,则被收取 $20 。 35 | - 批量定价(Volume pricing):这是一种分层定价策略,根据总用量所处的区间来决定最终单价,从而决定最终价格。例如我们可以设置 1-100 用量时单价为 $2 ,101-∞ 用量时单价为 $1 。则当用户最终使用 90 用量时会被收取 90 * $2 = $180 ,而如果用户最终用量为 200 ,则会被收取 200 * $1 = $200 。很多场景都可以适用这个定价模式,例如你提供按坐席收费的 SaaS 服务。 36 | - 分级定价(Graduated pricing):这是一种阶梯式的定价策略。和批量定价类似的地方是分级定价也为不同用量区间设置不同的单价。而和批量定价不同的地方则是分级定价采用累进的方式计算。我们沿用批量定价中的例子,当用户最终用量为 200 时,分级定价最终的计算方式是 100 * $2 + (200 - 100) * $1 = $300 。拿我们生活中的例子来讲的话,就类似个税的计算方式。可以看到**分级定价比批量定价会更加合理**,不会出现在临界点上用量更多反而收费更少的情况。 37 | 38 | 注:一次性付款模式下也支持 Standard pricing 和 Package pricing ,但一次性付款属于事前支付,与订阅制收款需要发送用量给 Lemonsqueezy 并事后收款的逻辑不同。 39 | 40 | ### 营销 41 | 42 | - 试用期:Lemonsqueezy 支持为订阅制收款设置免费试用时长。用户在下单订阅时仍然需要输入有效的支付方式但不会马上被扣款,而是在免费时长用完后才会被扣款。在 Lemonsqueezy 发送给我们平台的回调中会指明该订阅当前状态为试用,你可以以此为依据来控制用户的权限范围。 43 | - 联盟佣金(Affiliate):联盟佣金就是一种**推广购买返佣**的营销方式。Lemonsqueezy 支持整个联盟佣金流程。推广者可以直接通过 Lemonsqueezy 申请参与 Affiliate 计划,并在你批准后获得一个链接。经由该链接达成的成交会从你的收入中分润一定比例给推广者。 44 | - 折扣码:可设置比例折扣或固定金额折扣,还有起止时间和适用产品范围。此外还可以设置可供兑换的次数,以及对订阅制收款是否连续生效,生效多少次。当折扣设置为 100% 时,也可以用做发放免费试用资格。 45 | - 线索收集:Lemonsqueezy 支持一种免费的订单形式,称为 Lead magnet 。用户仍然需要正常走完订购流程,你的平台也会收到订单回调。但用户无需付费,仅需提供邮箱地址。这种方式和提供免费试用期不同,**并不收集支付信息**,所以无法产生后续付款。 46 | - 邮件营销:由于 Lemonsqueezy 在收款时会收集用户的邮件地址,因此可以做一些邮件营销的动作。除了基础的账单、订单通知邮件,Lemonsqueezy 还会自动帮你发送召回邮件。例如当用户付款失败、续费时支付方式失效、**进入支付界面后却放弃了支付**时。此外你也可以**手动创建邮件广播**,向所有你的付费用户发送邮件。 47 | 48 | 在营销上玩法上,这些海外产品都相对比较”朴素“,没有国内那么多花样。 49 | 50 | ### 我们的使用场景 51 | 52 | - 连续订阅 53 | 我们将月付和年付做成了两个不同的 Products ,分别在里面设置了 Standard 和 Pro 两个 Variants 。这样的做法用户可以分别在月付和年付的情况下在 Standard Plan 和 Pro Plan 间切换,但是无法在月付和年付之间切换。 54 | 虽然把月付和年付做成一个 Product 中的不同 Variants 是可以让用户直接进行切换的,但这会带来一些比较复杂的情况,而且也不是我们所期望的所以没有做。主要原因在于年付本身就是一种预付打折模式,如果用户可以在享受更加优惠的年付价格一段时间后就切换成月付,然后取消订阅并不是我们希望发生的情况。同时对于有月付切换年付需求的用户,仅需取消月付订阅,等待当前月付到期后重新订阅年付即可。 55 | 当然从更好体验的角度出发,最好是能支持月付向年付的切换同时禁用年付向月付的切换。但 Lemonsqueezy 不支持这样的配置,你需要自己编写额外的逻辑来进行处理。 56 | 57 | - 一次性会员资格 58 | 为了获取国内没有双币信用卡的客户,我们希望支持支付宝 / 微信支付的付款方式。由于 Lemonsqueezy 订阅制收款的时候并不支持支付宝 / 微信支付,因此我们做了一个一次性会员资格的 Product ,购买后一次性开通一定时间的会员资格。这里需要注意的是,订阅制的订单是有生命周期的,Lemonsqueezy 会通过回调向我们的平台更新状态,我们就可以依赖这个回调来更新用户的会员资格是否有效。但一次性购买的订单是没有过期这样的生命周期的,就需要我们自己来监控用户会员资格的失效。 59 | 60 | - 额外额度包 61 | 这也是一个一次性购买的 Product ,在用户成功购买后我们会为用户增加对应数量的永不过期的转录配额,但仅能在会员订阅有效期内使用,会员失效后会被冻结。值得一提的是在 Lemonsqueezy 的官方文档中,order_create 的回调中并不包含购买数量的字段,但实际是包含的,所以是支持一次性购买多份的。 62 | 63 | ### 不支持或不完全支持的场景 64 | 65 | 我们在做 Podwise 的过程中设想过一些场景,最后发现 Lemonsqueezy 的现有能力并不能支持。甚至有部分在我们邮件询问 Lemonsqueezy 官方得到可以支持的答复,并写完实现代码后,才发现和我们想的并不一样。在这里分享出来也能避免大家踩到一样的坑。 66 | 67 | - 赠送订阅时长 - 不完全支持 68 | 我们最初设想了一个邀请新用户送订阅时长的机制,希望促进用户的自发传播。在我们的设计中,当被邀请用户注册并订阅后,将会赠送被邀请用户和邀请者每人各半个月的订阅时长。尽管 Lemonsqueezy 没有赠送订阅时长的 API ,但有一个暂停订阅的 API(非取消订阅)。我们在向 Lemonsqueezy 官方邮件询问后也得到了这个暂停订阅的 API 可以暂停订阅计费的答复,于是开始着手实现这个功能。可最后我们发现它和我们想的并不一样。 69 | 简单来说就是暂停功能**并不会**按暂停的时间对应的推迟下个账单日,或者计算差价后在下个账单日减少收款金额。暂停功能的真实工作方式是:**如果这个订阅到达账单日时处于暂停状态,则这次账单就直接跳过不收钱**。也就是说它可以一定程度上起到赠送订阅时长的作用,但仅能赠送当前订阅周期的整数倍。这和我们最初的设想完全不同,例如我们无法做到仅赠送半个月时长,或者我们无法为年订用户赠送不足一年的订阅时长。也许你可以通过自己存储赠送时长,并在用户当前订阅周期快结束时,组合暂停、改变账单日、恢复等 API 做到这件事情,但它无疑会**非常复杂且容易出现问题**。我们最后放弃了这个机制并下线了这部分代码。 70 | 71 | - 每月固定订阅费用 + 按量计费 - 支持但和设想不同 72 | 有机构用户反馈即便 Pro Plan 提供的转录配额也不够他们使用,因此我们就考虑能否在现在每月固定订阅费用的基础上叠加一个对超出配额部分按量计费的方式。在仔细研究了 Lemonsqueezy 提供的计费模型后,发现与我们设想的并不相同。 73 | Lemonsqueezy 在订阅制的分级定价(Graduated pricing)模式下是支持这种方式的。这个定价模式支持为每一级用量单独设置单价和保底价格(Flat fee)。假设我们的 Pro Plan 的订阅费为 $19 ,提供的配额为 50 次,超出的次数每次额外收费 $0.3 。那我们只需要按如下方式设置分级定价即可满足我们的需求:第一级范围 0-50 ,单价 $0 ,保底价 $19 ;第二级范围 51-∞ ,单价 $0.3 ,保底价 $0 。 74 | 与我们设想并不相同的部分是收款的时间点。一般订阅是前置收款的,也就是当新的订阅周期开始时,就先对用户进行了扣款。而分级定价属于按量计费(usage-based billing)模式,它是**后置收款**的,是在一个订阅周期结束后按实际用量再对用户扣款。这个方式在月订的情况下不是大问题,但是在年订情况下会是一个非常大的问题。等于你要在整整一年后才能收到包含固定订阅费用和额外用量费用的第一笔钱,账期很长,风险也很大。 75 | 理想的方式是基础订阅采用前置收款,然后不论基础订阅是月订还是年订,额外用量都按月计算扣款。如果想做到这种方式,一种可行的办法就是**让用户订阅两次**。第一次订阅普通的 Pro Plan ,如果不够用,则再订阅一个 Flat fee 为 0 的 Graduated pricing 的月订产品。我们的平台在向 Lemonsqueezy 发送用户用量时,先行扣除包含在 Pro Plan 中的免费额度。 76 | 不过我们最终这两个方案都没选,而是选择了出售额外的额度包。 77 | 78 | - 退款同时关闭订阅 - 需部分自行处理 79 | 我们可以在 Lemonsqueezy 的控制台为用户进行退款操作。当为用户成功退款后,用户订阅的会员资格应该不再可用。Lemonsqueezy 对此的逻辑是:如果该订阅仅有一次付款,则会失效整个订阅,如果该订阅已经有多次付款,退款其中一次,**订阅仍然会继续有效**。同时在 Lemonsqueezy 的控制台中仅支持取消订阅(取消订阅的意思是取消连续付款,下个账单日不再续费,并不会即时使订阅失效),不支持关闭/过期订阅。因此我们需要自行接收 subscription_payment_refund 事件的回调并处理会员资格问题。 80 | 81 | - 部分退款 - 不支持 82 | 有时候因为一些特殊情况,用户已经不符合退款政策但我们仍然需要为他退款。此时我们会更希望按照用户的实际使用情况仅部分退款。但 Lemonsqueezy **仅能整单退款**,无法做到自定义退款金额进行部分退款。没有 workaround ,只能和用户沟通让用户通过另外的方式支付差额。 83 | 84 | ### 遇到过的问题和解决 85 | 86 | - 产品突然变成免费 87 | 非常离谱的一个问题。有个用户向我们反馈订阅付费时显示免费,我去检查后发现不论你如何修改产品的定价模式,最终呈现给用户的效果都是 Lead magnet 的效果,也就是仅需输入邮箱即可完成购买流程。多方尝试无果后,不得已将出现问题的产品隐藏并创建了新产品,同时修改了平台代码。随后联系了 Lemonsqueezy 的 support 途径,最终在好几天后收到了他们内部数据出现问题的答复。 88 | 89 | - 回调时不携带 custom data 90 | 这个问题我们遇到过不止一次。为了将 Lemonsqueezy 那边的购买行为和我们平台自己的用户关联起来,我们一般都会在发起支付的时候把 userId 塞给 Lemonsqueezy 作为 custom data ,以便在回调的时候拿到 userId 。但有好几次 Lemonsqueezy 给到的回调中 custom data 失踪了,同时我们也没有开启独立商店,用户仅能通过我们平台的入口进行购买和支付。这个问题造成的原因未知,我们的解决方法是用订单中用户的邮箱地址再查一次 userId 。但这不是一个足够安全的解决方案,因为用户在支付时使用的邮箱可能和在你平台注册时使用的不相同。更好的解决方案是做一个对账程序,每天将你的数据和 Lemonsqueezy 这边的数据进行对账,找出问题数据。 91 | 92 | - 回调丢失 93 | 遇到过两次应该来的回调没触发,结果用户会员状态不正确的问题。除了对账之外没有好的解决办法。 94 | 95 | 如何联系 Lemonsqueezy 的 support ? 96 | 97 | 这个入口藏的比较深。首先从 Header 或 Footer 中找到 Help / Help Center ,进入后点击页面中间的 form 右下角的 Get help 按钮,选择 Contact Support 提交表单。或者你也可以选择 Slack 按钮加入 Lemonsqueezy 的 Slack 频道进行沟通。 98 | -------------------------------------------------------------------------------- /pages/build/buildstack.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: 用方便且擅长的技术栈 3 | --- 4 | 5 | **先说最终选型结果,只列比较关键的。** 6 | 7 | - Web 前端:NextJS 、TypeScript 、TailwindCSS 、shadcn/ui 8 | - Web 后端:NextJS 、TypeScript 、Prisma 9 | - 纯后端服务:Golang 10 | - 移动端(计划中):CapacitorJS 11 | 12 | 虽然在选型过程中也有一些 feature 上的考量,但最核心的原则还是:**选择擅长且方便的。** 13 | 14 | 很多人在做一个新项目的时候会倾向于选择新技术。虽然有时候新技术本身能带来一些优势,但因为我们不熟悉这项新技术,或新技术本身不成熟而存在的问题,都会导致我们在这方面付出很多时间成本。而除非新技术带来的优势就是你的产品的竞争力,否则这些优势几乎不可能超过你因为使用新技术而产生的额外成本。 15 | 16 | **Web 前后端为什么选择 JS/TS 技术栈?** 17 | 18 | 我们团队比较熟悉的技术栈是 JavaScript/TypeScript 、Golang 、Java 、Ruby 、Rust 。对于开发 Web 应用来说,显然使用 JS/TS 全栈一站式搞定是最快捷方便的。尽管 Java 、Golang 包括 Rust 在性能方面相对 Node 会更有优势,但对于大部分开销都在数据库和网络 I/O 上的 Web 后端来说并没有多大意义。另外 Web 前后端开发由同一个人承担,那使用同样的技术栈肯定也会更加方便。所以 Web 前后端就决定使用 JS/TS 技术栈。 19 | 20 | **为什么选择 TS 而非开发速度更快的 JS ?** 21 | 22 | JS 在一人工作以及 POC 阶段效率确实很高,但当工程开始变得复杂且经过长期迭代之后,TS 会更具优势。当工程已经颇具规模之后再切换到 TS 会是一个很痛苦的过程。我一般仅在知道工程规模会长期保持轻量的时候选择使用 JS 。 23 | 24 | 很多人被 TS 灵活的类型系统搞的无所适从,花费很多时间在类型适配上,或把 TypeScript 写成了 AnyScript 。但实际上对于上层应用开发来说,我们大部分时候只需要使用第三方包暴露的类型就够了。同时我们也可以通过一些技巧来提取未被暴露的类型,例如使用 Utility Types `Awaited<>` `Parameters<>` `ReturnType<>` 等。 25 | 26 | **为什么选择 NextJS 作为 Web framework ?** 27 | 28 | NextJS 提供了一些吸引人的特性,例如:前后端同构、SEO 友好、ServerAction 、Vercel 快速部署等。当时我们因为已经脱离一线开发一段时间了,其实对当时主流 Web framework 的细节也并不了解,在快速评估了 NextJS 、Astro 和 Remix 之后,我们认为 NextJS 更加符合我们的需求。尽管这个选择帮助我们快速开发并上线了 [Podwise.ai](http://podwise.ai/) ,但也同样给我们带来了不少麻烦,在后面我们会讨论这些麻烦。 29 | 30 | 前后端同构在一人开发时效率很高。举例来说,所有接口的出入参类型你都只需要定义一份,而无需在两种不同技术栈下分开定义。当你需要修改接口时,同一份类型也可以确保你同时修改两边而不会发生遗漏。 31 | 32 | SEO 作为一种出海场景下有效且相对便宜的营销获客手段,是非常必要的。如果你的 SEO 内容都是 blog 文章,那完全可以为 blog 站点选择一个独立的技术栈,或者干脆使用 medium 这样的服务作为你的 blog 站点。但当你的应用内容本身就需要被 SEO 时,为你的应用开发选择一个支持 SEO 的技术栈就比较有必要。 33 | 34 | Astro 本身定位自己是用来做 “content-driven websites” ,内容驱动网站,也就是说像营销站点、文档站点、blog 、landing page 这种场景。对于功能比较复杂的应用网站,Astro 官方也并不推荐使用 Astro 来构建。 35 | 36 | Remix 看起来是一个不错的选择,但它的 action 将一个页面的所有请求混合在一起的写法让我有点不太能接受,而当时 NextJS 的 ServerAction 看起来会更加吸引人。 37 | 38 | **纯后端服务为什么选择 Golang 技术栈?** 39 | 40 | 1. **深度的 Go 经验和积累** 41 | 42 | 对于做应用层的产品创业,根据自己或团队选择最熟悉,最擅长的编程语言的决定是一定不会出错的。对于独立开发者更是如此。podwise 是一款 AI 播客应用产品,所以我们也选择了积累最深,经验最丰富,使用起来最顺手的 Go 语言开发了 SaaS 的后台服务。这绝对是一个正确的选择,只管专注开发产品功能,解决问题,快速构建自己想要的基础设施,这一切都不需要花任何时间在学习、摸索、解决未知问题上面,只需要将自己大脑中的知识和经验倾注而出就可以完成。(当然,从某种方面来说,这或许有点无聊) 43 | 44 | 我们根据自己的积累、经验和能力,选择了最保险的方案。我们也极力推荐大家采用这一准则进行编程语言的选择。但你并不一定完全需要遵照这一准则。有很多的领域或者产品类型,你甚至可能需要靠其他的指标和维度来做选择,比如你正在构建的是需要高性能的 infra 产品,那你有可能会选择像 Rust,C/C++ 这类编程语言,哪怕你并不是很擅长它们。 45 | 46 | 2. **资源** 47 | 48 | Go 比 Python 可以消耗更少的资源。如果你有 “白嫖” 过 AWS 的免费 EC2,你会发现这些免费的 ec2 只有 1vCPU 和 1G 内存。cpu 其实还好,毕竟作为独立开者新发布的产品,可能也没多少人使用,也没什么流量;但 1g 的内存极有可能会是一个硬伤,1g 内存被操作系统占用后,剩下给到你的也就是只有几百兆了,这远低于我们今天使用的笔记本电脑,像 python、java 这类带虚拟机的编程语言,一启动就可以占用上百M、甚至几百M 的内存,所以 1g 的 “白嫖” ec2 总体来说是捉襟见肘的。 49 | 50 | Go 语言在这方面的表现就是非常优秀了,轻松把内存稳定控制在几十M 内,只要我愿意做,控制在十几M 也不是不可以。 51 | 52 | 资源就是成本,是钱。作为独立开发者或startup,我们可以不在意编程语言的性能,但不能忽视资源消耗的多少问题。特别是刚起步的时候,能够让你的产品轻松的部署在任意环境,节省每一分钱都是有意义的。 53 | 54 | 3. **部署运维** 55 | 56 | Go 程序具有极大的可部署运维性。除 Go 以外,其他的所有主流编程语言的程序或多或少都需要解决部署环境的依赖问题,当然在 docker 出现以后,这个问题明显减少了。 57 | 58 | 大家是否想过,云计算领域诞生了云原生技术,驱动云原生应用开发的云原生技术,为什么大多数都采用了 Go 开发,包括 docker,kubernetes 等等。这其中就有 "Go 程序天然具备高可运维性" 的功劳。这种可运维性,给我们节省了大量的琐碎时间,在 Mac 电脑上采用交叉编译成一个二进制文件,然后一键将二进制文件 copy 到任意机器上就可以直接运行,甚至都不需要事先在目标机器上安装任何软件和库。 59 | 60 | 这种极大的部署、发布便利性,不光是节省了大量的零碎时间,甚至都让我忘记了 CI/CD。 61 | 62 | 我们需要 CI/CD 吗?作为刚起步的独立开发者和 startup,我可以负责任的说 “你不需要 CI/CD,更不需要 DevOps,你只需要方便和快捷”,不要害怕自己是草台班子。方便快捷的 “实现代码,跑完测试,部署发布”,这一切都可以在你的笔记本电脑上完成,这是最快,最方便的方式,超过了任何 CI/CD。 63 | 64 | 4. **并发** 65 | 66 | Go 具有比 Python 强大得多的并发能力,这种强大的并发能力不只是 “cpu上的效率” ,还体现在 "语言级别原生支持 goroutine 的语法表达能力"。这就是编程体验和性能都双双兼得。 67 | 68 | 并发为什么重要?Podwise 后台服务有太多的地方需要并发处理: 69 | 70 | - podcast 节目同步需要并发 71 | - 节目进行 AI 处理需要并发 72 | - 长 transcript 文稿分片后,需要并发总结 73 | - 等等 74 | 75 | AI 模型已经足够慢了,好的并发流程设计可以避免产品陷入 "更慢" 的窘境。所以,值得我们使用更好的并发语言。 76 | 77 | 5. **开发效率和性能平衡** 78 | 79 | 选择 Go 就选择了开发效率和性能的平衡,较高的运行时性能和非常不错的开发效率都兼顾了。但这个理由对独立开发者的一般应用来讲,我觉得不是特别的强烈和重要。理由有二: 80 | 81 | - 再好的开发效率,也抵不过自己真正擅长的编程语言。几年前,在阿里云 cdn 团队的时候,要开发一个面对大流量的安全防御系统,一部分人主张用 Go,但另外有个别人主张用 C ,主张用 C 的同学在平时开发中已经用事实证明他用 C 的效率同样很高。 82 | - 一般的独立开发者产品,很难有大流量的机会要靠编程语言的性能来顶,一般来说优化/调整一下逻辑就足够好使。 83 | 84 | 我把这段话写到这里的目的,是为了更好的告诉开发者们,一般情况不用痴迷于编程语言,选择自己最擅长的,真正能拿捏的语言就是最好的选择。 85 | 86 | 当然,我们应该追求个人工具箱里有多门擅长的,真正掌控的语言和工具栈,这能让你在构建产品的过程中做到真正的随心所欲。虽然我们没有选择用 Python 作为 SaaS 后台服务的核心语言,但构建 AI 产品的过程中,我们还是会碰到 Python,多语言本就是产品开发过程中的常态。 87 | 88 | 89 | **为什么不是 langchain** 90 | 91 | Podwise 是一款 AI 产品,技术实现层面涉及了 LLM 调用、prompt 和 prompt 管理以及长文本处理等等 AI 总结相关的技术模块。从道理上来讲,我们可以直接选择 Langchain 作为 AI 应用框架。但事实上,Podwise 最后并没有选择 Langchain,而是使用了最擅长的 Go + LLM API 直接实现 AI 部分。 92 | 93 | 1. **认识 langchain** 94 | langchain 框架有这样的几个核心部分:model IO,Retrieval,Chain,Agent和memory。 95 | - model io 模块提供了访问 LLM 相关的能力,其中包括 prompt 的编写和管理,LLM 调用接口以及 LLM 返回结果的 parser (也就是 output parser)。这部分是最符合我们的直觉,也非常容易理解,本质就是写一个 prompt,然后调用 llm 提供的 api,最后再解析返回的结果(返回的结果可能是一个 json,也可能是一个 markdown 或者一个纯文本字符串等等)。 96 | - retrieval 模块用来处理外部输入的数据,比如对一个 pdf 文档做总结,这个 pdf 文档就输入外部输入数据。这部分包括有文档 loader,文档拆分以及 embedding 。 97 | - Chain 模块属于最核心的模块,但并不难。我们所有的开发者在实际业务开发中,大概率都实现过自己的 workflow,也就是一个流程。Chain 本质就是一个链条,对一个流程的抽象。有了 Chain 这个抽象层,我们就可以自定义各种各样的 AI 工作流,处理各种任务。任何一个稍微有点经验的程序员,都可以轻松徒手撸一个简单灵活的工作流引擎。 98 | - Memory 理解成记忆,不是内存。memory 的作用就是存储运行期的数据状态,一些中间结果,比如chatbot 的对话记录等等。 99 | - agent 可以调用一些 tool 执行外部任务。 100 | 101 | 对于 Podwise 的业务场景来说,除了 agent 其他几个部分都会涉及,从工作原理的角度,每一个部分本质上都非常简单,简单到根本不需要堆叠一层厚厚的抽象概念,反而实现一层最朴素的wrapper 作为工具库是最简单,最易于理解的。 102 | 103 | 2. **langchain 总结场景** 104 | 105 | ![Untitled](/attachments/build/langchain.png) 106 | 107 | 上图来自 langchain 官方文档,它展示的是基于 langchain 实现文档总结的工作流程。 108 | 109 | "加载文档 -> 拆分文档 -> 编写 prompt -> 调用 LLM -> 解析获取输出结果",整个过程跑在一个 chain 链条上。 110 | 111 | 可惜的是,这个 chain 的每一个步骤,在 podwise 上都需要自定义,并不能直接使用默认提供的工具方法,比如第一个步骤 “加载文档” ,langchain 提供各种文档的 loader (比如:pdf 文档,markdown、csv、json 文档等等),但在 podwise 实际业务中,需要加载的数据直接来自 whisper 的结果,也可能直接来自某一个语音转文字的服务的 api 等,所有的这些数据源,往往都无法直接使用 langchain 的默认 loader,需要采用自定义机制实现自己的 loader。 112 | 113 | 又比如,第二个步骤的 “文档拆分”,langchain 也提供了多种对长文档进行拆分的方法,比如按文档大小拆分等等,但对 podwise 来讲,这些拆分方法都太简单粗暴了,无法从内容的上下文逻辑上进行长文档的拆分,所以这个地方又只能采用自定义实现。 114 | 115 | 到最后,为了更好的质量和效果,会发现大多数情况都要按照自己的业务场景进行自定义实现。那么,框架就只是做了封装、对接 infra 层面的组件。对于一个有经验的程序员,封装抽象 infra 恰恰是最简单,最不需要探索的事情(当然,每个人的实际情况不一样,对我来说,可能就非常擅长写这种封装抽象类的框架代码)。 116 | 117 | 1. **框架的不可控性** 118 | 119 | 任何一个你不熟悉的新框架,内部都会隐藏着巨大的不可控性。我们没有使用 langchain,在集成 AI 能力这个部分,遇到 “不可控情况” 就非常少。比如我们采用 next.js 作为前端框架,那就遇到了一些不可控的情况。 120 | 121 | 框架往往是复杂场景下的产物,快速打造应用层产品的 mvp,往往只需要最直接,最朴素的解决方案;引入一个厚厚的框架层,唯一的作用就是拉大你和你的目标之间的距离,解决你的目标问题之前,往往需要你先解决框架的问题。(注意:这里提到的框架都是不熟悉的新框架,那些实际业务中久经考验的框架不在此列) 122 | 123 | BuzzFeed 的数据科学家 Max Woolf,也写过一篇很火的文章,叫 “我为什么放弃了 Langchain”,里面就有很多工程实践上的细节问题,各种不可控性,供参考 https://mp.weixin.qq.com/s/Iwe6M391b2BBWae-HmOIJQ 。 124 | 125 | 以上基本就是我们为什么不选择 langchain 的思考。注意,我们并不是在批判 langchain,至少不是像 Max Woolf 那样,也不是教唆大家不要选择 langchain。而是展示我们对框架选择的思考方式 —— 首先是学习框架,然后将业务流程带入框架写一个简单的原型,最后再结合未来的产品发展进行评估,权衡框架带给你什么,你需要付出的风险成本又是什么。 126 | 127 | 128 | **其它选型:** 129 | 130 | - TailwindCSS 的就地编写样式的方式带来了非常高效的开发体验,免除了在样式文件和 TSX 文件之间反复切换并查找彼此关系的过程。选择 TailwindCSS 并不代表你就放弃了对样式的抽象和复用,你仍然可以命名并复用你的样式。但我的经验是仅在真的有必要时才这么做。尽管可能你的强迫症会让你不太愿意接受到处散落的 class name ,但一旦接受之后你会真正感受到效率提升带来的愉悦。 131 | - shadcn/ui 是一个优秀的 React UI 组件库,样式简洁美观,组件可以按需安装和更新。更重要的是它基于 TailwindCSS 编写样式,并通过复制的方式安装到你的工程中。因此我们可以很轻松的使用 TailwindCSS 来覆盖样式,或直接修改代码来改变它的默认样式和行为。除了 shadcn/ui 之外,radix-ui themes 和 NextUI 也是不错的组件库选择。 132 | 133 | **这个技术选型下遇到的问题。** 134 | 135 | 最大的问题来自对 NextJS 和 CapacitorJS 的不熟悉。原本我们计划使用 CapacitorJS 来包壳开发 App ,避免重复同样的业务逻辑开发。对于小团队来说,在多端用不同语言重复实现的时间成本有点不可接受,同时也是非常不利于体验一致和长期维护的。 136 | 137 | 但 CapacitorJS 需要被包壳的 Web 应用是一个标准的 SPA(Single Page Application),而 NextJS 是一个对 RSC(React Server Component)和 SSR(Server Side Render)高度优化的框架。尽管 NextJS 也可以用于开发 SPA 应用,但很多 NextJS 的有用特性就无法使用了。我们在最初并没有关注到这个限制,结果直到开始准备包壳的时候才发现。这直接导致我们需要对整个 Podwise 的 website 应用进行改造和重构,才能适用 CapacitorJS 来包壳。与此同时,我们还要确保 web 端的 SEO 内容可以被继续输出。目前我们计划通过自定义打包脚本,在尽可能复用代码的同时保留一定的差异化。 138 | 139 | 一个和 App 包壳相关联的问题是 NextJS 提供的 ServerAction 特性。ServerAction 可以让开发者以类型安全的方式,像调用本地方法一样从 browser 端调用 server 端的接口。ServerAction 使用起来非常方便,然而这也同样是一柄双刃剑。ServerAction 本身不被 NextJS 的静态导出方式(导出成 SPA)所支持,同时也不是一个标准的 RestAPI ,无法被其它二方或三方调用。我们最初用 ServerAction 用的多爽,当决定弃用它的时候就有多麻烦。 140 | 141 | Web 启动是比较轻量的,也便于试错。但如果你在后面的计划中有 App 的需求,那么我会非常建议在动手 Web 端编码之前先预研一下后面的 App 方案。 142 | 143 | 此外 Vercel 作为我们的主要部署平台,也给我们带来了一些麻烦。像 Vercel 以及 Netlify 这些类似平台,都使用 AWS lambda 作为他们 Serverless 能力的底层设施。AWS lambda 在安装它的基础镜像不包含的依赖库时需要使用自定义 layers 来完成,而 Vercel 和 Netlify 等都不支持自定义 layers 。这就导致当我们开发的功能需要使用到一些 AWS lambda 基础镜像中并没有预先安装的依赖库时,无法被成功部署到 Vercel 或 Netlify 中。例如我们的 DAI(Dynamic ADs Insertion)检测功能需要使用到 `libasound.so.2` 而 AWS lambda 基础镜像中没有,我们尝试了很多种方式都没有成功完成部署,包括 Vercel 的官方 support 给出的结论也是需要等待他们支持 layers 才可以。最终我们只能把这部分功能剥离出来单独部署,我们选择了 zeabur 来部署这一小部分。 144 | 145 | **在经历这一切之后重新选择技术栈的话,我们会怎么选?** 146 | 147 | 绝大多数选择我认为都没有问题,而 Web framework 我想我会考虑一下 Remix 。Remix 看起来能很好的满足 CapacitorJS 所需的 SPA 结构,无需做什么特殊处理。同时 Remix 也具备前后端同构,SEO 友好的特点。也同样能非常方便的在 Vercel 或 Netlify 等主流平台上部署。 148 | 149 | 对于我们遇到的 Vercel 无法安装额外依赖库的问题,我并不会因为这个问题而选择不使用 Vercel 或类似的 Netlify 等平台。Vercel 这样的一站式平台带来的其他价值完全值得我们继续选择它,能大大提升产品的迭代交付速度。 150 | 151 | **我们的技术选型建议:** 152 | 153 | - 选择你擅长的,而不是选择最新的 154 | - 在 PMF(Product Market Fit)之前,可以只关注开发效率;在 PMF 之后,应当考虑长期的可维护性 155 | - 善用基础设施 SaaS 服务,尽量只关注开发本身 156 | - 不要在技术服务上花过多的成本,能省一分是一分 157 | -------------------------------------------------------------------------------- /pages/appendix/capacitor_p1.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Capacitor & Next.js 开发 App 实战 Part I 3 | --- 4 | 5 | ## 前言 6 | 7 | >❗说在最前面:这并不是一个好的技术栈选择(Capacitor 不是主要问题,问题是 Capacitor + Next.js 的组合),你可能会碰到很多未知的问题。只能说在当时的我们看来它是一个还可以的选择。如果你也在考虑这个技术栈,可以看看这篇文章。 8 | 9 | 作为独立开发者或者小团队,在上 Mobile App 的时候,由于开发资源有限,一般都会考虑跨平台方案。例如 ReactNative 、Flutter 等,也包括今天说的 **Capacitor** 。 10 | 11 | Capacitor 是 Ionic Team 做的,前身可以认为是 Cordova ,本质上是 Web 包壳。相对 ReactNative 或 Flutter 这样的技术来说,Web 包壳在体验上可能会稍差一些,但好处是可以直接复用 Web 版的应用,或者其中的很多代码。 12 | 13 | 但 Capacitor 也有自己的适用范围,并不是所有 Website 都可以不做任何改造就用 Capacitor 包装成 Mobile App 。其中一个限制就是被包装的 Website 必须是个**纯静态站点**。当然这里所说的纯静态站点并不是指不能有动态内容,而是不支持像 SSR 那样的服务端动态内容。换句话说,你的应用可以是一个 SPA ,或者多页但所有数据交互和呈现都通过 Ajax + 客户端渲染来完成。 14 | 15 | 这个限制对 Next.js 是一个很大的问题,因为 Next.js 的默认范式就是 SSR 优先的,而我们有时候也确实会更倾向于使用 SSR 。那如果我们一开始选择了 Next.js 来构建我们的 Web 版 App ,并且大量采用了 SSR ,我们还能用 Capacitor 包成移动端应用吗?如果可以,怎么才能更简单更高效? 16 | 17 | Podwise 就遇到了这个问题。Podwise 最初为了快速上线选择了 Next.js 开发 Web 版本,同时用响应式的方式支持了移动端浏览器打开的可用性。后续随着用户量增加,用户对 Mobile App 的呼声也越来越高,我们就打算选择 Capacitor 包装的方案。随后我们发现网络上几乎没有真正可用的 Next.js + Capacitor 的教程,能找到的都只是最简单的 demo 场景,根本不涉及到 SSR 改造这样的核心问题。但好在我们在一路摸索中解决了不少问题,已经基本完成了 Mobile App 的开发。在这里把这部分经验分享给大家,这并不一定是最佳实践,权当抛砖引玉供大家参考。 18 | 19 | --- 20 | Next.js + Capacitor 开发 App 的内容会分为多个部分。Part I 是 Next.js 工程改造,适配 Capacitor ;后续部分我们会继续分享如何对接各种原生能力,以及如何让 App 在移动设备上拥有更好的体验。 21 | 22 | ## Part I - Next.js 工程改造 23 | 24 | 我们需要使用 Next.js 的 static exports 功能来将整个应用导出成一份不含服务端逻辑的纯前端工程。static export 的文档可以参考这里:https://nextjs.org/docs/app/building-your-application/deploying/static-exports 。 25 | 26 | 然后不出意外的,这里列出了一堆 Next.js 在 static exports 下不支持的特性,很多都非常常用,需要我们进行处理。让我们一项一项来。 27 | 28 | ### Dynamic Routes with `dynamicParams: true` and without `generateStaticParams()` 29 | 30 | 这指的就是动态的 SSR 。典型例如我们通过 `app/items/[itemId]/page.tsx` 来开发一个商品展示页面,Next.js 会在每一次用户访问 `/items/{itemId}` 的时候,动态在服务端获得 itemId ,并通过 SSR 生成页面内容。由于 static exports 会导出纯静态内容,不存在服务端,因此这项特性就无法被使用。 31 | 32 | 使用了 `generateStaticParams()` 的 Dynamic Routes 是可以在 static exports 的时候支持的,因为 `generateStaticParams()` 在 build 的时候就枚举了所有可能的 Dynamic Segments(在我们的例子中是所有的 itemId),并为每一个路径生成了静态的 html 文件。但这样我们就失去了动态性。 33 | 34 | 因此我们需要使用 CSR 的方式来动态获取数据并渲染页面。 35 | 36 | 如何开发 CSR 的细节不在本文的讨论范围内,不详细展开。我们来聚焦几个需要决策和解决的问题。 37 | 38 | #### 1. 我应该把整个工程改造都改造成 CSR(即在 Web 实现中也使用 CSR),还是仅针对 Mobile App 改造 CSR ? 39 | 40 | 在 Web 端使用 SSR 有两个好处。其一是**对 SEO 友好**,其二则是**对用户体验友好**。如果你计划让 Search Engine 成为你的一大流量来源,那么通过 SSR 来向搜索引擎喂数据,以及提高搜索引擎对你的站点的权重就非常重要。此外 SSR 能在一次 http 交互中返回可供用户查看的内容,对用户体验会带来一定的好处。 41 | 42 | 如果这两点都不是你关心的,那么把整个工程都改造成 CSR 也不错,否则你就需要为 Web 端保留 SSR ,然后对 Mobile App 端进行 CSR 改造。这会让你需要使用两份代码。 43 | 44 | 我们选择了为 Web 端保留 SSR 的方式。针对需要使用两份代码的问题,我们目前是这么实践的: 45 | - 为 Mobile App 再创建一个独立的 Next.js + Capacitor 工程,并通过自定义脚本选择性的从原工程中同步文件。这些文件主要是各种页面和 UI 组件,以及静态资源等。不包括例如 `route.ts` 这类提供 API 的文件或者其它 server 端的文件。 46 | - 对 SSR 的 `page.tsx` 进行改造,将实际页面内容剥离成独立的 `page_content.tsx` 组件,`page.tsx` 仅承担读取数据并传入 `` 的职责。在 Web 端工程中,`page.tsx` 通过 Server Component 的方式直接以 async/await 的方式读取数据;在 Capacitor 工程中,`page.tsx` 使用 swr 用 hooks 的方式读取数据。因此在两个工程中仅 `page.tsx` 文件不同,且两个文件中都只有几行获取数据的代码,不包含别的逻辑。 47 | 48 | #### 2. 解决两边内部 url 不一致的情况 49 | 50 | 由于 static exports 不支持 Dynamic Segments ,因此 `/items/{itemId}` 这样的 url 无法使用,需要被改造成类似 `/items/index?itemId={itemId}` 这样使用 query params 的形式。这会导致在应用内部跳转时,Web 端和 Mobile 端的目标地址不同。我们肯定不希望在每个内链的地方都使用两份代码,因此需要对 Next.js 的 Link 组件进行改造。 51 | 52 | 我们对 Link 组件进行了包装,将其 API 改造成了如下的方式: 53 | ```tsx 54 | 60 | Item 61 | 62 | ``` 63 | 64 | 在包装 Link 组件的内部,我们通过环境变量控制真实 href 是 Web 端还是 Mobile 端的拼接逻辑。当 Web 端时,segments 中的参数会替换 path 中的 `[itemId]` 占位符,最终输出 `/items/someItemId` 。当 Mobile 端时,path 中的占位符会被忽略,并在 path 后拼上 `segments?itemId=someItemId` ,最终输出 `/items/segments?itemId=someItemId` 。 65 | 66 | 也因此在 Web 端工程中,上述例子对应的 `page.tsx` 是 `app/items/[itemId]/page.tsx` 。而在 Mobile App 工程中,则对应 `app/items/segments/page.tsx` ,这属于我们自己的约定。 67 | 68 | 包装后的 Link 代码供参考: 69 | ```typescript 70 | import { IS_APP } from '@/lib/tools'; 71 | import NextLink from 'next/link'; 72 | 73 | type HrefConfig = string | { path: string, segments: Record, extra?: Record } 74 | type Props = Omit[0], 'href'> & { href: HrefConfig }; 75 | 76 | export function getHrefString(href: HrefConfig) { 77 | if (typeof href === 'string') { 78 | return href; 79 | } 80 | const { path, segments, extra } = href; 81 | let formattedPath = path; 82 | if (IS_APP) { 83 | for (const key in segments) { 84 | formattedPath = formattedPath.replace(`/[${key}]`, ''); 85 | } 86 | const query = new URLSearchParams(segments).toString(); 87 | let result = `${formattedPath}/segments?${query}`; 88 | if (extra != null) { 89 | const extraQuery = new URLSearchParams(extra).toString(); 90 | result += `&${extraQuery}`; 91 | } 92 | return result; 93 | } 94 | for (const key in segments) { 95 | formattedPath = formattedPath.replace(`[${key}]`, segments[key]); 96 | } 97 | if (extra != null) { 98 | const extraQuery = new URLSearchParams(extra).toString(); 99 | formattedPath += `?${extraQuery}`; 100 | } 101 | return formattedPath; 102 | } 103 | 104 | export default function Link(props: Props) { 105 | const { href, ...rest } = props; 106 | const realHref: string = getHrefString(href); 107 | 108 | return ; 109 | } 110 | ``` 111 | 112 | #### 3. 不要使用 Server Actions 113 | 114 | Server Actions 是 Next.js 提供的一种同构的进行前后端调用的技术,使用 Server Actions 可以让你像本地调用一样从 client 发起向 server 端的调用。Server Actions 不仅非常方便,省略了 API 层的代码,并且在使用 TypeScript 时还能为前后端之间的 API 调用提供类型安全的能力。 115 | 116 | 但是 Server Actions 也有其局限性,最大的问题是它只能在同一工程内被调用,不支持被外部调用。显然,我们的 Mobile App 无法通过 Server Actions 发起对 Web 端 server 的 API 调用。 117 | 118 | 为了避免在每个调用 API 的地方维护两套代码,我们建议**不要使用 Server Actions** 。 119 | 120 | 通过简单的封装,我们使用两个工具方法来为一个服务端方法生成 router handler 和 API caller ,并允许配置 server address 。借由这种方式,我们保留了类型安全调用的优点,并移除了绝大多数重复的制式代码。 121 | 122 | 两个工具方法的代码,有需要的朋友可以找我们索要。 123 | 124 | #### 4. 解决 API 调用时的跨域问题 125 | 126 | Mobile App 向 Web server 发起的调用默认是跨域的。因为你的 Mobile App 实际上跑在一个浏览器中,因此会受到浏览器的跨域安全策略限制。 127 | 128 | 解决办法是使用 Capacitor 官方的 http plugin 来发起请求。这个 plugin 会使用 native 能力来进行 http 请求从而绕过浏览器的跨域安全策略。具体请参考文档:https://capacitorjs.com/docs/apis/http 。 129 | 130 | 值得注意的是,Capacitor http plugin 在发送 request 的时候,header 中默认不会带上 cookie ,如果需要,要自己传入: 131 | ```ts 132 | const resp = await CapacitorHttp.request({ 133 | header: { 134 | Cookie: document.cookie, 135 | }, 136 | // other props 137 | }); 138 | ``` 139 | 140 | 另外这个 plugin 会同时 patch `fetch` API ,但它的内部实现细节仍然和 fetch 有所不同,你可能会遇到一些在 Web 端正常但 App 中异常的情况。 141 | 142 | 所以另一个可行的方式是不使用这个 Capacitor http plugin ,而是在你的服务端允许你的 App 跨域,然后沿用原生 fetch 的实现。但你要解决跨域传递 cookie 的问题。 143 | 144 | ### Router Handlers that rely on Request 145 | 146 | Router Handlers 即 `route.ts` 形式的路由。rely on Request 表示 router 根据请求不同进行不同的响应,因此是动态的。不依赖于 Request 的 Router Handlers 是可以被 static exports 支持的,它们会在 build 时被执行,然后输出成静态文件。 147 | 148 | Mobile App 不需要使用 Router Handlers(我们直接调用 Web 端的 Router Handlers 提供的 API ),因此只需要将这些 `route.ts` 文件从 Mobile App 的工程中排除掉即可。 149 | 150 | ### Cookies 151 | 152 | 这里指在 Server Components 或 Server Actions 或 Router Handlers 中读取 http requests 携带的 cookies 的 API 。由于改造后的 Mobile App 不会存在上述内容,因此也不会用到 server 端的 Cookies API ,也就无需特殊处理。 153 | 154 | ### Rewrites & Redirects & Headers 155 | 156 | 这三个都是 `next.config.js` 中的配置,都是在 server 端进行处理的,因此在 static exports 中不被支持。 157 | 158 | #### Rewrites 159 | 160 | 我们使用 rewrites 配置来将独立部署的 blog 挂载到主站的 /blog 路径下。这么做的目的是为了 SEO ,将所有内容都集中在一个域名下。 161 | 162 | 这个需求对 Mobile App 来说是不存在的,因此不需要处理,忽略即可。 163 | 164 | #### Redirects 165 | 166 | 我们用 redirects 配置来处理一些默认跳转逻辑,例如将 `/dashboard` 跳转到 `/dashboard/trending` 。 167 | 168 | 在 Mobile App 中我们通过增加一个 `app/dashboard/page.tsx` 路由,并简单在 `useEffect` 中使用 `router.replace('/dashboard/trending')` 来完成这个跳转逻辑。 169 | 170 | #### Headers 171 | 172 | Headers 主要为 API 服务,我们用 Headers 配置做了两件事:1. 设置 `X-Frame-Options` 为 `DENY` ;2. 为 Mobile App 调用 API 设置跨域头。 173 | 174 | 这两个需求和 Mobile App 本身都无关,因此在 Mobile App 工程中不需要处理,忽略即可。 175 | 176 | ### Middleware 177 | 178 | Middleware 也是在 server 端被执行的逻辑(在 deploy 到 Vercel 的时候,会放在 edge 执行),因此不被 static exports 支持。我们主要用 Middleware 做登录状态检查,当未登录时跳转登录界面或返回 401 。 179 | 180 | 除了 Middleware 中的未登录跳转,为了用户体验,我们在 Web 端也同时做了 API 返回 401 时的未登录提示和跳转。因为在 Mobile App 的实现中我们已经将所有页面改造成了 CSR ,通过 API 来获取数据,正好能适配上对这个 401 的处理。因此在我们的场景下,无需再在 Mobile App 中针对 Middleware 做改造,忽略即可。 181 | 182 | ### Incremental Static Regeneration & Draft Mode 183 | 184 | 我们没有使用这两个特性。 185 | 186 | ### Image Optimization with the default `loader 187 | 188 | 为了更快的图片加载速度,Next.js 可以对图片进行优化,按照实际显示大小来返回最优尺寸的图片。这个优化对本地图片和你配置允许的远程图片都可以生效,但这个优化的默认实现发生在 server 端,也因此在 static exports 下无法生效。 189 | 190 | 但在 Mobile App 场景下,所有工程中的本地图片都被打包在本地,不像 Web 端需要经过网络传输,因此这个图片优化行为对本地图片并不是很关键。你可以选择忽略它,不进行处理。 191 | 192 | 而对受你控制的远程图片(例如存储在你的 AWS 上的图片),你可以通过自定义 loader 来自动利用这些存储服务的图片优化能力。Next.js 的官方文档提供了很多示例可供参考:https://nextjs.org/docs/app/api-reference/next-config-js/images#example-loader-configuration 。 193 | 194 | --- 195 | 196 | 至此我们已经完成了对工程的改造,已经基本可以运行了。但还有一些问题需要被解决,例如 oauth2 登录时如何正确回调到我们的 Mobile App 内部。同时我们可能还希望使用一些 native 能力加强我们 App 的体验,比如本地存储、push notification 以及接入平台自己的支付等。我们将在 Part II 继续分享我们在这部分的实践。 197 | -------------------------------------------------------------------------------- /pages/appendix/stripe.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Stripe 接入实践 3 | --- 4 | Stripe 作为 LemonSqueezy 背后实际的支付平台,同时也是目前海外支付收款界的巨头,在能力和易用性上都比 LemonSqueezy 强出不少。但同样在复杂度上也高出一些。 5 | 6 | 在这里给大家分享一下 Podwise 实际接入 Stripe 的过程和使用场景,特别是一些仅看文档很难了解到的细节,希望能帮助大家少走弯路。 7 | 8 | 本文大概会涉及到这些方面: 9 | - 创建商品和定价,创建优惠码 10 | - 通过 API 发起购买 11 | - 改变用户订阅的 Plan 12 | - 允许用户取消、恢复订阅 13 | - 处理用户退款请求 14 | - 接入 Webhook 以便收到单次购买、订阅开通/修改/过期、退款等的通知 15 | - 开启支付宝和微信支付 16 | - 使用 customer portal 自带功能来简化你的开发 17 | 18 | ## 主要概念 19 | 20 | 本文中我们会涉及到这些 Stripe 中的概念: 21 | 22 | - Product - 商品 23 | - Price - 定价,一个商品可以有多个定价 24 | - Coupon & Promotion code - 优惠券和优惠码 25 | - Subscription - 订阅,本质上是对一个 Price 的循环收款 26 | - Checkout - 结账 / 付款,不论是单次购买还是进行订阅,都是走 checkout 来完成 27 | - Webhook - 事件回调 28 | - Customer - Stripe 中的客户,支付方式、订阅等都属于一个特定客户 29 | 30 | 我们在自己平台的数据库中,需要存储来自 Stripe Webhook 的一些信息: 31 | - subscriptionId - 用户的当前订阅 id ,用于后续调用取消订阅、恢复订阅、改变订阅 Plan 等场景 32 | - customerId - 用户在 Stripe 的客户 id ,在跳转 customer portal 的时候有用 33 | - subscription status - 当前订阅状态,方便我们判断用户当前订阅是否可用 34 | - priceId - 用来映射平台的 Plan ,例如是 Standard 还是 Pro 35 | 36 | 先大概看个眼熟,下面会提到这些内容。 37 | 38 | ## 创建商品、定价和优惠码 39 | 40 | 商品(Product)、定价(Price)和优惠码(Coupon & Promotion code)都可以在 Stripe dashboard 的 Product catalog 下管理。 41 | 42 | 这几个概念的相互关系是: 43 | - Pricing 属于 Product ,一个 Product 下面可以有多个 Prices 44 | - Promotion code 属于 Coupon ,一个 Coupon 下面可以有多个 Promotion codes 45 | - Coupon 可以作用于 Products ,不能只作用于特定 Price 46 | 47 | 一些重要的事实是: 48 | - 用户购买的最细粒度实际上是 Price 49 | - Price 可以是 recurring 即循环收款的(也就是用来实现订阅),也可以是 one-off 一次性的(用来售卖单次的商品或服务) 50 | - 一个 Price 下还可以有多个 currency ,针对不同货币进行不同定价(这里和开启支付宝、微信支付相关,后面会讲到) 51 | 52 | 由于用户实际购买的最细粒度是 Price ,但 Coupons 却仅能作用于 Products 粒度,这在我看来是不太合理的。所以在创建 Products / Prices 的时候我们需要关注一下我们自己对优惠策略的需求。这个等会儿在下面会讲到,我们先来关注一下 Stripe 官方推荐如何定义 Products / Pricing 。 53 | 54 | 在 Stripe 官方文档中是这么描述 Products 和 Pricing 的: 55 | 56 | > Products describe the specific goods or services you offer to your customers. 57 | > 58 | > - If you’re an e-commerce store selling clothing, one of your products might be a large white t-shirt. In Stripe, you can create a separate product for each size and color combination. 59 | > - If you’re a SaaS platform, you might have basic and premium pricing tiers. In this case, both basic and premium are separate products because they typically offer unique attributes or features. 60 | > - If you’re a donation platform that accepts donations for several different causes, each cause is a different product 61 | > 62 | > You can create as many products as you need to represent your product catalog. You can also create multiple prices for each product. Whether you should create multiple products as opposed to multiple prices depends on several factors. Generally, however, you want to: 63 | > 64 | > - Create multiple prices for a single product if you’re selling the same item at different price points. For example, if you offer a subscription tier at monthly and yearly rates, create one product for the tier and one price for the monthly rate and another for the yearly rate. See an example of this for a [good-better-best flat rate pricing model](https://docs.stripe.com/products-prices/pricing-models#flat-rate). (If you’re selling the same item in different currencies, then instead of creating multiple prices, create a single [multi-currency Price](https://docs.stripe.com/products-prices/pricing-models#multicurrency "multi-currency price").) 65 | > 66 | > - Create multiple products if the items require different provisioning or fulfillment in your application. In the [good-better-best](https://docs.stripe.com/products-prices/pricing-models#flat-rate) model, for example, you would create a different product for each tier. Similarly, if you have different versions of a product, like different colors or sizes of a t-shirt, you would create a product for each version. 67 | 68 | 以 Podwise 举例的话,不同的 Plan(Standard / Pro)就可以是不同的 Products ,而不同的订阅周期(月付/年付/单次)就是 Product 下的 Prices 。 69 | 70 | 因此我们按照 Stripe 的最佳实践会创建 2 个 Products ,每个下面 4 个 Prices 。 71 | - Product: Standard 72 | - Price: Monthly 73 | - Price: Annually 74 | - Price: 1 Month (one-off) 75 | - Price: 1 Year (one-off) 76 | - Product: Pro 77 | - Price: Monthly 78 | - Price: Annually 79 | - Price: 1 Month (one-off) 80 | - Price: 1 Year (one-off) 81 | 82 | 其中 Monthly 和 Annually 的 Prices 用来实现持续订阅,而两个 one-off Prices 则是为了照顾国内用户使用支付宝和微信支付的需求。因此我们将订阅 Prices 设置为 USD 价格,而将一次性的两个 Prices 设置为 CNY 价格(后面会说明为什么需要设置成 CNY 价格)。 83 | 84 | ### Product 组织方式对 Coupon 的影响 85 | 86 | 但这样的组织形式会出现一个问题,即我只能针对 Standard 或针对 Pro 创建 Coupon ,而不能针对年付这个订阅周期来创建 Coupon 。 87 | 88 | 我们可能会希望做一个优惠码,针对 Standard 的年付和 Pro 的年付计划来优惠,从而吸引更多用户选择年付方案从而更快回笼资金。 89 | 我们也可能会希望做一个折扣力度很大的优惠码,但仅对 Standard 的月付的首月生效,来吸引用户首次尝试我们产品的付费 features 。 90 | 91 | 这种场景,按上述方式组织 Products & Prices ,都是做不到的。因此目前 Podwise 是按如下的方式组织的: 92 | - Product: Standard Monthly 93 | - Price: recurring 94 | - Price: one-off 95 | - Product: Standard Annually 96 | - Price: recurring 97 | - Price: one-off 98 | - Product: Pro Monthly 99 | - Price: recurring 100 | - Price: one-off 101 | - Product: Pro Annually 102 | - Price: recurring 103 | - Price: one-off 104 | 105 | > 💡 请记得先在 Stripe dashboard 右上角打开 Test mode 的开关,在 Test mode 下创建 Products 等,并测试完成后,再 copy to live mode 。 106 | 107 | ## 通过 API 发起购买 108 | 109 | 现在产品已经创建好了,我们希望用户能去购买花钱了。这时候有两种做法,一种是使用 Stripe 的 Payment links 功能,从 Price 创建一个付款链接发给用户,让他付款。另一种则是通过 Stripe 的 API 将用户引导到 Stripe 的收银台进行付款。 110 | 111 | 显然作为开发者我们不会用第一种方式,这里我们需要用到的 API 接口是 `checkout.sessions.create`([文档](https://docs.stripe.com/api/checkout/sessions/create))。 112 | 113 | **注:以下所有 API 接口名称都是 Node.js SDK 的命名。** 114 | 115 | `checkout.sessions.create` 主要接收 priceId 、quantity 、mode(payment/subscription/setup)这些参数,然后返回一个 session 对象。其中最主要的返回值是 `url` ,即 Stripe 的收银台地址。 116 | 117 | 我们只需要在新窗口打开这个 url(或者也可以用 iframe 的方式嵌入,Stripe 提供了这个模式,详细请参考文档),用户就可以在新窗口中完成购买行为。随后我们就可以通过 Webhook 接收到购买的结果,从而为用户进行履约。Webhook 的部分我们在下一节讲。 118 | 119 | > 💡 需要注意的是,`checkout.sessions.create` 的入参中有一个参数 `success_url` ,用作付款成功后的跳转。请**不要依赖这个跳转**来作为付款成功的回调,而是使用 Webhook 。这是一个前端跳转,缺乏安全性的同时,也不能保证用户不会在付款后立即关闭窗口导致不触发跳转。 120 | 121 | ### 使用 metadata 和 subscription_data.metadata 传递用户信息 122 | 123 | 为了在收到 Webhook 回调时知道是哪个用户购买的产品,我们需要将 `userId` 在 checkout 的时候传递给 Stripe ,并最终在 Webhook 的时候重新获取。 124 | 125 | 尽管在付款时也可以要求用户输入邮箱地址,我们再从邮箱地址来对应我们自己平台的用户,但这种方式并不是很保险。因为你无法保证用户会输入和你的平台注册地址相同的邮箱地址(不过你也可以在 `checkout.sessions.create` 时传入邮箱地址)。 126 | 127 | 我们可以使用 metadata 来传递我们自己平台的 `userId` ,只需要在调用 `checkout.sessions.create` 时这样传入即可: 128 | ```typescript 129 | const session = await stripe.checkout.sessions.create({ 130 | client_reference_id: referenceId, 131 | customer_email: user.email, 132 | line_items: [ 133 | { 134 | price: params.priceId, 135 | quantity: params.quantity, 136 | }, 137 | ], 138 | mode: params.mode, 139 | metadata: { 140 | userId: user.id, 141 | priceId: params.priceId, 142 | quantity: params.quantity, 143 | }, 144 | subscription_data: params.mode === 'subscription' ? { 145 | metadata: { 146 | userId: user.id, 147 | }, 148 | } : undefined, 149 | }); 150 | 151 | // 获得 url 后即可跳转支付,你可以在前端跳转,也可以在后端跳转 152 | const redirectUrl = session.url; 153 | ``` 154 | 155 | 大家可以注意到我在 metadata 里除了 `userId` 之外还塞了 `priceId` 和 `quantity` ,这个在下面的 Webhook 那一小节会解释。 156 | 157 | 对于 one-off 的购买,只设置 checkout.session 的 metadata 就够了,因为我们通过 `checkout.session.completed` 这个 Webhook event type 就可以走完业务逻辑。但对于 subscription 订阅,只设置 checkout.session 的 metadata 并不足够。对订阅来说,后续我们需要通过 `customer.subscription.*` 的几个 Webhook events 来进行业务逻辑处理。我们是通过 checkout 来引导用户付款,同时创建了 subscription 。**但 checkout.session 和 subscription 是不同的对象,metadata 也不同**,因此我们需要通过 `subscription_data.metadata` 字段来为 subscription 对象同样设置我们需要的 metadata 。 158 | 159 | 所以可以看到我在上面的代码例子中,在 `subscription_data.metadata` 中也设置了 `userId` 作为回调时匹配平台用户的依据。 160 | 161 | > 💡 记得在调用 `checkout.sessions.create` 的时候,同时也为 subscription 设置 metadata 。 162 | 163 | > 💡 我没有在 `subscription_data.metadata` 中设置 `priceId` ,是因为 subscription 是可以改变订阅的 price 的。 164 | 165 | ## 用户改变订阅的 Plan 166 | 167 | 最简单的方式是在 Settings -> Billing -> Customer portal 中打开 Customers can switch plans 的开关,然后引导用户去 customer portal 完成这个动作。但同时我们也可以选择自制界面然后通过调用 Stripe API 的方式来完成。 168 | 169 | customer portal 将会在后面的小节中提到。 170 | 171 | 在 Stripe 中,要改变一个用户订阅的 Plan ,实际上就是修改该 subscription object 中的 subscription item object 。通过修改 subscription item 中的 `price` 为另一个 priceId ,即可完成 Plan 的变更。Stripe 会自动处理差额的问题。 172 | 173 | > 💡 差额具体会怎么处理有多种策略,默认会在下个周期的账单中体现,但也可以选择立即结束当前周期产生新账单。这部分比较复杂,建议仔细阅读官方文档。 174 | 175 | 那么我们需要做的就是通过用户的 subscriptionId 用 `subscriptions.retrieve` 查询到 subscription item ,然后用 `subscriptionItems.update` 来修改即可。 176 | 177 | - [subscriptions.retrieve 文档](https://docs.stripe.com/api/subscriptions/retrieve) 178 | - [subscriptionItems.update 文档](https://docs.stripe.com/api/subscription_items/update) 179 | 180 | ## 用户取消 / 恢复订阅 181 | 182 | 这个功能同样可以在 Customer portal 中完成,并且默认是开启的。同样我们在这一小节先讨论通过 Stripe API 来完成的方式。 183 | 184 | 首先很重要的一点是,Stripe 在**取消订阅时有两种不同的策略**。第一种策略是 Cancel immediately ,立即取消。这种策略会马上使用户的订阅失效,你的 Webhook 会即时收到一个 `customer.subscription.deleted` 事件。这种取消方式往往发生在因为某些原因我们需要为用户退款,同时结束用户的会员资格时。 185 | 186 | 而绝大部分时候,我们希望使用的是第二种策略:Cancel at end of billing period ,也就是在这个订阅周期结束时候取消。换句话说就是不再续订。这种策略下我们不需要为用户退款。 187 | 188 | 而这两种取消策略在 API 的调用上是两个不同的接口。 189 | 190 | 立即取消使用 API `subscriptions.cancel`([文档](https://docs.stripe.com/api/subscriptions/cancel))来完成。需要注意的是这个 API 并不会帮你向用户退款未履约的部分,你仍然需要额外的操作来为用户退款。 191 | 192 | > 💡 另一个 API `subscriptions.resume` 看起来很像是和 cancel 成对的 API ,但实际上并不是。resume 的作用是将 paused 状态的订阅重新激活,请注意区别。 193 | 194 | 不再续订则需要我们使用更新订阅的 API:`subscriptions.update`([文档](https://docs.stripe.com/api/subscriptions/update)),将字段 `cancel_at_period_end` 设置为 `true` 。同理恢复续订则是把该字段设置为 false 。 195 | 196 | 也因此当你自己的界面想要显示用户是否已经不再续订时,请在 Webhook 收到事件时将 `cancel_at_period_end` 字段记录下来。在 subscription object 的 `status` 中的值,不论是 `canceled` 还是 `paused` 都**不代表**不再续订的意思。 197 | 198 | > 💡 总之不要被 `subscriptions.cancel` 和 `subscriptions.resume` 这两个 API 的名字迷惑了,它们很可能不是你想要用的那两个 API 。 199 | 200 | ## 处理用户退款请求 201 | 202 | Podwise 让用户发送邮件来申请退款,而不是在页面中自助发起。自助退款可能对用户体验会更好,但邮件退款申请给了我们一次收集用户意见并和用户交流的机会,且实现起来更加简单。 203 | 204 | 如果你想提供自助退款的能力,那么 API `refunds.create`([文档](https://docs.stripe.com/api/refunds/create))将是你的选择。这部分因为我们没接,就不展开了。 205 | 206 | 对于 subscription 的退款请求,Stripe 提供了取消订阅 + 退款的一站式功能。你只需要在 Stripe dashboard 的 Subscriptions 菜单中找到需要退款的订阅,并点击 Cancel subscription 。然后在弹出菜单中选择 Immediately ,并选择退款是退最后一次付款还是退当前周期未履约的差价。 207 | ![[CleanShot 2024-06-20 at 18.35.09@2x.png]] 208 | 209 | 通过 Cancel 来进行退款会触发订阅取消的 Webhook 事件,因此只要你接了对应的事件,就无需再在自己的系统这边进行处理了。 210 | 211 | 而对于一次性付款的退款请求,则需要对 payments 进行退款。在 Stripe dashboard 的 Payments 菜单下找到对应的付款记录,然后选择 Refund payment 就可以退款,并且可以自选退款金额。 212 | 213 | 而对于这种一次性付款的退款,我们可以通过 Webhook `charge.refunded` 事件来监听。具体会在下一节里讨论。 214 | 215 | > 💡 需要注意的是,在 Payments 菜单下也可以对订阅的付款进行退款,但从这里退款并不会取消订阅,需要再到 Subscriptions 中去取消一下。 216 | 217 | ## 接入 Webhook 218 | 219 | 通过 Stripe 右上角的 Developer 功能入口,我们可以进行 Webhook 的创建和配置。不过在配置线上环境的 Webhook 之前,我们可以利用 stripe cli 和 Stripe 的 Test mode ,首先在本地调试 Webhook 。 220 | 221 | stripe cli 的安装和登录请参考[文档](https://docs.stripe.com/stripe-cli#install)。 222 | 223 | 完成安装后,我们就可以使用 stripe cli 来将我们的本地端口连上 Stripe 的测试环境,从而接收到 Webhook 。 224 | 225 | 对我们的场景来说,目前我们关心了这 4 个 Webhook events : 226 | - `checkout.session.completed` 227 | - `customer.subscription.created` 228 | - `customer.subscription.updated` 229 | - `customer.subscription.deleted` 230 | 231 | 此外还有一个退款相关的 event `charge.refunded` ,虽然我们没有用,但也会提到。 232 | 233 | 我们本地调试时使用的 stripe cli 命令供参考: 234 | ```shell 235 | stripe listen --events checkout.session.completed,customer.subscription.created,customer.subscription.updated,customer.subscription.deleted --forward-to localhost:3000/api/webhooks/stripe 236 | ``` 237 | 238 | ### one-off payment ,单次付款购买 239 | 240 | 我们通过 `checkout.session.completed` event 来处理单次付款购买的业务。 241 | 242 | `checkout.session.completed` 会在用户完成 checkout.session 的付款后触发,我们用这个事件来处理一次性付款购买的情况。如果用户使用会延迟确认付款的付款方式,则可能在用户付款成功前该事件就会触发。不过大部分付款方式都是即时确认的,因此我们没有考虑这种情况,我们就简单认为这个事件触发时,用户已经付了钱了。 243 | 244 | > 💡 付款方式是否是即时确认的,可以通过 Stripe dashboard 的 settings -> payments -> payments method ,展开特定支付方式详情,查看 Confirmation 字段是否为 Immediate 来确定。 245 | 246 | 那么我们需要做的就是在从 Webhook 接收到 `checkout.session.completed` 事件的时候,从事件中获取到 `userId` 、`priceId` 和 `quantity` ,即可完成我们的业务流程。例如为用户开通一次性的会员资格,或者充值特定数量的 tokens 等。 247 | 248 | 在 `checkout.session.completed` 这个 Webhook 回调中,默认并不存在用户购买的 `priceId` 和 `quantity` 信息。 249 | 250 | 这部分信息存储在 session object 的 `line_items` 字段中。在文档我们可以看到 session object 中的 line_items 被标注为 **expandable** ,因此默认不会返回,也不在 Webhook 中出现。你需要使用 API `checkout.sessions.retrieve` 并传入 `{expand: ['line_items']}` 才能获取到用户在这个 checkout 中购买的具体内容。 251 | 252 | 因为我们的场景很简单,用户一次只会购买一样东西,所以直接将 `priceId` 和 `quantity` 塞到 metadata 中,然后在 Webhook 时直接获取会更加方便一些。当然你也可以选择在收到回调之后重新用 retrieve + expand 获取一次。 253 | 254 | ### subscription 订阅购买 255 | 256 | 当用户订阅时,虽然在完成首次付款开通订阅的时候也会产生 `checkout.session.completed` 事件,但无法满足后面订阅生命周期状态改变时的同步需求,因此我们需要监听和 subscription 相关的事件才行。 257 | 258 | 我们目前监听这三个事件: 259 | - `customer.subscription.created` 260 | - `customer.subscription.updated` 261 | - `customer.subscription.deleted` 262 | 263 | > 💡 同 `line_items` 类似,在 session object 中,可以通过 expandable 的 `subscription` 字段拿到 subscription object 的信息,但 `customer.subscription.created` 事件更直接,我们可以直接使用这个事件。 264 | 265 | 一个无试用期的订阅生命周期会收到的事件过程是这样的: 266 | 1. 用户在 checkout 中提交了付款,`customer.subscription.created` 触发,`status` 为 `incomplete` 。 267 | 2. 用户付款确认成功,`customer.subscription.updated` 触发,`status` 为 `active` 。 268 | 3. 用户选择 Cancel (at the end of period) ,`customer.subscription.updated` 触发,`cancel_at_period_end` 为 `true` 。 269 | 4. 用户选择 ReSub ,`customer.subscription.updated` 触发,`cancel_at_period_end` 为 `false` 。 270 | 5. 用户变更订阅的 Plan ,`customer.subscription.updated` 触发,`items` 中的 `price` 改变。 271 | 6. 用户取消订阅后订阅到期 / 或多次扣款失败后订阅停止,`customer.subscription.deleted` 触发 。 272 | 273 | > 💡 当你的产品有免费试用时,你收到的事件中的 `status` 可能和上述举例不同,请自行验证一下。 274 | 275 | - 对于 `status` 为 `incomplete` 的 created 事件,我们可以选择忽略。因为用户还未成功付款,我们也无需为用户开通服务。 276 | - 在收到 updated 事件时,在 event object 中会有一个 `previous_attributes` 字段,记录了发生变化的字段之前的值是什么。我们可以结合这个字段来判断订阅到底发生了什么变化。例如判断订阅是从无效状态变成了有效状态,从而为用户开通服务。又或者判断订阅的 priceId 发生了变化,从而改变用户的 Plan 。(当然我们可以在每次收到事件时在自己的数据库中存储起来,用我们自己数据库中的值和收到的新事件的值进行比较。) 277 | - 建议在收到事件时将 customerId 记录下来,后续可以让用户访问 Stripe customer portal 。 278 | - 收到 deleted 事件则表示用户订阅失效,可以关闭用户在我们平台的服务了。 279 | 280 | > 💡 Stripe 的 Webhook events 不保证不会重复投递,因此请做好幂等控制。 281 | 282 | ### 退款并关闭服务 283 | 284 | 对于订阅类的退款,前面也提到可以通过 Cancel Subscription 来完成,所以`customer.subscription.deleted` 事件自然会触发,我们也因此可以自动关掉用户的服务。 285 | 286 | 但对于一次性付款开通的服务,就需要借助 `charge.refunded` 事件了。 287 | 288 | `charge.refunded` 中的 charge object 有 expandable 的字段 payment_intent 。一旦我们得知 payment_intent_id 之后,我们就可以使用 `checkout.sessions.list`([文档](https://docs.stripe.com/api/checkout/sessions/list))这个 API 反查这笔付款具体购买的 Product 和 Price 。同时我们也能在 checkout session object 中找到我们放在 metadata 中的 userId ,以便处理我们的业务逻辑。 289 | 290 | ## 开启支付宝和微信支付 291 | 292 | 支付宝和微信支付默认都仅支持一次性付款,不支持订阅。支付宝的 recurring payments 可以通过向客服申请并递交材料通过审核的方式来开通(这并不容易),但微信支付是完全不支持的。 293 | 294 | 因此如果你需要考虑国内用户的付费情况,那就有必要设计一套非订阅制的收费模式。 295 | 296 | 首先我们需要到 Stripe 的 Settings -> Payments -> Payment methods 中找到 Alipay 和 WeChat Pay 。这两项默认是没有激活的,我们点击后面的 Turn on ,等待即可。 297 | 298 | > 💡 在 Test mode 下 turn on 会立即 active ,你就可以开始联调测试了。但在正式环境下,turn on Alipay 和 WeChat Pay 会进入一个 pending 的状态。此时耐心等待即可,大概在几个小时内就会成功 active 。 299 | 300 | 一旦成功开启之后,我们可以先点击后面的 `...` 按钮查看一下 Alipay 和 WeChat Pay 的 customer rules 。主要是允许的金额范围、支持的币种和支持的地区。只有这些规则全部通过,对应的支付方式才会出现在用户的收银台界面上。而我们最需要关注的就是支持的币种。 301 | 302 | 不出意外的话,支持的币种肯定会有 CNY ,然后可能还会有你公司所在国家的币种。我们需要在收银台提供支持的币种价格,才能使支付宝和微信支付出现。 303 | 304 | 为 Product 单独创建一个币种为 CNY 的 Price ,使用这个 priceId 发起 checkout session ,不出意外支付宝和微信支付将会出现在用户的收银台上。 305 | 306 | > 💡 记得为你的 Price 选择 One-off 类型,而不是 Recurring 类型。 307 | 308 | ## 启用 Customer Portal 309 | 310 | Customer Portal 是 Stripe 提供的一个供客户使用的操作界面,这个 portal 提供这些功能:查看历史账单、修改客户联系信息/税号等、修改付款方式、取消/续期订阅、改变订阅的 Plan 。 311 | 312 | 这些功能如果我们自己开发还是比较麻烦的,但只需要将用户引导到 Stripe Customer Portal ,就无需自己开发这些功能了。 313 | 314 | 在开始之前,我们需要先启用这个 Customer Portal 。首先到 Settings -> Billing -> Customer portal 根据需要修改设置后保存设置。 315 | 316 | > 💡 即便你什么设置都没有改,也一定要点击一下 `Save changes` ,只有这样 Customer Portal 才会被激活,才能被使用。 317 | 318 | 然后我们要做的就是在我们的应用中设置一个入口(例如就叫 Manage your subscription),并在用户点击的时候调用 API `billingPortal.sessions.create` 生成一个一次性 url ,跳转即可。 319 | 320 | 这个 API 需要传入 customerId ,而这个 customerId 我们可以通过 Webhook 在 `customer.subscription.created` 等事件中获得。也因此这个一次性 url 无需用户再登录,即可准确的查看和管理自己的数据。 321 | 322 | > 💡 在 Customer Portal 设置界面有一个功能是:Launch customer portal with link 。它会生成一个供用户直接访问的链接,但这个链接需要用户自行登录,没有通过 API 获取 portal url 的方式方便。 323 | --------------------------------------------------------------------------------