平台架构与数据库设计
1. 需求摘要
cmmuu 不只是一套网站代码,而是多个产品共用的基础平台:
- 主站:
cmmuu.com,负责品牌、产品入口、用户总览和统一管理。 - 账号中心:例如
account.cmmuu.com,统一处理 Apple、Google、GitHub 与邮箱登录。 - 产品服务:例如
reqforge.cmmuu.com,未来继续增加其他工具和订阅服务。 - 管理后台:管理用户、产品、套餐、订阅、订单、权限、运营配置和审计日志。
- 订阅隔离:每个产品可独立定义免费、Pro、Super,以及月付、年付价格和权益。
- 当前不加入账户充值或余额体系,避免提前引入资金账本、退款、过期余额和合规复杂度。
2. 目标架构
flowchart TD User["国内与海外用户"] --> Edge["Cloudflare DNS、CDN 与 Tunnel"] Edge --> Web["TypeScript 前端<br/>主站与产品子域名"] Web --> Auth["统一账号中心<br/>OIDC 与 OAuth"] Web --> Api["Go API<br/>统一鉴权与业务路由"] Auth --> Pg["PostgreSQL<br/>账户、订阅、订单与审计"] Api --> Pg Api --> Redis["Redis<br/>缓存、会话与限流"] Api --> Object["Cloudflare R2<br/>对象文件与异地备份"] Pg --> Backup["WAL-G 或 pgBackRest<br/>基础备份与 WAL"] Backup --> Object Object --> Local["广州 N100 或 NAS<br/>只读拉取与本地快照"]
2.1 前端
统一使用 TypeScript,但不要求所有页面运行在同一个前端进程中:
apps/web-main:cmmuu 主站。apps/account-web:登录、绑定身份、账号安全和授权确认页面。apps/admin-web:统一管理后台。apps/reqforge-web:AI 前置产品。- 后续每个产品新增一个前端应用,共享组件、设计令牌、SDK 和鉴权包。
账号中心需要前端页面,但不需要独立仓库;它可以作为 Monorepo 中的独立应用,通过独立域名部署。
2.2 Go 后端
早期优先采用模块化单体,而不是立即拆成微服务:
identity:账号、外部身份、邮箱验证和会话。catalog:产品、套餐、价格和权益定义。subscription:订阅状态、周期和权益发放。billing:订单、支付回调、退款状态和幂等事件。entitlement:运行时权限判定。admin:后台管理接口和审计。
模块之间通过明确接口和事务边界隔离,先共用一个 Go 进程和一个 PostgreSQL 集群。只有当团队规模、发布频率、负载或故障域出现明确冲突时,再拆分独立服务。
3. OIDC 与统一登录
OIDC 是 OpenID Connect,即建立在 OAuth 2.0 之上的身份认证协议。OAuth 2.0 主要解决“允许应用访问什么资源”,OIDC 补充“当前用户是谁”。
统一登录流程:
flowchart LR Product["产品子域名"] --> Account["account.cmmuu.com"] Account --> Provider["Apple、Google、GitHub 或邮箱"] Provider --> Account Account --> Code["一次性授权码"] Code --> Product Product --> Session["产品会话与统一用户标识"]
安全边界:
- 浏览器授权码流程使用 Authorization Code + PKCE。
- 严格校验
state、nonce和回调地址白名单。 - Apple、Google、GitHub 的客户端密钥只保存在服务端密钥存储中。
- 外部平台账号只作为
identity,内部统一映射到不可变的user_id。 - 同一邮箱不自动合并账号;必须通过已登录状态或二次验证进行身份绑定。
- 子域名不共享高权限后台 Cookie;后台会话使用更短过期时间和多因素认证。
4. PostgreSQL 数据模型
PostgreSQL 是事务型事实来源,关键表建议如下:
| 表 | 核心用途 | 关键隔离字段 |
|---|---|---|
users | 平台统一用户 | id |
identities | Apple、Google、GitHub、邮箱身份映射 | user_id、provider、provider_subject |
oauth_clients | 主站和各产品的 OIDC 客户端 | service_id、redirect_uri |
sessions | 登录会话和刷新令牌摘要 | user_id、client_id |
services | cmmuu 下的独立产品 | id、slug |
plans | 免费、Pro、Super 等套餐 | service_id、code |
prices | 月付、年付及币种价格 | plan_id、billing_period、currency |
subscriptions | 用户在某产品的订阅状态 | user_id、service_id、plan_id |
entitlements | 套餐实际授予的功能与配额 | service_id、plan_id、feature_key |
orders | 下单、支付和退款状态 | user_id、service_id、idempotency_key |
payment_events | 支付渠道回调的不可重复事件 | provider、provider_event_id |
audit_logs | 后台敏感操作审计 | actor_id、service_id、action |
4.1 订阅隔离
- 所有套餐、价格、订阅和权益都必须关联
service_id。 - 唯一约束使用组合键,例如
UNIQUE(service_id, code),防止不同产品的pro套餐相互覆盖。 - 权益查询至少同时校验
user_id、service_id、订阅有效期和套餐状态。 - 支付回调先按
provider_event_id做幂等判断,再在单个数据库事务中更新订单、订阅和权益。 - API 层从可信路由或令牌声明解析
service_id,不直接相信客户端提交的产品标识。 - 早期使用同库同 schema 加行级隔离即可;出现法规、客户专属数据库或明显故障域需求后,再考虑独立 schema 或独立数据库。
4.2 原子性与一致性
需要强一致的操作全部放在 PostgreSQL 事务中:
- 创建订单与冻结价格快照。
- 接收并去重支付事件。
- 更新订单状态。
- 创建或续期订阅。
- 写入权益变更和审计记录。
事件通知、邮件和缓存刷新通过 Outbox 表在事务提交后异步执行,避免“数据库已成功、消息未发送”或反向不一致。
5. Redis 的职责边界
Redis 适合:
- 高频且可重建的缓存。
- 短期会话、验证码、限流计数。
- 分布式锁,但必须设置有效期并处理锁续期和重复执行。
- 可重建的异步任务状态。
Redis 不适合成为以下数据的唯一来源:
- 用户身份和账号绑定关系。
- 订单、支付结果、订阅有效期。
- 永久权益和审计记录。
因此默认不把 Redis 持久化文件作为核心备份对象;若未来确有不可重建数据,应先迁移到 PostgreSQL,再决定是否为 Redis 增加 AOF、快照和异地备份。
6. 代码仓库与 CI/CD
完整的单仓库目录、前后端边界、新产品接入和路径触发流水线见 项目仓库与前后端架构。
当前阶段建议使用 Monorepo 统一管理:
apps/
web-main/
account-web/
admin-web/
reqforge-web/
api/
packages/
ui/
auth-sdk/
api-client/
config/
deploy/
compose/
migrations/
github-actions/GitHub Actions 可按路径过滤触发构建:
- 前端目录变化,只构建和部署对应前端。
apps/api或数据库迁移变化,才构建 Go 镜像并执行后端流程。- 公共包变化,计算受影响应用后批量构建。
- 数据库迁移独立审查,执行前备份,使用向前兼容的 expand-contract 流程。
GitHub 免费分钟按账号或组织的计费规则统计,不因 Monorepo 天然增加;真正影响消耗的是工作流运行时长、操作系统倍率、触发次数和缓存命中率。自托管 Runner 不消耗 GitHub 托管分钟,但需要承担服务器、隔离、补丁和供应链安全成本。
7. 部署边界
容器可以统一运行环境,但不能绕开承载它的计算资源:
- Docker 运行在 VPS:支付 VPS 和运维成本。
- 托管容器平台:按 CPU、内存、请求和流量支付平台费用。
- Serverless 容器:空闲成本低,但冷启动、数据库连接和持续任务受平台约束。
目标部署方式:
- TypeScript 前端:Cloudflare Pages、Vercel 或同类边缘托管。
- Go API:与主要用户区域和 PostgreSQL 同地域部署,避免每次请求跨境访问数据库。
- PostgreSQL:单一主库起步,定期备份;达到明确收入和可用性要求后再迁移托管高可用数据库。
- Redis:与 Go API 同机房部署,不暴露公网,仅允许内网或本机访问。
- Cloudflare:负责 DNS、CDN、TLS、WAF 和可选 Tunnel,不替代源站或数据库。
8. 当前基础设施的合理角色
| 节点 | 当前条件 | 建议角色 |
|---|---|---|
| 广州 N100 | 16 GB 内存、1 TB 存储、无公网 IP、Tailscale | 现有开发与低负载服务;未来可作为备份拉取节点和恢复演练机 |
| 上海云主机 | 4 核 4 GB、60 GB、有公网入口 | 面向大陆用户的 API 入口或轻量业务节点 |
| 香港云主机 | 2 核 1 GB、40 GB | Tunnel、Nginx、探活或静态转发,不承载 PostgreSQL 主库 |
| 新加坡 VPS | 待购买与测试 | 面向海外用户的 Go API、PostgreSQL 与 Redis 同地域节点 |
若数据库主库位于新加坡,不应让所有大陆请求逐次跨境访问数据库;可以保持上海业务栈独立,或只将海外产品部署到新加坡。
9. 实施顺序
- 建立
services、users、identities和 OIDC 客户端模型。 - 完成账号中心最小前端与 Apple、Google、GitHub、邮箱登录。
- 接入 reqforge,验证主站和子域名统一登录。
- 建立套餐、价格、订阅、权益和支付事件幂等模型。
- 建立统一管理后台和审计日志。
- 完成 PostgreSQL PITR 备份和恢复演练。
- 新产品只新增产品模块、前端和权益定义,复用平台能力。
10. 主要风险
- 身份误合并:邮箱相同不代表同一个外部账号。
- 订阅串权:遗漏
service_id会导致跨产品访问。 - 支付重复回调:缺少幂等键会重复发放权益。
- Redis 当事实库:重启或淘汰会丢失关键业务状态。
- 跨境数据库:时延和网络抖动会放大每个 API 请求的尾延迟。
- 单机故障:Docker Compose 不等于高可用,恢复能力依赖已验证的备份。