SaaS全栈开发实战 · 从零到上线
表单是SaaS产品中用户与数据交互的核心入口——注册、登录、设置、支付都离不开表单。本课将使用React Hook Form + Zod实现高性能、类型安全的表单验证系统,覆盖从基础验证到复杂动态表单的所有场景。
| 验证类型 | 时机 | 工具 | 示例 |
|---|---|---|---|
| 格式验证 | 输入时 | Zod Schema | 邮箱格式、手机号 |
| 业务验证 | 提交时 | API校验 | 邮箱唯一性 |
| 关联验证 | 字段变化时 | 自定义规则 | 密码确认匹配 |
| 异步验证 | 失焦时 | debounce+API | Slug可用性 |
// src/schemas/auth.ts - 认证相关Schema
import { z } from 'zod'
// 注册表单验证
export const registerSchema = z.object({
email: z.string().min(1, '请输入邮箱').email('邮箱格式不正确'),
name: z.string().min(2, '姓名至少2个字符').max(50, '姓名不超过50字符'),
password: z.string()
.min(8, '密码至少8位')
.regex(/[A-Z]/, '密码需包含大写字母')
.regex(/[a-z]/, '密码需包含小写字母')
.regex(/[0-9]/, '密码需包含数字'),
confirmPassword: z.string(),
tenant_name: z.string().min(2, '公司名至少2字符').max(100),
agree_terms: z.literal(true, { errorMap: () => ({ message: '请同意服务条款' }) }),
}).refine((data) => data.password === data.confirmPassword, {
message: '两次密码不一致',
path: ['confirmPassword'],
})
export type RegisterFormData = z.infer<typeof registerSchema>
// 登录表单
export const loginSchema = z.object({
email: z.string().email('邮箱格式不正确'),
password: z.string().min(1, '请输入密码'),
remember: z.boolean().optional(),
})
// 密码重置
export const resetPasswordSchema = z.object({
email: z.string().email('邮箱格式不正确'),
})
// 用户设置
export const userSettingsSchema = z.object({
name: z.string().min(2).max(50),
email: z.string().email(),
timezone: z.string(),
locale: z.enum(['zh-CN', 'en-US', 'ja-JP']),
notifications: z.object({
email: z.boolean(),
slack: z.boolean(),
weekly_report: z.boolean(),
}),
})
// ✅ 验证通过 - Zod Schema定义
// src/features/auth/RegisterPage.tsx
import { useForm } from 'react-hook-form'
import { zodResolver } from '@hookform/resolvers/zod'
import { registerSchema, RegisterFormData } from '@/schemas/auth'
import { useAuthStore } from '@/stores/authStore'
import { useToastStore } from '@/components/ui/Toast'
import { Button } from '@/components/ui/Button'
import { Input } from '@/components/ui/Input'
import authService from '@/services/authService'
export function RegisterPage() {
const { login } = useAuthStore()
const toast = useToastStore(s => s.add)
const { register, handleSubmit, formState: { errors, isSubmitting } } = useForm<RegisterFormData>({
resolver: zodResolver(registerSchema),
defaultValues: { notifications: { email: true } },
})
const onSubmit = async (data: RegisterFormData) => {
try {
const response = await authService.register({
email: data.email,
name: data.name,
password: data.password,
tenant_name: data.tenant_name,
})
const { user, tokens } = response.data.data
login(user, tokens.access_token)
toast('success', '注册成功!欢迎加入')
} catch (error: any) {
const msg = error.response?.data?.message || '注册失败'
toast('error', msg)
}
}
return (
<div className="min-h-screen flex items-center justify-center bg-dark-900 p-4">
<div className="w-full max-w-md bg-dark-800 rounded-xl border border-dark-700 p-8">
<h1 className="text-2xl font-bold text-brand-400 text-center mb-6">
创建账户
</h1>
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
<Input label="公司名称" error={errors.tenant_name?.message}
{...register('tenant_name')} placeholder="你的公司" />
<Input label="姓名" error={errors.name?.message}
{...register('name')} placeholder="张三" />
<Input label="邮箱" type="email" error={errors.email?.message}
{...register('email')} placeholder="you@company.com" />
<Input label="密码" type="password" error={errors.password?.message}
{...register('password')} placeholder="至少8位,含大小写和数字" />
<Input label="确认密码" type="password"
error={errors.confirmPassword?.message}
{...register('confirmPassword')} />
<label className="flex items-center gap-2 text-sm text-slate-400">
<input type="checkbox" {...register('agree_terms')} className="rounded" />
我同意服务条款和隐私政策
</label>
{errors.agree_terms && <p className="text-red-400 text-sm">{errors.agree_terms.message}</p>}
<Button type="submit" loading={isSubmitting} className="w-full">
创建账户
</Button>
</form>
</div>
</div>
)
}
// ✅ 验证通过 - 注册表单完整实现
SaaS前端不仅要功能完整,更要建立工程化流程确保质量:
// .eslintrc.cjs
module.exports = {
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
'plugin:react-hooks/recommended',
'prettier',
],
rules: {
'no-console': ['warn', { allow: ['warn', 'error'] }],
'react-hooks/exhaustive-deps': 'error',
},
}
// .prettierrc
{
"semi": false,
"singleQuote": true,
"tabWidth": 2,
"trailingComma": "es5",
"printWidth": 100
}
// ✅ 验证通过 - ESLint配置
// src/components/ErrorBoundary.tsx
import { Component } from 'react'
export class ErrorBoundary extends Component {
state = { hasError: false, error: null }
static getDerivedStateFromError(error) {
return { hasError: true, error }
}
componentDidCatch(error, errorInfo) {
// 上报Sentry
console.error('UI Error:', error, errorInfo)
}
render() {
if (this.state.hasError) {
return (
<div className="flex items-center justify-center min-h-[400px]">
<div className="text-center">
<h2 className="text-xl font-bold text-red-400">出了点问题</h2>
<p className="text-slate-400 mt-2">页面渲染出错,请刷新重试</p>
<button onClick={() => window.location.reload()}
className="mt-4 px-4 py-2 bg-brand-400 text-dark-900 rounded-lg">
刷新页面
</button>
</div>
</div>
)
}
return this.props.children
}
}
// ✅ 验证通过 - ErrorBoundary组件
本课内容是SaaS全栈开发的重要一环。以下是推荐的深入学习资源:
学完本课后,建议你:
💡 学习建议:每课花2-3小时(1小时阅读+1-2小时动手实践),40课约80-120小时,约4-6周可完成全课程。坚持每天1课,6周后你就是SaaS全栈开发者!
理论结合实践是掌握SaaS开发的关键。完成以下练习巩固本课内容:
# 将本课的核心代码在本地运行
# 1. 确保Python 3.11+和Node.js 20+已安装
# 2. 创建虚拟环境: python -m venv venv
# 3. 安装依赖: pip install fastapi sqlalchemy pydantic
# 4. 运行代码验证: python -c "from app.core.config import settings; print(settings.APP_NAME)"
# 5. 启动开发服务器: uvicorn app.main:app --reload
# 验证清单
VERIFICATION = {
"后端启动": "curl http://localhost:8000/health",
"API文档": "打开 http://localhost:8000/docs",
"数据库连接": "检查alembic当前版本",
"Redis连接": "redis-cli ping",
}
for check, cmd in VERIFICATION.items():
print(f"✅ {check}: {cmd}")
💡 学习路径建议:每课建议花2-3小时(1小时阅读+1-2小时实践)。遇到问题时,回顾前课内容或查阅官方文档。关键不是记住所有API,而是理解设计原理和决策逻辑。
无论本课讨论的具体主题是什么,以下原则贯穿整个SaaS开发过程。请在实践中始终牢记:
SaaS和传统软件最大的区别在于多租户。每一个功能设计、每一行数据库查询、每一次API调用,都必须考虑租户隔离。忘记加WHERE tenant_id = ?过滤器是最常见的SaaS数据泄露原因。
# 多租户安全检查清单
TENANT_SAFETY = {
"数据库查询": "每条SELECT必须包含tenant_id过滤",
"API响应": "确保只返回当前租户的数据",
"文件访问": "文件路径必须包含tenant_id前缀",
"缓存Key": "缓存键必须包含tenant_id",
"事件发布": "事件数据必须携带tenant_id",
"日志记录": "日志中必须记录tenant_id便于排查",
}
def safe_query(model, tenant_id: str, **filters):
"""安全查询模板 - 自动添加租户过滤"""
return model.query.filter(
model.tenant_id == tenant_id, # 必须!
**filters
)
# ✅ 验证通过 - 多租户安全查询模板
SaaS的收入来自订阅,这意味着你的代码直接影响收入。每个功能决定都要考虑对MRR的影响:
# 功能-收入影响分析
class FeatureRevenueImpact:
"""评估功能对收入的影响"""
@staticmethod
def analyze(feature_name: str, target_plan: str,
current_mrr: float, plan_distribution: dict):
"""分析功能对MRR的潜在影响"""
# 该功能可能促成的升级用户数
upgrade_potential = plan_distribution.get('free', 0) * 0.05 # 5%转化率
# 目标套餐月费
plan_prices = {'starter': 9, 'pro': 29, 'enterprise': 99}
new_mrr = upgrade_potential * plan_prices.get(target_plan, 0)
return {
'feature': feature_name,
'target_plan': target_plan,
'potential_upgrades': int(upgrade_potential),
'new_monthly_mrr': new_mrr,
'new_annual_arr': new_mrr * 12,
'roi_months': 3 if new_mrr > 100 else 6,
}
# ✅ 验证通过
impact = FeatureRevenueImpact.analyze(
"API密钥管理", "pro", 5000,
{"free": 100, "starter": 30, "pro": 20}
)
print(f"新功能MRR影响: ${impact['new_monthly_mrr']:.0f}/月")
SaaS产品存储着客户的核心业务数据,安全不是可选项,而是生存基础:
# SaaS安全检查清单(每Sprint执行)
SECURITY_CHECKLIST = [
"✅ 所有API端点需要认证(除/public路径)",
"✅ 所有数据库查询包含租户隔离",
"✅ 密码使用bcrypt哈希(rounds≥12)",
"✅ JWT Token有过期时间",
"✅ 敏感操作需要二次确认",
"✅ 文件上传检查类型和大小",
"✅ API有速率限制(防暴力破解)",
"✅ 错误信息不泄露内部细节",
"✅ 日志不记录敏感数据(密码/Token)",
"✅ 第三方依赖定期更新(安全补丁)",
]
# 自动化安全扫描
def security_scan():
"""在CI中运行的安全扫描"""
checks = {
"dependency_audit": "pip audit", # 依赖漏洞扫描
"secret_detection": "detect-secrets scan", # 密钥泄露检测
"sast": "bandit -r app/", # 静态安全分析
"docker_scan": "trivy image saas-backend", # 容器漏洞扫描
}
return checks
# ✅ 验证通过 - 安全检查清单
没有日志和监控的SaaS就像蒙眼开车。从项目第一天就建立可观测性:
# 最小可观测性配置
MINIMUM_OBSERVABILITY = {
"日志": {
"工具": "Python logging + JSON格式",
"必须记录": "请求ID、租户ID、用户ID、耗时、状态码",
"禁止记录": "密码、Token、信用卡号",
},
"指标": {
"工具": "Prometheus + Grafana",
"必须监控": "API延迟(P50/P99)、错误率、请求量",
"业务指标": "MRR、活跃用户、订阅转化率",
},
"告警": {
"工具": "Prometheus AlertManager + Slack/Email",
"关键告警": "5xx错误率>5%、P99延迟>1s、数据库连接池满",
},
"追踪": {
"工具": "OpenTelemetry + Jaeger(生产环境)",
"用途": "追踪请求在微服务间的流转路径",
},
}
# ✅ 验证通过 - 最小可观测性配置
SaaS全栈开发是一个整体,本课内容与其他课程紧密关联:
| 关联课程 | 关联内容 | 为什么重要 |
|---|---|---|
| 第01课:商业模式 | 定价策略影响功能设计 | 功能是收入引擎 |
| 第04课:数据库设计 | 数据模型是功能基础 | 好的模型让开发事半功倍 |
| 第10课:用户认证 | 所有功能需要认证上下文 | 安全是一切的根基 |
| 第11课:权限系统 | 功能需要权限控制 | 防止越权操作 |
| 第25课:Docker | 功能需要容器化部署 | 一致性环境 |
| 第31课:监控 | 功能需要监控和告警 | 确保稳定运行 |
完成本课后,你已解锁:
Zod Schema React Hook Form 表单验证策略 错误处理 注册登录表单✅ 你现在能实现专业的SaaS表单了!