技术栈
| 层 | 技术 | 为什么 |
|---|---|---|
| 框架 | Next.js 15(App Router、RSC、Turbopack) | server-first 渲染、生态成熟。 |
| Runtime | React 19 | async server components、use() hook。 |
| 语言 | TypeScript 5(strict) | 上线前抓住类型形状错误。 |
| 样式 | Tailwind v4 + shadcn/ui | OKLCH 色彩、globals.css 里 CSS-first 配置。 |
| ORM | Drizzle | 类型化 schema、无 codegen、可写裸 SQL。 |
| 数据库 | 仅 Postgres(v0.1) | 真相单一来源、事务、JSONB。 |
| 认证 | Better Auth + Drizzle adapter | 服务端 session、OAuth、邮件验证。 |
| i18n | next-intl | EN/ZH 双语,URL 前缀 as-needed。 |
| 文档 | Fumadocs MDX | 双语 *.zh.mdx,按 locale 查找。 |
| 支付 | Stripe / Paddle / Lemon Squeezy / Creem | 同一个 facade 后面。 |
| AI | Mock / OpenRouter / OpenAI / Anthropic / Replicate / fal | Phase 2;同样的 facade 模式。 |
| 邮件 | Resend + React Email | 模板化、类型安全、dev 可预览。 |
目录结构
app/(在 src/ 里)是路由表面。src/ 里 app/ 之外的模块是页面和 route handler 调用的业务逻辑。
app/[locale]/ 段是 i18n 路由发生的地方;(marketing)、(auth)、(app) 这些 route group
是为了 layout 分组和认证守门,不影响 URL 形状。
关键决定
Provider facade 模式
每个对接外部服务的模块都导出一个对象(facade),背后是可互换的 provider。 facade 在启动时从siteConfig.<module>.provider 选实现。消费者只见 facade。
src/ai/、src/mail/、src/newsletter/、src/customer-service/、src/affiliate/
都是同一套。给 mail 加 Postmark 就是 src/mail/provider/postmark.ts 一个新文件加一条 switch
分支——消费者代码一行不改。
server-only 强制
碰 DB 或读 secret 的文件在顶部import 'server-only'。这种模块要是被 client component 引用,
Next.js build 会失败。已保护:src/db/index.ts、src/lib/auth.ts、src/lib/server.ts、
src/lib/safe-action.ts、所有 src/payment/provider/*、src/mail/index.ts、
src/credits/index.ts、src/credits/server.ts。新建仅服务端模块时记得加这一行——
半秒钟敲完,省掉一次意外泄密。
三档 server actions
所有写操作走next-safe-action,三种 client 在 src/lib/safe-action.ts:
actionClient—— 公共,不需要认证。userActionClient—— 需要登录;提供ctx.user。adminActionClient—— 需要登录;要求user.role === 'admin'。
if (!user) throw 样板。
幂等 webhook
支付 webhook 在任何非 2xx 响应时都会重试。如果你在重试时重复发积分,就等于一笔付款付了两次。 脚手架在src/payment/handlers/ 里靠两条保证解决:
payment.invoiceId有 unique index——重复 insert 会让事务失败。- 每个 handler 在 insert 前先做
payment.sessionId查询,命中就早退。
拆分的 DB schema
src/db/ 按职责拆开:auth.schema.ts(Better Auth)、app.schema.ts(payment + credits +
transactions)、affiliate.schema.ts、ai.schema.ts、license.schema.ts。schema.ts
统一 re-export 给 Drizzle Kit 和 Better Auth adapter。id 用 text(nanoid 前缀或 snowflake)——
不用 serial,因为 text id 让你换 provider 时不破坏外键,也不会通过枚举泄露用户数。所有外键
在用户删除时级联(开箱即合 GDPR)。
双语 MDX
每篇文档两个平行文件:path/to/doc.mdx 和 path/to/doc.zh.mdx。Fumadocs 配置成先找带 locale
后缀的文件,找不到回落默认。同一套机制用于 blog 文章和 changelog。两种 locale 都存在才算上线。
Locale 路由——as-needed 前缀
src/i18n/routing.ts 把 next-intl 配成英文留在根(/about)、中文加前缀(/zh/about)。
全程用 @/i18n/navigation 里的 Link / useRouter——它们自动保留 locale 前缀。
直接 import next/link 或 next/navigation 会在客户端导航时悄悄丢掉 locale。
钱用 micro-cent
AI 推理和积分账务都涉及小于一分钱的数。用 float 存钱招舍入 bug;用整数 cent 存又丢亚分精度 (百万 token 的 Claude 调用每 token 价 $0.000003)。脚手架统一存整数 micro-cent(百万分之一 USD),到边缘再换显示单位。算法见src/ai/pricing.ts。
Edge runtime —— 只给 OG 图
src/app/opengraph-image.tsx 跑在 Edge runtime,因为这是唯一一处 cold start 直接影响用户可见
性能的路径(社交平台抓图器超时很快)。其他全部——包括所有访问 Postgres 的 API 路由——用 Node
runtime,因为 pg 驱动和不少 auth 依赖需要完整 Node API。除非你清楚在让出什么,否则别把路由
搬到 Edge。