ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Cloudflare免费额度上快速搭建开源SaaS骨架

Cloudflare免费额度上快速搭建开源SaaS骨架 一直想找一条“轻量级上线业务”的路子不用买服务器、不用搞容器编排又想带上完整的登录、支付和管理后台。如果你也在这个方向上绕了很久这篇内容应该能帮你省不少时间。本文围绕一套开源的 SaaS 骨架展开它把“登录、收款、管理后台”三件套都集成好了并且直接跑在 Cloudflare 免费额度上。你不需要先有一台云主机也不需要提前准备复杂的运维环境跟着本文把代码部署到 Cloudflare Workers D1 KV就能快速得到一套可以继续扩展的 SaaS 基础项目。不管你是想快速验证一个付费产品还是在学习 SaaS 系统的工程落地下面这套方案都值得参考。1. 为什么 SaaS 也能跑在 Cloudflare 免费额度上1.1 SaaS、IaaS、PaaS、DaaS 先分清楚在动手之前先把这些容易混淆的概念理清楚。很多人把云服务名词挂在嘴边但真正选择技术栈时容易选错方向。类型英文全称提供什么典型例子IaaSInfrastructure as a Service虚拟机、存储、网络AWS EC2、阿里云 ECSPaaSPlatform as a Service运行时、数据库、中间件Cloudflare Workers、VercelSaaSSoftware as a Service直接可用的业务软件各类在线文档、客服系统DaaSData as a Service数据接口和数据服务地图数据、天气数据 API你平时听说的“SaaS 系统”指的是把软件本身作为一种服务卖给客户。而 Cloudflare Workers 属于 PaaS它提供的是运行代码的环境。我们可以在 PaaS 上搭建 SaaS 业务系统本质上就是用平台能力替代自建服务器。1.2 Cloudflare 免费额度能做什么Cloudflare 免费计划对个人开发者和开源项目非常友好下面这几项刚好构成一套 SaaS 的最小依赖Workers运行服务端逻辑处理 API 请求。D1基于 SQLite 的关系型数据库适合存用户、订单。KV全局键值存储适合存登录 Session、临时数据。Pages托管前端静态资源可以绑定自定义域名。R2对象存储适合存头像、附件等文件。需要注意Cloudflare 免费额度的具体数值会随着官方政策调整并且不同账号的免费额度也可能不同。以 Workers 请求量为例免费计划包含一定数量的每日请求额度适合个人项目和 MVP 验证但如果你要做高并发业务还是要提前评估付费版本。1.3 一个晚上能上线的最小 MVP 边界“一个晚上上线”不是夸张但前提是把范围控制好。一个能收钱的 SaaS MVP 至少要包含用户注册、登录、退出。商品或套餐展示。下单并跳转支付。支付结果回调更新订单状态。一个简单的管理后台查看订单和用户。不需要一开始就做的功能多租户复杂权限、完整的工单系统、消息通知中心、数据报表大屏。先用最少代码把付费闭环跑通后续再按业务需求扩展。这套开源骨架正是按照这个边界设计的。2. 系统设计登录、支付、后台三件套2.1 整体架构整个项目由一个 Worker 承担 API项目结构如下用户浏览器 | | HTTPS v Cloudflare WorkersAPI 页面入口 | |--- D1 数据库用户表、订单表 | |--- KVSession、临时状态 | |--- 第三方支付支付宝 / 微信支付 / Stripe前端页面可以直接部署到 Cloudflare Pages也可以通过同一个 Worker 返回静态 HTML。为了减少部署步骤骨架里我更推荐后者一个 Worker 同时处理页面访问和 API 请求。2.2 数据库表设计在 D1 中创建两张核心表用户表和订单表。-- 文件路径migrations/0001_init.sql CREATE TABLE IF NOT EXISTS users ( id INTEGER PRIMARY KEY AUTOINCREMENT, email TEXT UNIQUE NOT NULL, password_hash TEXT NOT NULL, role TEXT NOT NULL DEFAULT user, created_at TEXT NOT NULL DEFAULT (datetime(now)) ); CREATE TABLE IF NOT EXISTS orders ( id TEXT PRIMARY KEY, user_id INTEGER NOT NULL, plan TEXT NOT NULL, amount_cents INTEGER NOT NULL, currency TEXT NOT NULL DEFAULT CNY, status TEXT NOT NULL DEFAULT pending, provider TEXT NOT NULL, provider_trade_no TEXT, created_at TEXT NOT NULL DEFAULT (datetime(now)), paid_at TEXT );简单说明金额统一用“分”存储避免浮点数精度问题。订单号使用业务侧生成的唯一字符串不用自增数字方便后续对账。provider字段记录支付渠道比如alipay、wechat、stripe。provider_trade_no保存第三方支付平台的流水号退款和账单核对时会用到。2.3 项目目录结构后端代码采用 TypeScript前端页面用原生 HTML/CSS/JS方便新人阅读。saas-skeleton/ ├── migrations/ │ └── 0001_init.sql ├── src/ │ ├── index.ts // Worker 入口和路由 │ ├── auth.ts // 注册、登录、Session │ ├── payment.ts // 支付下单和回调 │ ├── admin.ts // 管理后台接口 │ └── views/ │ ├── login.html │ ├── admin.html │ └── pay.html ├── wrangler.toml ├── package.json └── tsconfig.json这种目录划分足够简单也保留了后续拆成多个服务的空间。3. 环境准备与项目初始化3.1 安装工具本地需要安装 Node.js 18 或更高版本然后通过 npm 安装 Wrangler CLI。npm install -g wrangler如果安装速度慢可以换用国内 npm 镜像。安装完成后执行下面命令登录 Cloudflare 账号wrangler login这个命令会打开浏览器确认授权后Wrangler 就能管理你的 Cloudflare 资源。3.2 创建 D1 数据库和 KV Namespace先创建 D1 数据库wrangler d1 create saas-db执行后控制台会输出一个database_id需要把它记下来等会儿填到wrangler.toml中。创建 KV Namespacewrangler kv namespace create SESSIONS同样需要记录下输出的id。3.3 wrangler.toml 配置创建项目根目录的wrangler.toml文件内容如下# 文件路径wrangler.toml name saas-skeleton main src/index.ts compatibility_date 2024-11-01 [[d1_databases]] binding DB database_name saas-db database_id your-d1-database-id [[kv_namespaces]] binding SESSIONS id your-kv-namespace-id执行建表命令将本地 migrations 目录下的 SQL 应用到 D1wrangler d1 migrations apply saas-db --local wrangler d1 migrations apply saas-db --remote--local是本地开发环境--remote是正式远程环境。在开发阶段可以先只跑本地。4. 登录功能实现登录是一个 SaaS 系统的基础能力。这里我们实现邮箱密码登录并支持后续扩展第三方登录。4.1 注册与密码哈希在 Workers 环境中BCrypt 这类依赖原生模块的库使用起来比较麻烦。骨架里使用 Web Crypto API 自带的 PBKDF2 派生密钥不依赖额外包部署更简单。// 文件路径src/auth.ts async function hashPassword(password: string): Promisestring { const salt crypto.getRandomValues(new Uint8Array(16)); const keyMaterial await crypto.subtle.importKey( raw, new TextEncoder().encode(password), PBKDF2, false, [deriveBits] ); const bits await crypto.subtle.deriveBits( { name: PBKDF2, salt, iterations: 100000, hash: SHA-256 }, keyMaterial, 256 ); const saltHex [...salt].map((b) b.toString(16).padStart(2, 0)).join(); const hashHex [...new Uint8Array(bits)].map((b) b.toString(16).padStart(2, 0)).join(); return ${saltHex}:${hashHex}; }登记处理流程export async function handleRegister(request: Request, env: Env): PromiseResponse { const { email, password } await request.json(); if (!email || !password) { return json({ error: 邮箱和密码不能为空 }, 400); } const passwordHash await hashPassword(password); try { await env.DB.prepare(INSERT INTO users (email, password_hash) VALUES (?, ?)) .bind(email, passwordHash) .run(); return json({ ok: true }); } catch (e) { return json({ error: 该邮箱已被注册 }, 409); } }这里的重点是密码绝不能明文存储。PBKDF2 虽然不如 Argon2 新但配合足够的迭代次数在 Workers 这种受限环境里是简单可行的方案。4.2 登录与 Session登录成功后需要在 KV 中写入一个短期有效的 Session。export async function handleLogin(request: Request, env: Env): PromiseResponse { const { email, password } await request.json(); const user await env.DB.prepare(SELECT * FROM users WHERE email ?) .bind(email) .first(); if (!user) { return json({ error: 用户不存在或密码错误 }, 401); } const [saltHex, hashHex] user.password_hash.split(:); const passwordHash await hashPasswordWithSalt(password, saltHex); if (passwordHash ! user.password_hash) { return json({ error: 用户不存在或密码错误 }, 401); } const token crypto.randomUUID(); await env.SESSIONS.put( token, JSON.stringify({ userId: user.id, role: user.role }), { expirationTtl: 60 * 60 * 24 * 7 } ); // 写入 httpOnly Cookie前端 JS 无法读取降低 XSS 风险 const headers new Headers(); headers.append( Set-Cookie, session${token}; HttpOnly; Path/; Max-Age604800; SameSiteLax ); return json({ ok: true, role: user.role }, 200, headers); }Session 和 JWT 的选择上这里选用 Session KV需要下线某个用户或强制退出时直接删除 KV 中的记录即可管理更可控。4.3 第三方扫码登录思路很多 SaaS 会支持微信扫码登录或 GitHub 登录。整体流程如下前端跳转到第三方授权页。第三方回调时带上授权码。后端用授权码换取用户信息。如果邮箱未注册则自动创建用户。写入 Session完成登录。以 GitHub OAuth 为例可以简化成如下代码结构export async function handleGitHubCallback(request: Request, env: Env): PromiseResponse { const url new URL(request.url); const code url.searchParams.get(code); if (!code) { return json({ error: 缺少授权码 }, 400); } const tokenRes await fetch(https://github.com/login/oauth/access_token, { method: POST, headers: { Content-Type: application/json, Accept: application/json }, body: JSON.stringify({ client_id: env.GITHUB_CLIENT_ID, client_secret: env.GITHUB_CLIENT_SECRET, code }) }); const tokenData await tokenRes.json(); // 拿 access_token 获取用户信息再走注册或登录逻辑 return json({ ok: true }); }微信扫码登录的流程类似但回调中会多出签名校验等步骤。实际接入时一定要阅读对应开放平台的最新文档参数签名以官方要求为准。5. 支付功能实现支付是一个 SaaS 系统最核心的环节。骨架里做了一个支付 Provider 的抽象后续可以平滑切换不同的支付渠道。5.1 支付方式与资质选择支付宝支持沙箱环境适合个人开发者测试和联调。微信支付需要商户号服务商模式可以支持多商户收款。Stripe适合海外用户和跨境支付测试模式容易申请。要注意无论选择哪个支付渠道正式收款前都需要企业资质或个人资质审核。沙箱环境只是用来验证技术与流程不能直接替代真实交易。5.2 创建支付订单创建一个订单并返回支付链接的接口核心逻辑如下// 文件路径src/payment.ts export async function handleCreateOrder(request: Request, env: Env): PromiseResponse { const user getCurrentUser(request, env); if (!user) { return json({ error: 请先登录 }, 401); } const { plan, amountCents } await request.json(); const orderId crypto.randomUUID().replace(/-/g, ); await env.DB.prepare( INSERT INTO orders (id, user_id, plan, amount_cents, status, provider) VALUES (?, ?, ?, ?, ?, ?) ) .bind(orderId, user.id, plan, amountCents, pending, alipay) .run(); // 拿到第三方支付平台的支付链接 const payUrl await createProviderPayment(env, { orderId, amountCents }); return json({ orderId, payUrl }); }在沙箱支付宝中createProviderPayment负责组装参数并返回收银台地址。比如使用支付宝官方 SDK 前需要配置应用 ID、应用私钥、支付宝公钥等参数。async function createProviderPayment(env: Env, order: { orderId: string; amountCents: number }) { // 核心片段需要按你使用的 SDK 版本调整 const alipay new AlipaySdk({ appId: env.ALIPAY_APP_ID, privateKey: env.ALIPAY_PRIVATE_KEY, alipayPublicKey: env.ALIPAY_PUBLIC_KEY, gateway: env.ALIPAY_GATEWAY, // 沙箱环境使用支付宝沙箱网关 }); const result await alipay.pageExecute(alipay.trade.page.pay, { bizContent: { out_trade_no: order.orderId, product_code: FAST_INSTANT_TRADE_PAY, total_amount: (order.amountCents / 100).toFixed(2), subject: SaaS 基础套餐 } }); return result; // 返回跳转地址 }5.3 异步回调验签与状态更新支付平台支付成功后会异步通知你的服务器。这个环节最容易踩坑如果不校验签名任何人都可以伪造“支付成功”的请求如果校验逻辑有问题又会导致订单状态更新失败。以微信支付 v3 为例回调会返回一个加密的 resource需要用 API v3 密钥解密。核心思路如下// 伪代码片段需结合官方 SDK 和实际参数核对 function decryptWechatCallback(resource: any, apiV3Key: string): any { const { ciphertext, nonce, associated_data } resource; // 使用 AES-256-GCM依次拼接 associated_data、nonce、ciphertext // 解密得到 JSON 字符串再解析出 out_trade_no 和 trade_state return JSON.parse(decryptedText); }拿到回调后先验证商户订单号是否存在再判断支付状态是否为成功。如果订单已经是paid直接返回成功应答避免重复处理。export async function handleWechatCallback(request: Request, env: Env): PromiseResponse { // 1. 校验请求头中的签名和时间戳 // 2. 解密密文得到支付结果 // 3. 根据 out_trade_no 查询本地订单 // 4. 若订单状态为 pending则更新为 paid并写入 paid_at // 5. 返回支付平台要求的成功应答 return json({ code: SUCCESS, message: 成功 }); }5.4 对账与幂等支付不是“回调成功”就万事大吉还要考虑网络抖动导致回调丢失、重复通知等情况。最小可行的处理是风险点处理方式回调丢失定时调用支付平台的查询接口主动核对未支付订单状态重复回调更新前检查订单状态只处理pending状态的订单金额不一致用本地订单金额与回调金额做比对不等则告警加密参数错误记录完整回调报文便于排查数据库中建议给provider_trade_no加上唯一索引防止并发更新造成重复。6. 后台管理页面后台管理可以让运营人员查看订单、用户数据。这里不引入复杂的前端框架直接做两个页面。6.1 管理员鉴权管理员登录后后台页面请求 API 时需要携带 Session。后端接口先判断用户角色是否为adminexport async function handleAdminOrders(request: Request, env: Env): PromiseResponse { const user getCurrentUser(request, env); if (!user || user.role ! admin) { return json({ error: 无权限访问 }, 403); } const { results } await env.DB.prepare( SELECT * FROM orders ORDER BY created_at DESC LIMIT 100 ).all(); return json(results); }6.2 订单与用户管理前端后台页面可以这样设计!-- 文件路径src/views/admin.html -- div idapp h2SaaS 管理后台/h2 section h3订单列表/h3 table idorders-table thead tr th订单号/th th套餐/th th金额/th th状态/th th创建时间/th /tr /thead tbody/tbody /table /section /div script async function loadOrders() { const res await fetch(/api/admin/orders); const orders await res.json(); const tbody document.querySelector(#orders-table tbody); tbody.innerHTML orders.map((order) tr td${order.id}/td td${order.plan}/td td${(order.amount_cents / 100).toFixed(2)}/td td${order.status}/td td${order.created_at}/td /tr ).join(); } loadOrders(); /script这个页面不涉及复杂交互但已经可以完成最基本的运营查看需求。后续要扩展“退款”“人工标记订单”等功能只需要在表格上增加操作按钮再对接对应 API 即可。7. 部署上线与 Cloudflare 安全配置7.1 发布 Worker 与 D1本地验证无误后执行以下命令部署wrangler d1 migrations apply saas-db --remote wrangler deploy部署完成后Wrangler 会输出一个*.workers.dev域名这就是你的线上服务地址。如果你希望访问根路径时直接看到登录页需要在src/index.ts里把/路由指向静态 HTML 文件。7.2 绑定自定义域名如果需要自己的域名可以在 Cloudflare 后台操作在 DNS 中添加一条记录指向 Worker。进入 Workers 详情页添加自定义域名。等 HTTPS 证书自动签发。Cloudflare 会自动处理证书不需要你再配置 Nginx 或申请证书文件。7.3 Security → Bots 与安全防护免费额度上跑服务尤其要关注恶意请求。Cloudflare Dashboard 左侧菜单进入Security → Bots可以开启 Bot Fight Mode 或调整安全策略拦截明显的爬虫和恶意 Bot。由于不同账号可见的功能不同如果你的后台没有对应选项可以退而求其次在 Worker 里对单一 IP 做请求频率限制。登录接口增加验证码校验。支付回调接口只放行支付平台官方 IP 段或校验请求头签名。API 全部走 HTTPSCookie 标记Secure和HttpOnly。另外不要把数据库连接字符串、支付密钥写到仓库中。使用 Wrangler 的 Secret 功能管理敏感变量wrangler secret put ALIPAY_PRIVATE_KEY wrangler secret put WXPAY_API_V3_KEY这样密钥会以加密方式绑定到 Worker 环境变量中不会出现在代码仓库里。8. 常见问题与排查思路问题现象常见原因解决思路注册成功但无法登录密码哈希算法不一致检查注册和登录是否使用同一个哈希方法登录后前端拿不到用户信息Cookie 跨域或 SameSite 设置限制Worker 与页面在同一域名时优先不改跨域配置创建订单接口返回 401请求未携带 Session Cookie检查 fetch 是否配置 credentials: include支付跳转失败或提示参数错误支付宝公钥、应用私钥配置错误检查密钥类型优先在沙箱环境联调微信支付发起失败提示 banned商户号未开通对应产品权限或触发风控检查商户平台产品权限、支付目录和 IP 白名单支付成功但订单状态未更新异步回调验签失败或回调未到达查看 Worker 日志在回调入口打印原始报文部署后访问根路径 404路由未匹配/在 Worker 路由中增加根路径返回页面免费请求额度很快用完前端频繁轮询接口增加缓存、降低轮询频率、对接口限流如果你看到类似requestPayment:fail banned的微信支付报错先不要着急改代码。这个错误通常与支付能力权限、开发版小程序配置或触发风控有关建议先回到支付平台后台检查产品能力和账号状态而不是单纯调接口参数。9. 最佳实践与工程建议9.1 配置与密钥管理所有密钥放在wrangler secret或环境变量中。支付回调URL不能写死在代码中要按环境区分。不同环境本地、沙箱、生产使用不同的数据库和 KV Namespace。9.2 幂等与事务支付核心链路一定要保证幂等。回调处理时先查订单状态再执行更新如果使用 D1 事务可以这样组织await env.DB.batch([ env.DB.prepare(UPDATE orders SET status ?, provider_trade_no ?, paid_at ? WHERE id ? AND status ?) .bind(paid, tradeNo, now, orderId, pending) ]);利用 SQL 条件更新本身来保证不重复更新是最简单也比较可靠的方式。9.3 上线前合规自检支付渠道必须是合法注册的商户不能借用他人资质。产品页面需要明确展示价格、服务内容和退款规则。用户协议和隐私政策不能省略。涉及微信支付服务商模式时确认自己具备服务商资质并明确各商户的分账与结算关系。9.4 免费额度成本控制Cloudflare 免费额度适合个人项目、中小规模 SaaS 和开源项目。生产环境建议关注以下几点Worker 请求量增长后提前规划升级到付费计划。D1 不适合大量高频写入把 Session 这类高频读数据放在 KV。数据库请求尽量走 Prepared Statement避免拼接 SQL。静态资源尽量启用 Cloudflare 缓存减少 Worker 执行次数。整个骨架的核心是把 SaaS 最基础的三件事做扎实登录让人进得来支付让人付得了款后台让人看得清数据。你可以在跑通这套闭环之后再做多租户隔离、订阅计费、Webhook 事件中心这些更复杂的能力。先把收款链路跑通后面的迭代才有依据。
返回列表