Shipyard:无需繁琐设置的生产就绪 Django SaaS 样板
为 SaaS 产品启动全新 Django 项目,通常光是搭建基本组件就要耗费 2-3 周时间。Shipyard 通过提供预配置模块的生产就绪样板解决了这个问题。我们将剖析其架构和技术方案,这些方案能大幅节省开发时间。
技术栈与项目结构
Shipyard 基于现代技术栈构建,针对生产负载进行了优化。主要组件:
- Django 5 + DRF 3.15:API 优先架构,不使用 HTML 模板
- PostgreSQL 16 + Redis 7:数据库和缓存使用独立容器
- Celery 5 + Beat + Flower:异步任务与监控
- Docker Compose:两种配置——开发版和生产版
- Stripe Webhooks:支持幂等性的事件处理
- 多租户与 RBAC:User → TeamMembership → Team 的所有权链
项目结构严格模块化:
shipyard/
├── apps/
│ ├── core/
│ ├── users/
│ ├── teams/
│ ├── billing/
│ ├── notifications/
│ └── api/
├── config/
├── docker/
└── docker-compose.yml
每个 app 负责自己的职责领域。例如,billing 包含订阅模型,而 api 处理 DRF 基础设施,不涉及业务逻辑。
基础模型架构
核心是 core/models.py 中的抽象类:
python
# apps/core/models.py
import uuid
from django.db import models
class TimestampedModel(models.Model):
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
abstract = True
class UUIDModel(models.Model):
id = models.UUIDField(
primary_key=True,
default=uuid.uuid4,
editable=False,
)
class Meta:
abstract = True
这些 mixin 在所有模型中使用。选择 UUID 而非整数主键是深思熟虑的结果:URL 中的可预测 ID 不安全,而在生产数据库中从整数迁移到 UUID 非常痛苦。TimestampedModel 提供变更审计功能,避免代码重复。
users app 实现了自定义 User 模型,以 email 作为主要标识符:
python
# apps/users/models.py
class User(UUIDModel, AbstractBaseUser, PermissionsMixin):
email = models.EmailField(unique=True)
full_name = models.CharField(max_length=255, blank=True)
is_email_verified = models.BooleanField(default=False)
stripe_customer_id = models.CharField(max_length=255, blank=True, null=True)
USERNAME_FIELD = "email"
REQUIRED_FIELDS = ["full_name"]
stripe_customer_id 字段添加到用户模型中,用于计费与个人绑定的场景。验证和密码重置令牌通过 EmailVerificationToken 和 PasswordResetToken 模型单独处理——不使用缓存,直接存数据库以便审计。
通过 RBAC 实现多租户
Shipyard 采用务实的多租户方案,而不是 schema 分离(django-tenants)或全局 tenant_id,而是使用所有权链:
User → TeamMembership → Team
TeamMembership 作为中间表,带有 role 字段。Team 存储限额(max_members、max_projects),从 Plan 反范式化以实现快速检查而无需 JOIN。这种设计降低了处理限额时的查询复杂度。
通过 DRF 权限实现了三种角色:
python
# apps/teams/permissions.py
class IsTeamMember(BasePermission):
def has_permission(self, request, view):
team_id = view.kwargs.get('team_id')
return TeamMembership.objects.filter(
user=request.user,
team_id=team_id
).exists()
模式非常机械:所有 ViewSet 都根据 URL 中的 team_id 过滤 queryset。这种方式比手动在每个查询中添加 tenant_id 更能降低租户间数据泄露风险。
计费系统与 Webhooks
Plan 模型镜像 Stripe Product + Prices:
python
class Plan(models.Model):
stripe_product_id = models.CharField(max_length=255)
stripe_price_id_monthly = models.CharField(max_length=255)
stripe_price_id_yearly = models.CharField(max_length=255)
max_members = models.PositiveIntegerField()
max_projects = models.PositiveIntegerField()
Plan 中的反范式化限额允许即时配额检查,而无需调用 Stripe。创建团队时,post_save 信号会预创建 Stripe 客户——团队立即即可处理支付。
Webhook 处理基于幂等性。WebhookEvent 模型跟踪已处理事件:
python
class WebhookEvent(models.Model):
event_id = models.CharField(max_length=255, unique=True)
event_type = models.CharField(max_length=255)
payload = models.JSONField()
processed_at = models.DateTimeField(null=True)
这能防止 Stripe 重试时的重复事件处理。
核心亮点
- UUID 取代整数 PK——一次决策,永不过时;生产数据库中无法迁移
- 集中式健康检查——
/health/和/ready/端点,便于与 Docker 和 Kubernetes 集成 - DRF 基础设施独立成 app——版本控制、分页和错误处理与业务逻辑分离
- EmailLog 邮件审计——记录每封发送邮件,便于支持
- 双 Docker Compose 配置——开发版带卷挂载,生产版用多阶段构建
Stripe 集成特别注重幂等性。每条 webhook 事件通过 event_id 检查唯一性。这对 charges 等操作至关重要,避免重处理导致双重计费。Shipyard 使用 WebhookEvent 模型作为天然锁机制。
通知系统内置模板机制,基于通用布局。所有邮件(欢迎邮件、验证邮件、邀请邮件)继承共同结构,便于维护。模板存放在 apps/notifications/templates/notifications/,提供独立的 HTML 和文本版本。
生产 Docker Compose 配置使用多阶段构建。第一阶段安装依赖,第二阶段复制代码。这能缩小最终镜像体积并加速部署。为 CI/CD 设置了三个 GitHub Actions 工作流:PR 时运行 lint 测试,主分支合并时构建镜像,release 标签时部署。
— Editorial Team
暂无评论。