我如何用 Next.js + Supabase + Cloudflare R2 搭建壁纸销售平台——月成本接近 $0

我如何用 Next.js + Supabase + Cloudflare R2 搭建壁纸销售平台------月成本接近 $0

独立开发者实战复盘:WallpaperSense 的架构设计、技术选型与踩坑经验。


我想做一个壁纸销售平台。不是那种满屏广告的免费图库,而是一个真正的电商产品------支持主题包和单张壁纸购买、即时下载,覆盖手机到平板到桌面端所有屏幕尺寸。

约束条件:我是一个独立开发者,产品还没验证,所以技术栈必须便宜(最好免费),直到跑出来为止。

技术栈全景

层级 技术
框架 Next.js 16(App Router + Turbopack)
前端 React 19 + TypeScript 5.7
UI Tailwind CSS + shadcn/ui(Radix UI)
数据库 Supabase PostgreSQL(仅数据库,不用 Auth/RLS/Storage)
对象存储 Cloudflare R2(S3 兼容)
支付 Creem.io
图片处理 Sharp
多语言 next-intl(中文 + 英文)
部署 Vercel Hobby

关键决策:Supabase 只当 PostgreSQL 数据库用,认证、存储、业务逻辑全部自己掌控。

分层架构

所有请求走严格的分层:

bash 复制代码
前端(React 组件)
    │  fetch /api/*
    ▼
API Routes(app/api/*/route.ts)
    │  参数校验、鉴权、响应格式化
    ▼
