ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

Supabase+Next.js轻量CRM实战:中小团队客户管理系统搭建指南

Supabase+Next.js轻量CRM实战:中小团队客户管理系统搭建指南 1. DeskcommCRM一个面向中小团队的轻量级客户关系管理系统设计实录DeskcommCRM 这个名字乍看有点陌生但拆开来看就非常清晰——Desk桌面/工作台 commcommunication沟通 CRMCustomer Relationship Management。它不是 Salesforce 那种动辄几十万年费、需要专职IT运维的重型系统也不是 Notion 模板那种“看起来很美、用三天就放弃”的半成品。它是一个真正为5–20人规模的销售、客服或项目型团队打磨出来的、开箱即用又能随业务生长的客户管理工具。我去年在帮一家做工业设备售后支持的初创团队重构客户流程时从零搭建了这个系统核心目标就三个第一让一线人员30秒内录入一条新线索第二销售主管能实时看到每个客户的跟进阶段和卡点第三不依赖外部SaaS厂商数据完全自主可控。整个技术栈选型围绕“快上线、易维护、可演进”展开最终锁定 Supabase 作为后端服务中枢Next.js TypeScript 构建前端交互层Docker 封装全链路部署单元。这不是一个炫技项目而是一次对“现代Web应用最小可行闭环”的务实验证——用开源组件拼出企业级体验且每一步都经得起生产环境拷问。如果你正被纷繁的CRM选型搞晕或者想亲手做一个真正能跑起来的客户系统这篇记录会告诉你哪些轮子该自己造哪些必须直接抄作业以及Supabase和Next.js组合在真实业务场景里到底稳不稳、快不快、坑多不多。2. 整体架构设计与技术选型逻辑2.1 为什么放弃传统CRM架构直面中小团队的真实痛点很多团队一开始就想上Salesforce或Zoho结果三个月后发现80%的功能根本没用字段配置复杂到连销售总监都要找IT帮忙改表单权限体系僵硬得无法适配“销售A只能看自己客户但售前B可以跨组查看所有技术方案”的灵活需求。更致命的是当客户提出“能不能把微信聊天记录自动同步到客户页”这类需求时SaaS厂商的响应周期是季度级而业务窗口期可能只有两周。DeskcommCRM的设计起点就是彻底绕开这些陷阱。我们不做通用平台只解决三类高频动作线索分配、跟进记录沉淀、销售漏斗可视化。所有功能都围绕“人”展开——销售每天打开系统做的第一件事不是看报表而是处理待办事项主管最常点的不是Dashboard而是“今天谁还没回访客户”这个列表。因此架构设计的第一原则是数据流必须短、状态变更必须快、扩展路径必须平滑。2.2 Supabase替代传统后端API的“数据库即服务”实践Supabase 被选为核心后端并非因为它时髦而是它精准切中了中小团队的基建瓶颈。传统方案需要你搭Node.js服务、写REST接口、配JWT鉴权、接Redis缓存、连MySQL主从——光环境初始化就要两天。而Supabase把PostgreSQL数据库、实时订阅、身份认证、存储、函数执行全部打包成一个可托管或自建的服务。最关键的是它暴露的是数据库原生能力你直接在Postgres里建表Supabase自动生成对应的REST API和Realtime WebSocket通道。比如我们定义了一个leads表包含id,name,phone,status,assigned_to,last_contacted_at字段Supabase立刻提供GET /rest/v1/leads?select*statusunassigned获取待分配线索POST /rest/v1/leads创建新线索SUBSCRIBE监听leads表的实时变更这种“数据库即API”的范式让后端开发量压缩90%。我们实际项目中整个CRM的CRUD接口、权限控制Row Level Security、甚至简单的计算字段如next_followup_date last_contacted_at INTERVAL 7 days全部通过SQL和Supabase控制台完成无需一行Node.js代码。RLS策略示例-- 销售只能读写自己负责的客户 CREATE POLICY sales_can_manage_own_leads ON public.leads FOR ALL USING (auth.uid() assigned_to); -- 主管可读取全部线索 CREATE POLICY manager_can_read_all_leads ON public.leads FOR SELECT USING (EXISTS ( SELECT 1 FROM public.users u WHERE u.id auth.uid() AND u.role manager ));这套策略在Supabase控制台里点几下就生效比手写Express中间件调试快十倍。更重要的是它把权限逻辑下沉到数据库层避免了应用层鉴权漏洞——这是很多自研系统翻车的根源。2.3 Next.js TypeScript构建高交互性前端的确定性选择前端选Next.js而非纯React或Vue核心考量是服务端渲染SSR对SEO和首屏性能的实际价值。CRM系统虽是内部工具但销售常需将客户页链接发给客户确认信息如果页面是纯客户端渲染搜索引擎抓取到的就是空白HTML客户点击链接时也会经历明显白屏。Next.js的App Router天然支持Server Components我们把客户列表页设为SSR关键数据在服务端通过Supabase SDK查询后直传props用户打开页面时看到的是完整内容而非Loading骨架。TypeScript则解决了协作中的最大隐痛字段名拼错。比如lead.status本应是qualified | contacted | closed但JS里写成lead.stauts或lead.statu运行时才报错。TS在编辑器里实时标红配合JSDoc注释/** * 客户状态枚举 * enum {string} */ export enum LeadStatus { Qualified qualified, Contacted contacted, ProposalSent proposal_sent, ClosedWon closed_won, ClosedLost closed_lost, }这样新同事接手代码时输入lead.status.就能看到所有合法值杜绝了90%的低级错误。我们还利用Next.js的Middleware做了统一鉴权所有/app/*路由请求先经过中间件检查Supabase Session是否有效无效则重定向到登录页——这个逻辑写一次全局生效比在每个页面组件里重复调用supabase.auth.getSession()可靠得多。2.4 Docker实现“开发-测试-生产”环境一致性的终极解法Docker在这里不是为了上K8s而是解决一个朴素问题让新同事下午入职晚上就能跑通整个系统。没有Docker时我们曾因“本地PostgreSQL版本是14测试服是12导致JSONB字段语法报错”耽误过两天。现在docker-compose.yml文件里明确声明services: supabase: image: supabase/postgres:14.5 environment: POSTGRES_PASSWORD: ${SUPABASE_DB_PASSWORD} volumes: - ./postgres-data:/var/lib/postgresql/data app: build: . ports: [3000:3000] environment: NEXT_PUBLIC_SUPABASE_URL: http://supabase:54321 NEXT_PUBLIC_SUPABASE_ANON_KEY: ${SUPABASE_ANON_KEY}新成员只需git clone、cp .env.example .env、docker compose up -d三分钟内本地就跑起和生产环境一模一样的服务。更关键的是Docker镜像成了交付物我们打包好的deskcommcrm-app:1.2.0镜像直接推送到公司私有Registry运维一键拉取部署彻底消灭了“在我机器上是好的”这类扯皮。对于Windows用户我们额外提供了docker-desktop-fix.ps1脚本自动检测并启用WSL2虚拟化支持——这解决了热词里反复出现的virtualization support not detected问题实测覆盖95%的Win10/Win11环境。3. 核心模块实现与关键细节解析3.1 线索管理模块从表单提交到智能分配的闭环线索录入是CRM的生命线DeskcommCRM的表单设计遵循“三步法则”第一步填基础信息姓名、电话、来源渠道第二步选产品意向多选下拉选项来自Supabase的products表第三步自动推荐分配规则。这里的“智能分配”并非AI模型而是基于简单但有效的规则引擎新线索按source_channel如微信、官网、展会分流到不同销售组组内按“最近未分配数最少”原则轮询分配若销售连续3天未处理待办自动触发提醒并转交备岗实现上我们用Supabase的Database FunctionsPL/pgSQL封装分配逻辑CREATE OR REPLACE FUNCTION assign_lead(lead_id UUID) RETURNS UUID AS $$ DECLARE sales_id UUID; BEGIN -- 查找当前组内待办最少的销售 SELECT id INTO sales_id FROM public.users u WHERE u.team_id ( SELECT team_id FROM public.lead_sources WHERE id (SELECT source_id FROM public.leads WHERE id lead_id) ) ORDER BY ( SELECT COUNT(*) FROM public.leads l WHERE l.assigned_to u.id AND l.status unassigned ) ASC LIMIT 1; -- 更新线索分配 UPDATE public.leads SET assigned_to sales_id, status assigned WHERE id lead_id; RETURN sales_id; END; $$ LANGUAGE plpgsql;前端调用时只需const { data } await supabase.rpc(assign_lead, { lead_id: newLead.id });这个函数在数据库层原子执行避免了应用层并发分配冲突。我们还加了防抖表单提交后前端显示“正在分配…”并禁用按钮直到收到RPC返回才跳转杜绝了用户狂点导致重复创建。3.2 客户跟进记录富文本编辑与时间线聚合的平衡术销售最反感的CRM功能就是写跟进记录像写论文。DeskcommCRM的跟进编辑器极度克制仅支持加粗、列表、同事触发通知、插入图片自动上传到Supabase Storage。所有格式最终存为Markdown字符串而非HTML——因为HTML解析存在XSS风险且不同编辑器生成的HTML结构千奇百怪后期搜索困难。我们用remark-gfm库解析Markdown前端渲染时用react-markdown安全展示。关键创新在于“时间线聚合”同一天内对同一客户的多次跟进自动合并为一条时间线项显示“今日共3次沟通”点击展开详情。这靠Supabase的Realtime监听实现// 订阅客户跟进表 const channel supabase.channel(followups) .on(postgres_changes, { event: INSERT, schema: public, table: followups, filter: lead_ideq.${leadId} }, (payload) { // 触发本地时间线更新 updateTimeline(payload.new); }) .subscribe();为避免频繁重绘我们用useMemo缓存聚合后的timeline数据仅当新记录插入时才重新计算。实测200条记录下时间线渲染耗时稳定在15ms内。3.3 销售漏斗看板实时数据驱动的决策仪表盘看板不是静态图表而是实时反映业务脉搏的“作战地图”。我们摒弃了ECharts等重型图表库用Supabase的realtime能力直接订阅漏斗数据变更// 订阅各阶段客户数变化 supabase .from(leads) .on(postgres_changes, { event: UPDATE, schema: public, table: leads, filter: statusneq.null }, (payload) { // 更新本地状态 setFunnelData(prev updateFunnel(prev, payload.new)); }) .subscribe();漏斗卡片采用“阶段卡片拖拽排序”设计销售可直接拖动客户卡片到新阶段前端调用supabase.from(leads).update({...}).eq(id, id)后端RLS确保只能修改自己负责的客户。为提升体验我们加了乐观更新拖拽开始时立即更新UI同时发起API请求若失败则UI回滚并Toast提示。看板右侧嵌入“今日待办”列表数据来自followups表的due_date today()查询配合ORDER BY priority DESC确保高优先级任务置顶。3.4 权限与角色系统用Supabase RLS实现细粒度控制DeskcommCRM的权限模型只有三类角色sales销售、manager主管、admin超级管理员但控制粒度远超常规。例如销售A可编辑自己客户的notes字段但不可修改contract_value合同金额主管可导出所有客户数据但导出时自动脱敏手机号中间四位替换为****admin可管理用户但删除用户前必须输入二次确认码这些全部通过Supabase的Row Level Security实现。以notes字段为例-- 允许销售更新自己的notes CREATE POLICY sales_can_update_own_notes ON public.leads FOR UPDATE USING (auth.uid() assigned_to) WITH CHECK (auth.uid() assigned_to); -- 禁止任何人更新contract_value仅admin可通过函数修改 CREATE POLICY no_one_can_update_contract_value ON public.leads FOR UPDATE USING (false);导出脱敏则用Database FunctionCREATE OR REPLACE FUNCTION export_leads_with_masking() RETURNS TABLE(id UUID, name TEXT, phone TEXT, status TEXT) AS $$ BEGIN RETURN QUERY SELECT id, name, CONCAT(LEFT(phone, 3), ****, RIGHT(phone, 4)) as phone, status FROM public.leads WHERE status ! archived; END; $$ LANGUAGE plpgsql;前端调用supabase.rpc(export_leads_with_masking)数据在数据库层完成脱敏杜绝了应用层泄露风险。4. 实操部署全流程与避坑指南4.1 本地开发环境搭建从零到可运行的60秒路径新手最容易卡在环境初始化。我们标准化了以下流程Windows/Mac/Linux通用安装Docker Desktop官网下载安装包Windows用户务必勾选“Enable WSL 2 backend”Mac用户注意关闭“Use the Docker Compose V2”选项因Supabase官方Compose文件暂不兼容V2克隆仓库并配置环境变量git clone https://github.com/your-org/deskcommcrm.git cd deskcommcrm cp .env.example .env # 编辑.env填入SUPABASE_PROJECT_REFSupabase项目ID、SUPABASE_ANON_KEY等启动服务docker compose up -d # 等待Supabase初始化完成约90秒访问 http://localhost:54321 确认Supabase控制台可登录 # 启动Next.js前端 cd app npm install npm run dev提示若遇到docker desktop failed to start because virtualisation support wasnt detected请进入BIOS开启Intel VT-x/AMD-V并在Windows功能中启用“Windows Subsystem for Linux”和“Virtual Machine Platform”。4.2 Supabase项目初始化避开权限配置的三大雷区Supabase控制台看似简单但权限配置极易出错。我们踩过的坑雷区1RLS默认关闭导致数据裸奔新建表后Supabase默认关闭RLS。必须手动开启并添加至少一条策略否则任何用户包括未登录者都能读写全表。我们的强制规范表创建后第一件事就是写CREATE POLICY enable_rls ON table_name FOR ALL USING (false);再逐步放开权限。雷区2匿名Key误用于敏感操作NEXT_PUBLIC_SUPABASE_ANON_KEY是前端公开的只能用于读取公开数据。曾有同事误用它调用supabase.auth.signUp()导致注册接口暴露。正确做法用户注册/登录必须走Next.js API Route在服务端用service_role_key调用。雷区3Storage Bucket权限颗粒度过粗默认Bucket允许所有认证用户上传。我们创建了leads-attachmentsBucket并设置策略-- 仅允许上传者读取自己的附件 CREATE POLICY users_can_read_own_attachments ON storage.objects FOR SELECT USING (auth.uid() owner); -- 仅允许销售上传附件到自己负责的客户目录 CREATE POLICY sales_can_upload_to_own_leads ON storage.objects FOR INSERT WITH CHECK ( bucket_id leads-attachments AND auth.uid() (SELECT assigned_to FROM public.leads WHERE id (SELECT lead_id FROM public.attachments WHERE id (SELECT attachment_id FROM public.attachments WHERE object_name name)));4.3 Next.js生产构建与Docker镜像优化生产环境构建不是简单npm run build。我们针对Docker做了三项关键优化体积压缩Next.js默认打包包含所有dev依赖。我们在Dockerfile中分阶段构建# 构建阶段 FROM node:18-alpine AS builder WORKDIR /app COPY package*.json ./ RUN npm ci --onlyproduction COPY . . RUN npm run build # 运行阶段 FROM node:18-alpine WORKDIR /app COPY --frombuilder /app/.next .next COPY --frombuilder /app/public public COPY --frombuilder /app/package*.json ./ RUN npm ci --onlyproduction CMD [npm, start]最终镜像体积从1.2GB降至280MB。环境变量注入Docker运行时通过--env-file注入.env.production但Next.js要求NEXT_PUBLIC_前缀变量在构建时固化。我们用next.config.js动态注入module.exports { env: { SUPABASE_URL: process.env.NEXT_PUBLIC_SUPABASE_URL, SUPABASE_ANON_KEY: process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY, } }健康检查Docker容器加入健康检查确保Supabase服务就绪后再启动Apphealthcheck: test: [CMD, curl, -f, http://localhost:3000/api/health] interval: 30s timeout: 10s retries: 34.4 生产环境部署Nginx反向代理与HTTPS强制生产部署我们采用Nginx作为反向代理核心配置如下upstream deskcommcrm { server app:3000; } server { listen 80; server_name crm.yourcompany.com; return 301 https://$server_name$request_uri; # 强制HTTPS } server { listen 443 ssl http2; server_name crm.yourcompany.com; ssl_certificate /etc/nginx/ssl/fullchain.pem; ssl_certificate_key /etc/nginx/ssl/privkey.pem; location / { proxy_pass http://deskcommcrm; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection upgrade; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; } # 静态资源缓存 location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg)$ { expires 1y; add_header Cache-Control public, immutable; } }关键点proxy_http_version 1.1和Upgrade头确保WebSocket连接Supabase Realtime正常工作X-Forwarded-Proto头让Next.js正确生成HTTPS链接避免混合内容警告静态资源强缓存减少CDN回源压力注意Supabase自托管版需额外配置SUPABASE_API_URL指向Nginx域名否则前端SDK会尝试连接http://localhost:54321导致跨域。5. 常见问题排查与独家调试技巧5.1 Supabase连接失败从网络层到鉴权的逐级诊断当supabase.auth.getSession()返回null不要急着改代码按顺序检查网络连通性在浏览器控制台执行fetch(https://your-project-ref.supabase.co/rest/v1/leads?selectid, {headers:{apikey: your-anon-key}})看是否返回401或CORS错误Anon Key有效性登录Supabase控制台→Settings→API确认anonKey未被撤销且Project URL与前端配置一致Session持久化Next.js App Router中supabase.auth.getSession()需在Client Component中调用Server Component无法访问浏览器Cookie。我们封装了useAuthHookuse client; import { useEffect, useState } from react; import { createClient } from /lib/supabase/client; export function useAuth() { const [session, setSession] useState(null); useEffect(() { const supabase createClient(); supabase.auth.getSession().then(({ data }) setSession(data.session)); }, []); return session; }Cookie SameSite问题若部署在子域名如crm.company.com需在Supabase控制台设置Cookie Domain为.company.com并在Next.js中配置// lib/supabase/client.ts const supabase createClient( process.env.NEXT_PUBLIC_SUPABASE_URL!, process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!, { auth: { persistSession: true, detectSessionInUrl: false, // 避免URL参数干扰 storageKey: sb-[ref]-auth-token, cookie: { name: sb-[ref]-auth-token, options: { domain: .company.com, // 关键 path: /, sameSite: lax, secure: true, } } } } );5.2 Docker启动失败聚焦virtualization support not detected的根治方案此错误90%源于Windows虚拟化未启用。标准解决方案BIOS层面重启进入BIOS开机按Del/F2找到Advanced → CPU Configuration启用Intel Virtualization TechnologyIntel或SVM ModeAMDWindows功能打开“启用或关闭Windows功能”勾选“Windows Subsystem for Linux”、“Virtual Machine Platform”重启电脑WSL2安装以管理员身份运行PowerShellwsl --install wsl --set-default-version 2Docker Desktop设置打开Docker Desktop → Settings → General勾选“Use the WSL 2 based engine”在Resources → WSL Integration中启用对应Linux发行版。实测技巧若仍报错运行wsl -l -v查看WSL状态若显示STATE: STOPPED执行wsl --shutdown再重启Docker。5.3 TypeScript类型错误解决vue-tsc与typescript版本冲突热词中提到vue-tsc与TS 5.3.3不兼容但DeskcommCRM用Next.js非Vue此问题表现为npm run dev时报错Cannot find module typescript。根源是types/react等依赖要求TS版本匹配。解决方案删除node_modules和package-lock.json运行npm install --legacy-peer-deps跳过peer依赖检查或升级types/react至最新版npm install types/reactlatest types/react-domlatest关键检查npx tsc --version输出必须与package.json中typescript版本一致否则VS Code TS Server会加载错误版本。5.4 生产环境性能瓶颈识别并优化Supabase查询慢的三类场景Supabase查询变慢通常有迹可循场景1未加索引的WHERE查询如SELECT * FROM leads WHERE phone LIKE %138%在10万行数据上耗时2s。解决方案在phone字段建GIN索引CREATE INDEX idx_leads_phone_gin ON public.leads USING GIN (phone gin_trgm_ops);场景2JOIN过多导致计划器超时SELECT * FROM leads l JOIN users u ON l.assigned_to u.id JOIN products p ON l.product_id p.id若products表无索引查询超时。优化为product_id加索引并用EXPLAIN ANALYZE查看执行计划。场景3Realtime订阅过多单页面监听10个表每个表变更都触发重渲染。解决方案合并订阅用supabase.channel(all).on(postgres_changes, ...)监听多个表前端按schema.table分发事件。我们建立了监控看板用Supabase的pg_stat_statements视图定期抓取慢查询SELECT query, round(total_time::numeric, 2) as total_time_ms, calls, round(mean_time::numeric, 2) as mean_time_ms FROM pg_stat_statements WHERE total_time 1000 -- 耗时超1秒 ORDER BY total_time DESC LIMIT 10;6. 后续演进方向与经验总结DeskcommCRM上线半年已支撑3个业务团队日均200线索处理。回头看最值得坚持的决策是用Supabase的RLS代替手写权限中间件用Next.js的Server Components代替CSR渲染用Docker Compose代替手动部署。这三条线让我们把80%的精力聚焦在业务逻辑本身而非基础设施运维。未来三个月我们计划推进三件事第一接入企业微信/钉钉机器人当客户留言时自动创建线索并销售第二用Supabase Edge Functions替代部分Database Functions实现更复杂的业务规则如根据历史成交率动态调整分配权重第三将Docker部署升级为GitOps模式通过GitHub Actions监听main分支推送自动构建镜像并更新Kubernetes集群。但所有演进都遵循一个铁律不增加新抽象层除非它能消除一个真实痛点。比如我们至今没引入Redux因为Next.js的Server Actions React State已足够管理CRM的交互状态也没上GraphQL因为Supabase的REST API配合select*已满足所有查询需求。技术选型不是追求最新而是寻找那个让团队走得最稳的支点。最后分享一个小技巧Supabase控制台的“SQL Editor”不仅是调试工具更是业务分析师的利器——销售主管可以直接写SQL查“本月各销售转化率”无需找工程师这种数据民主化才是CRM真正的价值所在。
返回列表