📖 核心概念
- SaaS产品架构设计
- 多租户数据模型
- 订阅计费系统
- 全栈功能实现
- 安全与合规
- 上线与迭代
💻 代码实现
SaaS应用架构
✅ prisma
// 全栈SaaS项目结构
// my-saas/
// ├── src/
// │ ├── app/ # Next.js App Router
// │ │ ├── (auth)/ # 认证相关页面
// │ │ │ ├── login/
// │ │ │ └── register/
// │ │ ├── (dashboard)/ # 主应用
// │ │ │ ├── layout.tsx # 仪表盘布局
// │ │ │ ├── page.tsx # 仪表盘首页
// │ │ │ ├── projects/ # 项目管理
// │ │ │ ├── settings/ # 设置
// │ │ │ └── billing/ # 计费
// │ │ ├── (landing)/ # 落地页
// │ │ │ ├── page.tsx
// │ │ │ └── pricing/
// │ │ └── api/
// │ │ ├── stripe/ # 支付Webhook
// │ │ └── webhooks/
// │ ├── components/
// │ │ ├── ui/ # 基础UI组件
// │ │ └── features/ # 业务组件
// │ ├── lib/
// │ │ ├── db.ts # Prisma实例
// │ │ ├── auth.ts # 认证配置
// │ │ └── stripe.ts # 支付配置
// │ └── stores/ # Zustand状态
// ├── prisma/
// │ └── schema.prisma
// └── public/
// 多租户数据模型
// prisma/schema.prisma (核心部分)
model Tenant {
id String @id @default(cuid())
name String
slug String @unique
plan Plan @default(FREE)
members Member[]
projects Project[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model Member {
id String @id @default(cuid())
role Role @default(MEMBER)
user User @relation(fields: [userId], references: [id])
userId String
tenant Tenant @relation(fields: [tenantId], references: [id])
tenantId String
joinedAt DateTime @default(now())
@@unique([userId, tenantId])
}
model Project {
id String @id @default(cuid())
name String
description String?
status Status @default(ACTIVE)
tenant Tenant @relation(fields: [tenantId], references: [id])
tenantId String
tasks Task[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model Task {
id String @id @default(cuid())
title String
description String?
priority Priority @default(MEDIUM)
status TaskStatus @default(TODO)
assignee User? @relation(fields: [assigneeId], references: [id])
assigneeId String?
project Project @relation(fields: [projectId], references: [id])
projectId String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
enum Plan { FREE PRO ENTERPRISE }
enum Role { OWNER ADMIN MEMBER }
enum Status { ACTIVE ARCHIVED }
enum Priority { LOW MEDIUM HIGH URGENT }
enum TaskStatus { TODO IN_PROGRESS IN_REVIEW DONE }
订阅计费与核心功能
✅ tsx
// Stripe订阅计费
import Stripe from 'stripe';
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);
// 创建订阅
export async function createSubscription(tenantId: string, priceId: string) {
const tenant = await prisma.tenant.findUnique({ where: { id: tenantId } });
if (!tenant) throw new Error('Tenant not found');
let customerId = tenant.stripeCustomerId;
if (!customerId) {
const customer = await stripe.customers.create({
metadata: { tenantId },
});
customerId = customer.id;
await prisma.tenant.update({
where: { id: tenantId },
data: { stripeCustomerId: customerId },
});
}
const subscription = await stripe.subscriptions.create({
customer: customerId,
items: [{ price: priceId }],
metadata: { tenantId },
});
return subscription;
}
// Webhook处理
// app/api/stripe/webhook/route.ts
export async function POST(request: Request) {
const body = await request.text();
const sig = request.headers.get('stripe-signature')!;
const event = stripe.webhooks.constructEvent(
body, sig, process.env.STRIPE_WEBHOOK_SECRET!
);
switch (event.type) {
case 'checkout.session.completed':
const session = event.data.object;
await prisma.tenant.update({
where: { id: session.metadata.tenantId },
data: { plan: 'PRO' },
});
break;
case 'customer.subscription.deleted':
// 降级为免费版
break;
}
return new Response('OK', { status: 200 });
}
// 核心功能:任务看板
'use client';
import { useStore } from '@/stores/taskStore';
import { DragDropContext, Droppable, Draggable } from '@hello-pangea/dnd';
function TaskBoard() {
const { tasks, moveTask } = useStore();
const columns: Record<TaskStatus, Task[]> = {
TODO: tasks.filter(t => t.status === 'TODO'),
IN_PROGRESS: tasks.filter(t => t.status === 'IN_PROGRESS'),
IN_REVIEW: tasks.filter(t => t.status === 'IN_REVIEW'),
DONE: tasks.filter(t => t.status === 'DONE'),
};
const handleDragEnd = (result: any) => {
if (!result.destination) return;
moveTask(result.draggableId, result.destination.droppableId);
};
return (
<DragDropContext onDragEnd={handleDragEnd}>
<div className="board">
{Object.entries(columns).map(([status, items]) => (
<Droppable key={status} droppableId={status}>
{(provided) => (
<div ref={provided.innerRef} {...provided.droppableProps} className="column">
<h3>{status}</h3>
{items.map((task, index) => (
<Draggable key={task.id} draggableId={task.id} index={index}>
{(provided) => (
<div ref={provided.innerRef} {...provided.draggableProps} {...provided.dragHandleProps}>
<TaskCard task={task} />
</div>
)}
</Draggable>
)}
{provided.placeholder}
</div>
)}
</Droppable>
))}
</div>
</DragDropContext>
);
}
🎯 练习任务
- 1 设计并实现SaaS应用的多租户数据模型
- 2 实现基于Stripe的订阅计费系统
- 3 构建任务看板,支持拖拽排序
- 4 完整部署并上线你的SaaS应用
🏆 成就解锁
🏆 全栈SaaS工程师 — 从零构建完整的SaaS产品,成为全栈前端工程师!