Service 层(lib/services/*)
    │  业务逻辑、流程编排
    ▼
DAO 层(lib/dao/*)
    │  createServiceRoleClient()
    ▼
Supabase PostgreSQL(schema: 'wallpaper')

核心原则:前端永远不直接碰数据库。每次数据库操作都经过 API Route → Service → DAO 三层。对于独立项目来说可能显得"过度设计",但当你需要换数据库、加缓存、或者凌晨两点排查线上问题时,这个结构能救命。

BaseDAO:泛型 CRUD

所有数据访问类继承 BaseDAO,开箱即用的类型安全 CRUD:

typescript 复制代码
export class BaseDAO<T extends { id?: string }, TInsert = Partial<T>> {
  protected tableName: string;

  constructor(tableName: string) {
    this.tableName = tableName;
  }

  protected getClient() {
    return createServiceRoleClient();
  }

  async findById(id: string): Promise<{ data: T | null; error: any }> {
    const { data, error } = await this.getClient()
      .from(this.tableName)
      .select('*')
      .eq('id', id)
      .single();

    if (error?.code === 'PGRST116') return { data: null, error: null };
    return { data: data as T, error };
  }

  async findMany(options?: {
    filters?: Record<string, any>;
    orderBy?: { column: string; ascending?: boolean };
    limit?: number;
  }): Promise<{ data: T[] | null; error: any }> {
    let query = this.getClient().from(this.tableName).select('*');
    if (options?.filters) {
      Object.entries(options.filters).forEach(([key, value]) => {
        query = query.eq(key, value);
      });
    }
    if (options?.orderBy) {
      query = query.order(options.orderBy.column, {
        ascending: options.orderBy.ascending ?? true,
      });
    }
    if (options?.limit) query = query.limit(options.limit);

    const { data, error } = await query;
    return { data: (data as T[]) || [], error };
  }

  // + create(), update(), delete(), count()
}

新建一个表的 DAO 就是几行代码:

typescript 复制代码
export class WallpaperDAO extends BaseDAO<Wallpaper> {
  constructor() { super('wallpapers'); }

  async findByCategory(categoryId: string) {
    return this.findMany({
      filters: { category_id: categoryId },
      orderBy: { column: 'created_at', ascending: false },
    });
  }
}

BaseService:统一校验和错误处理

所有 Service 继承 BaseService,提供 UUID 校验、权限检查、分页封装、结构化错误处理等公共能力:

typescript 复制代码
export abstract class BaseService {
  protected validateUuid(id: string, fieldName = 'ID'): void {
    if (!/^[0-9a-f]{8}-...-[0-9a-f]{12}$/i.test(id)) {
      throw new ApiError(`Invalid ${fieldName}`, 400, 'INVALID_UUID');
    }
  }

  protected handleError(error: any, operation: string): never {
    if (error instanceof ApiError) throw error;
    throw new ApiError(`Failed to ${operation}`, 500, 'INTERNAL_ERROR');
  }

  protected createPaginatedResponse<T>(data: T[], total: number, options: PaginationOptions) {
    return { data, pagination: { page: options.page, limit: options.limit, total } };
  }
}

handleError 放在一个地方,所有 Service 统一的日志和错误结构。不会再出现 50 个文件里散落着 catch (e) { return Response.json({ error: "出错了" }) } 的情况。

为什么只用 Supabase 的数据库

Supabase 提供 Auth、RLS、Storage、Edge Functions------一整套后端平台。我只用了 PostgreSQL 这一个功能。原因:

认证 :我需要 Google OAuth + 邮箱密码登录,JWT token 完全自己掌控。Supabase Auth 挺好,但一旦需要自定义 claims、角色权限、非标准流程,就要跟抽象层较劲。自己用 jsonwebtoken + bcrypt 撸了一个,花了一个周末,换来完全的灵活性。

RLS:Row-Level Security 很强大,但调试起来很痛苦------查询没返回数据,是策略写错了还是查询条件有问题?用 service-role 客户端 + Service 层显式权限检查,问题永远清晰。

存储:Cloudflare R2 免费 10GB 存储 + 1000 万次读取/月。Supabase Storage 免费 1GB。对壁纸平台来说,图片就是产品,R2 在经济性上完胜。

核心模式是 createServiceRoleClient()------创建一个绕过 RLS 的 Supabase 客户端,只在服务端调用:

typescript 复制代码
export function createServiceRoleClient() {
  return createClient(process.env.SUPABASE_URL!, process.env.SUPABASE_SERVICE_ROLE_KEY!, {
    db: { schema: 'wallpaper' },  // 自定义 schema,不用默认的 public
    auth: { autoRefreshToken: false, persistSession: false },
  });
}

注意用了自定义 schema 'wallpaper',让应用表和 Supabase 内部表彻底分开。

图片处理管道

壁纸要适配所有设备------6.7 寸 iPhone、27 寸 iMac、12.9 寸 iPad。一种尺寸行不通。

管理员上传原图到 R2 后,异步任务自动生成 4 个版本

markdown 复制代码
原图(上传到 R2)
    ├── thumbnail    → 列表缩略图
    ├── square       → 800×800 方形网格图
    ├── preview      → 高清预览(详情页)
    └── watermarked  → 带品牌水印的预览图

尺寸按设备类型区分:

设备 缩略图 预览图
手机 400×800 1440×2560
桌面 800×450 2560×1440
平板 600×800 2048×2732

所有变体存为 WebP(90-92% 质量)。水印用双层策略:右下角品牌文字 + 全图对角线低透明度平铺------简单的角标水印太容易裁掉。

成本明细

服务 方案 月费
Vercel Hobby $0
Supabase Free(500MB 数据库) $0
Cloudflare R2 Free(10GB 存储 + 1000 万读取) $0
Creem.io 按交易抽成 $0 底费
域名 .com ~ <math xmlns="http://www.w3.org/1998/Math/MathML"> 0.83 ( 0.83( </math>0.83(10/年)
合计 ~$0.83/月

Vercel Hobby 的代价是 10 秒函数超时。大部分 API 调用没问题,但图片处理可能超限。Supabase Free 给 500MB 数据库,对几千张壁纸的早期产品绰绰有余。

这套架构的核心优势:第一块钱的收入几乎是纯利润。只有流量和存储真正上量时才需要付费------而那时候你已经付得起了。

踩坑总结

1. Vercel 10 秒超时是真的

Sharp 处理一张手机壁纸(1440×2560,4 个变体)大约 3-4 秒。但批量处理 5 张就会超时。我的方案:每次只处理 3 张(串行),前端轮询自动续跑。不优雅,但在 $0 方案上跑得稳。

2. Supabase 默认 1000 行限制

这个坑很隐蔽。Supabase 的 PostgREST 默认最多返回 1000 行。如果有 1200 张壁纸,.select('*') 静默返回 1000 条------没有报错,没有警告。现在我每个查询都显式设 limit,并且用 count + head 做统计查询而不是全表 select。

3. R2 Presigned URL 实现秒级下载

用户购买后不经过服务器中转,直接 302 重定向到 R2 的 presigned URL。Cloudflare 边缘网络送文件,比 Vercel 单区域快得多。从"点击后等 5 秒"变成"点击即下载"。

4. 产品内容多语言比 UI 翻译难得多

next-intl 翻译按钮文字很简单。翻译产品内容------壁纸标题、Pack 描述------完全不同。需要在数据库里存翻译、优雅处理缺失翻译、决定缺失时显示原文还是隐藏。WallpaperSense 用 AI 异步任务自动翻译,这是另一篇文章的话题。

5. 自定义 Schema 省心

'wallpaper' 替代默认的 'public' schema,让应用表和 Supabase 内部表(auth.usersstorage.objects 等)彻底分开,迁移时心里更有底。

系列预告

这篇文章覆盖了 WallpaperSense 的高层架构。更多深入话题正在路上:

  • AI 自动生成壁纸标题、描述和 SEO Slug
  • Vercel Hobby 10 秒超时下处理 8K 图片的生存指南
  • Sharp.js 批量生成多版本壁纸实战
  • Cloudflare R2 + Presigned URL 秒级下载方案
  • Supabase 性能踩坑:全表扫描、1000 行限制、N+1 查询

如果你也在做独立产品,或者对全栈架构感兴趣,欢迎关注这个系列。


WallpaperSense 已上线:wallpapersense.com。一个独立开发者做的壁纸平台------好看的壁纸不贵,跑它的基础设施也不贵。


标签: #nextjs #supabase #cloudflare #typescript #独立开发 #全栈 #架构设计

相关推荐
左夕3 小时前
分不清apply,bind,call?看这篇文章就够了
前端·javascript
滕青山3 小时前
文本行过滤/筛选 在线工具核心JS实现
前端·javascript·vue.js
时光不负努力3 小时前
编程常用模式集合
前端·javascript·typescript
大雨还洅下3 小时前
前端JS: 跨域解决
javascript
OpenTiny社区4 小时前
OpenTiny NEXT-SDK 重磅发布:四步把你的前端应用变成智能应用
前端·javascript·ai编程
梦想CAD控件4 小时前
在线CAD开发包结构与功能说明
前端·javascript·vue.js
时光不负努力4 小时前
TS 常用工具类型
前端·javascript·typescript
Hilaku4 小时前
我会如何考核一个在简历里大谈 AI 提效的高级前端?
前端·javascript·面试
进击的尘埃4 小时前
Vue3 中 emit 能 await 吗?事件机制里的异步陷阱
javascript