admin 面板 (Supabase) 是做什么的?
为 Next.js 项目 (App Router) 生成一个 1CRM 风格的 /admin 管理页面。包含3个标签页——总览 (KPI + 图表)、客户管理 (表格 + 筛选)、邮件营销 (撰写 + 批量发送 + 历史记录)。
核心理念:比 1CRM 更简化(使用统一密码,无多用户),但提供足够的图表、环比变化和邮件营销功能,足以满足 1-2 人企业主的需求。所有状态存储在 Supabase 中,生成的代码与现有项目独立。
/admin → 弹出“请输入管理密码” → 各标签页通过 /api/admin/{leads|dashboard|campaigns} 接口获取数据
→ 每次请求发送 x-admin-pass 请求头
→ 服务端使用 timing-safe 比较方法与 ADMIN_PASSWORD 环境变量进行比对
密码存储在 React state 中——刷新页面=重新弹出密码框 (有意的设计,简化流程,无需 session cookie)。
仅支持 Supabase — Vercel KV 变体已被移除。原因:邮件活动 (campaigns) 和发送记录 (campaign_sends) 需要 SQL JOIN 以及 service_role 绕过 RLS;KV 原生不支持此功能。
何时使用此技能
- 项目已通过
lib/leads-supabase.ts(来自biz-setup-sepay-payment并选择了 Supabase) 集成了 Sepay 支付和潜在客户存储 → 需要视图层、仪表盘和邮件营销功能。 - 用户想要一个 CRM 风格的管理页面:包含带环比变化的 KPI 卡片、按日统计的折线图、转化率环形图,并能向已注册客户发送促销邮件。
不适用场景:
- 项目尚未创建
lib/leads-supabase.ts→ 请先告知用户运行/biz-setup-sepay-payment(选择 Supabase 后端) 进行初始化。 - 项目使用 Vercel KV 而非 Supabase → 此技能 不再 支持 KV。用户只有两种选择:迁移到 Supabase 或自行手动移植代码。
- 需要多管理员且带角色/权限功能 → 此技能使用统一密码,不适用。
- 需要详细的邮件追踪功能 (如每个收件人的打开/点击统计) → 此技能仅追踪发送成功/失败状态。用户需要支持追踪功能的邮件服务,如 Resend、SendGrid 或 Brevo 的批量发送功能。
工作流程 (6 个阶段)
阶段 0: 检测 Next.js App Router + 验证 lib/leads-supabase.ts 存在 + 验证 SMTP 环境变量
阶段 1: 询问用户设置后台密码 (默认 123456) — 关卡 (GATE)
阶段 2: 安装 npm 依赖 recharts
阶段 3: 在 Supabase SQL 编辑器中运行 supabase-migration-admin.sql — 关卡 (GATE)
阶段 4: 将 ADMIN_PASSWORD 写入 .env.local 文件
阶段 5: 创建 9 个代码文件 (5 个库文件 + 4 个路由文件 + 1 个页面文件)
阶段 6: 三级测试 (curl 测试仪表板 API、UI 界面冒烟测试、发送测试邮件活动)
阶段 0 — 检测并验证依赖
# 检查 Next.js + App Router
test -f next.config.js -o -f next.config.mjs -o -f next.config.ts && echo "Next.js: ✓"
test -d app && echo "App Router: ✓" || { echo "✗ 需要 App Router。此技能不支持 Pages Router。"; exit 1; }
# 验证 Supabase 潜在客户存储
test -f lib/leads-supabase.ts && echo "Supabase store: ✓" || { echo "✗ 缺少 lib/leads-supabase.ts — 请先运行 /biz-setup-sepay-payment 并选择 Supabase"; exit 1; }
test -f lib/leads-store.ts && echo "Provider-agnostic wrapper: ✓"
test -f lib/supabase-admin.ts && echo "Service-role client: ✓"
# 验证 SMTP 环境变量 (来自 biz-email-setup)
grep -E "^SMTP_HOST=" .env.local && echo "SMTP_HOST: ✓" || { echo "✗ 缺少 SMTP 环境变量 — 请先运行 /biz-email-setup"; exit 1; }
grep -E "^SMTP_USER=" .env.local
grep -E "^SMTP_PASS=" .env.local
grep -E "^MAIL_FROM=" .env.local
# 验证包依赖
grep '"nodemailer"' package.json && echo "nodemailer: ✓"
grep '"@supabase/supabase-js"' package.json && echo "@supabase: ✓"
如果缺少 lib/leads-supabase.ts:
⚠ 我注意到项目还没有 Supabase 潜在客户存储。请您先运行
/biz-setup-sepay-payment(选择 Supabase 后端) 来连接支付功能、创建leads表和设置 RLS,然后再回来,我将为您搭建管理仪表盘。
如果缺少 SMTP 环境变量:
⚠ 我注意到 SMTP 尚未配置。邮件营销标签页需要通过 SMTP 发送邮件。请您先运行
/biz-email-setup选择邮件提供商 (Gmail/Resend SMTP/Brevo/SendGrid/Zoho...),然后再回来,我将为您搭建管理仪表盘。如果您确认暂时不需要邮件功能,我也可以跳过此部分继续搭建。
向用户总结:
我已检测到:Next.js App Router ✓,Supabase 潜在客户存储 ✓,SMTP 配置 ✓。我将进行以下操作:
- 询问您 /admin 页面的访问密码
- 安装
recharts(用于折线图和环形图)- 在 Supabase 中运行 SQL 迁移脚本以创建
campaigns和campaign_sends表- 将
ADMIN_PASSWORD写入 .env.local 文件- 生成 9 个文件 (5 个库文件 + 4 个路由文件 + 1 个页面文件)
- 指导您进行测试
准备好进入阶段 1 了吗?
阶段 1 — 询问密码 (关卡 GATE)
直接询问用户:
您希望为 /admin 页面设置什么密码?
- 我建议使用默认密码:
123456- 或者您也可以粘贴一个更强的密码用于生产环境 (至少 12 个字符,包含字母、数字和特殊字符)
请粘贴您的密码 (或直接输入 "123456" 使用默认值):
等待用户回复。获取用户粘贴的密码。如果为空 → 使用 123456。
在用户确认密码前,不要进入阶段 2。
阶段 2 — 安装 recharts
# 检测包管理器
test -f pnpm-lock.yaml && PM=pnpm
test -f yarn.lock && PM=yarn
test -z "$PM" && PM=npm
$PM add recharts # 或: pnpm add recharts / yarn add recharts
nodemailer 已通过 /biz-email-setup 安装,无需再次安装。
阶段 3 — 运行 SQL 迁移脚本 (关卡 GATE)
指导用户:
请您打开 Supabase 控制台 → SQL 编辑器 → 新建查询 → 粘贴下方完整内容 → 点击 运行:
[在此处粘贴 templates/supabase-migration-admin.sql 的内容]此 SQL 脚本将创建两个新表:
campaigns(邮件活动主表) 和campaign_sends(每个收件人的发送日志)。不会修改已有的leads表。运行完成后,请在 SQL 脚本的结尾部分确认——您应该会看到
campaigns: 0和campaign_sends: 0。
等待用户确认“SQL 已执行完毕”后,方可进入阶段 4。
阶段 4 — 写入 .env.local
将以下内容追加到 .env.local 文件:
# Admin /admin page password
ADMIN_PASSWORD=<user-paste-from-phase-1>
如果 ADMIN_PASSWORD 已存在 → 询问用户是否覆盖 (默认不覆盖)。
确保 .env.local 已在 .gitignore 中:
grep -qxF ".env.local" .gitignore 2>/dev/null || echo ".env.local" >> .gitignore
阶段 5 — 创建 9 个文件
lib/admin-auth.ts ← templates/lib-admin-auth.ts
lib/admin-stats.ts ← templates/lib-admin-stats.ts
lib/campaigns.ts ← templates/lib-campaigns.ts
lib/email-render.ts ← templates/lib-email-render.ts
lib/mailer.ts ← templates/lib-mailer.ts
app/admin/page.tsx ← templates/app-admin-page.tsx
app/api/admin/leads/route.ts ← templates/app-api-admin-leads-route.ts
app/api/admin/dashboard/route.ts ← templates/app-api-admin-dashboard-route.ts
app/api/admin/campaigns/route.ts ← templates/app-api-admin-campaigns-route.ts
邮件渲染模块:lib/email-render.ts 是 纯函数 模块 (不导入 nodemailer) —— 包含 textToHtml / wrapEmailHtml / personalize / renderCampaignEmail。lib/mailer.ts 重新导出这些函数并添加 SMTP transport。如此分离是为了让 app/admin/page.tsx (客户端组件) 能够直接导入渲染辅助函数用于“预览”功能,而不会将 nodemailer 打包到客户端 bundle 中。
注意:如果项目中已有来自 biz-email-setup 的 lib/mailer.ts:不要覆盖 —— 将此技能的邮件模块创建为 lib/admin-mailer.ts,并修改 app/api/admin/campaigns/route.ts 中的引用为 @/lib/admin-mailer。lib/email-render.ts 始终是安全的 (独立名称,不会冲突)。
注意导入别名:所有模板使用 @/lib/...。请确认项目中 tsconfig.json 的 paths 配置了 "@/*": ["./*"] (Next.js 默认配置)。如果没有,则需改为相对路径导入。
关于 lib/leads-store.ts 的注意事项:Leads API 路由从 @/lib/leads-store (来自 biz-setup-sepay-payment 的与具体数据源无关的包装器) 导入 listLeads。如果用户之前设置了 KV 后重命名了此文件,则需验证包装器指向的是 Supabase。
数据流图
浏览器 (admin/page.tsx)
│ 发起 fetch 请求 (携带 x-admin-pass 请求头)
├──► GET /api/admin/dashboard?days=30 → lib/admin-stats.ts → leads 表 → { kpi, timeseries, deltas }
├──► GET /api/admin/leads?status=... → lib/leads-store.ts → leads 表 → { leads, stats }
├──► GET /api/admin/campaigns → lib/campaigns.ts → campaigns 表 → { campaigns: [...] }
└──► POST /api/admin/campaigns (请求体包含 bodyFormat: 'text' | 'html')
└─► lib/campaigns.queryAudience(leads.where status/days)
└─► lib/email-render.renderCampaignEmail(format, body) → { html, text } (渲染一次)
└─► lib/campaigns.createCampaign (campaigns 行)
└─► loop (节流 200ms):
lib/mailer.personalize(html/text per recipient)
lib/mailer.sendOne(to, subject, html)
lib/campaigns.logCampaignSend (campaign_sends 行)
└─► lib/campaigns.setCampaignStatus (最终计数 + sentAt)
return { successCount, failCount }
客户端预览 (不调用服务器): admin/page.tsx 直接导入 lib/email-render,
“预览”按钮渲染 { html } + personalize(模板) → 在 <iframe srcDoc sandbox> 中显示。
阶段 6 — 三级测试
测试 1 — 使用 curl 测试仪表板 API
PASS="<password-từ-phase-1>"
# 错误密码 → 应返回 401
curl -i http://localhost:3000/api/admin/dashboard -H "x-admin-pass: wrong"
# 期望状态码: 401, 响应体: {"error":"invalid_password"}
# 正确密码 → 应返回 200,包含 KPI 和时间序列数据
curl -s http://localhost:3000/api/admin/dashboard?days=7 -H "x-admin-pass: $PASS" | jq '.period, .deltas'
# 期望返回: period {days, leads, paid, revenue, conversionRate}, deltas {leadsPct, ...}
测试 2 — 浏览器 UI 冒烟测试
1. pnpm dev (或 yarn/npm)
2. 打开浏览器访问 http://localhost:3000/admin
3. 应看到“管理页面”弹出窗口 → 输入错误密码 → 应显示“密码错误,请重试”
4. 输入正确密码 → 进入仪表盘
5. 总览标签页:应看到 4 个 KPI 卡片、折线图、环形图、7天/30天/90天时段选择器正常工作
6. 客户管理标签页:应看到表格、筛选项和 CSV 导出按钮
7. 邮件营销标签页:应看到邮件撰写表单和历史记录列表 (初始应为空)
8. 邮件营销标签页:输入内容 → 点击“👁 预览”按钮 → 应弹出模态框显示邮件
渲染效果 (切换“纯文本”和“HTML”格式以确认两种模式均正常)
测试 3 — 向自己发送测试邮件活动
重要:选择受众为
last_days= 7 (使用已设置的密码),并在邮件正文中包含{{name}}。先向数据库中唯一的测试线索 (拥有者自己的测试数据) 发送测试邮件,以确认整个流程无误,再向实际用户发送。
1. 进入邮件营销标签页
2. 活动名称: "测试发送"
3. 邮件主题: "来自 /admin 的测试邮件 --- 您收到了吗?"
4. 受众: "最近几天" → 7 天
5. 邮件正文:
你好 {{name}},
这是一封从管理后台发送的测试邮件,用于确认 SMTP 配置正常。
如果您收到了这封邮件,说明可以正式开始发送真实邮件活动了。
6. 点击“👁 预览”按钮 → 确认邮件显示正确 → 关闭模态框
7. 点击“立即发送”按钮 → 确认
8. 等待 10-30 秒 (200ms × 收件人数)
9. 验证:
- 应出现绿色横幅:“已成功发送 X/X 封邮件 (0 次错误)”
- 历史记录表格中应新增状态为“已发送”的行
- 检查收件箱 → 应收到主题为测试内容的邮件,且 {{name}} 已被替换为实际名称
最终输出
✓ 已成功创建 /admin CRM 风格管理仪表盘 (Supabase + recharts + 邮件营销功能)
📁 新增文件 (9 个):
lib/
admin-auth.ts (timing-safe 密码校验)
admin-stats.ts (KPI + 时间序列 + 环比变化)
campaigns.ts (CRUD + 受众查询)
email-render.ts (纯函数: textToHtml + wrapEmailHtml + renderCampaignEmail — 服务端和客户端预览共用)
mailer.ts (nodemailer 传输器 + 重新导出 email-render + 可选页脚)
app/admin/page.tsx (1CRM 三标签页 UI — 总览 / 客户管理 / 邮件营销 — 支持纯文本/HTML 格式 + 预览功能)
app/api/admin/
leads/route.ts (GET 列表 + 筛选)
dashboard/route.ts (GET KPI + 时间序列)
campaigns/route.ts (GET 历史记录 + POST 创建并发送)
🗄 Supabase 数据库迁移:
- 已创建 campaigns 和 campaign_sends 表 (RLS 拒绝所有,通过 service_role 绕过)
- 您已成功执行 templates/supabase-migration-admin.sql ✓
📦 NPM 依赖:
- recharts (已新安装)
- nodemailer (已通过 biz-email-setup 安装)
🔐 环境变量:
- ADMIN_PASSWORD 已写入 .env.local
- .gitignore 已包含 .env.local
- SMTP_* + MAIL_FROM 复用自 biz-email-setup
🧪 测试:
1. ✓ 使用 curl 测试仪表板 API (测试 1)
2. ✓ 浏览器 UI 冒烟测试 (测试 2)
3. ✓ 已向项目所有者发送测试邮件 (测试 3)
🔧 您还需要做的:
1. (如果部署到生产环境) 将 ADMIN_PASSWORD 添加到 Vercel 环境变量:
vercel env add ADMIN_PASSWORD production
2. 重新部署: vercel --prod
3. 打开 https://<domain>/admin → 输入密码 → 确认三个标签页正常工作
4. 首次发送真实邮件活动: 选择小范围受众 (如最近 7 天) 先测试
确认 SMTP 提供商的速率限制,然后再扩大到 '所有'
模板文件
| 文件 | 描述 |
|---|---|
templates/supabase-migration-admin.sql | 创建 campaigns 和 campaign_sends 表的数据库迁移脚本 |
templates/lib-admin-auth.ts | checkAdminPass() timing-safe 密码比较 |
templates/lib-admin-stats.ts | getDashboardData(days) — KPI + 时间序列 + 环比变化 |
templates/lib-campaigns.ts | campaigns 的 CRUD 操作 + queryAudience() |
templates/lib-email-render.ts | 纯函数模块 — textToHtml() + wrapEmailHtml() + personalize() + renderCampaignEmail() (支持纯文本/HTML 格式)。不导入 nodemailer → 可在客户端组件中用于预览 |
templates/lib-mailer.ts | nodemailer 传输器 + sendOne() + sleep() + 从 email-render 重新导出渲染辅助函数 |
templates/app-admin-page.tsx | 1CRM 风格页面,包含侧边栏 + 3 个标签页 + recharts。邮件标签页包含纯文本/HTML 格式切换和预览按钮 (iframe 模态框) |
templates/app-api-admin-leads-route.ts | 潜在客户表 API (调用 listLeads()) |
templates/app-api-admin-dashboard-route.ts | 仪表盘分析 API |
templates/app-api-admin-campaigns-route.ts | GET 列表 + POST 创建并发送 |
反模式 (请避免)
- ❌ 使用
===比较密码 → 容易受到时序攻击 (timing attack)。模板通过lib/admin-auth.ts使用crypto.timingSafeEqual进行比较。 - ❌ 在源代码中硬编码密码 → 容易忘记修改,导致生产环境部署时仍使用测试密码。始终通过
process.env.ADMIN_PASSWORD获取。 - ❌ 通过 URL 查询字符串传递密码 (
?pass=123456) → 密码会泄露到浏览器历史记录和访问日志中。应使用x-admin-pass请求头。 - ❌ 当用户只需要简单的后台功能时,单独创建登录页面 + session cookie + JWT。使用内联弹出窗口即可满足需求。
- ❌ 将密码存储在 localStorage → 可通过 JavaScript 读取,导致 XSS 攻击泄漏密码。模板将密码存储在 React state 中 (刷新即丢失,此为有意设计)。
- ❌ 跳过检查