返回首页

Django SaaS 样板 Shipyard:无需琐事的生产模板

Shipyard 分析——适用于 SaaS 应用的生产就绪 Django 样板。多租户架构、Stripe 集成以及节省 2 周开发时间的的技术解决方案的分解。代码示例和最佳实践。

Shipyard:适用于 SaaS 的生产就绪 Django 模板
Advertisement 728x90

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 的所有权链

项目结构严格模块化:

Google AdInline article slot
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 提供变更审计功能,避免代码重复。

Google AdInline article slot

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 字段添加到用户模型中,用于计费与个人绑定的场景。验证和密码重置令牌通过 EmailVerificationTokenPasswordResetToken 模型单独处理——不使用缓存,直接存数据库以便审计。

通过 RBAC 实现多租户

Shipyard 采用务实的多租户方案,而不是 schema 分离(django-tenants)或全局 tenant_id,而是使用所有权链:

Google AdInline article slot
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

Advertisement 728x90

继续阅读