Biz Telegram Payment Notify — Sepay 收款实时告警
此技能将 Telegram 机器人通知 集成到项目现有的 Sepay webhook 中。每当客户成功转账到已链接 Sepay 的银行账户时,技能会通过 Telegram 机器人向所有者(或销售团队群组)实时推送一条消息——包含客户姓名、电话号码、金额、产品名称和交易时间。
设计理念:面向客户的邮件自动回复(Resend)和面向所有者的 Telegram 告警服务于两个不同的角色。邮件是给客户的正式回复。Telegram 是给所有者的“提醒”——无论您在哪里,打开 Telegram 立即看到新订单,无需在 100 封邮件中翻找 Gmail。Telegram 延迟 < 2 秒,而邮件需要 30-60 秒。
为何选择 Telegram 而非 Slack/Discord/Zalo:Telegram 机器人 5 分钟即可完成设置(1 个 token + 1 个 chat_id),无需工作空间/服务器管理员权限,免费且无限制消息,推送通知比邮件更强,拥有原生 iOS/Android/桌面应用。Slack 需要工作空间和邀请。Discord 需要服务器和权限配置。Zalo OA 需要经过认证的企业账户(审批需要 3-7 天)和月度活跃用户费用。对于所有者侧的告警,Telegram 是最简单直接的路径。
输出:代码补丁 + 分步机器人设置指南 + 环境变量列表 + 三级测试计划。技能 不会自动测试发送 Telegram——要求用户先提供 bot token 和 chat_id,因为这些凭证属于用户资产。
何时使用此技能
- 用户刚完成 Sepay webhook 集成,希望在客户转账时通过 Telegram 接收实时通知。
- 用户已有
/api/payment-success(或类似路由)处理 Resend 邮件,现在希望 并行 添加 Telegram 告警——不创建新路由,不重复逻辑。 - 用户说“有订单时通知我”,并且上下文是带有 Sepay 集成的销售落地页。
不要在此情况下使用此技能:
- 用户需要 Slack/Discord 通知 → 属于其他用例。
- 用户需要 Zalo OA 机器人 → 需要经过认证的 OA 企业账户,结构不同且有成本。
- 用户尚未设置 Sepay webhook(还没有链接 Sepay 的银行账户,没有接收 webhook 的端点)→ 此技能不从头设置 Sepay,仅集成到现有 webhook。
- 用户只需要面向所有者的邮件通知 →
biz-email-setup中的邮件 B(所有者通知)已涵盖。
工作流程总览(6 个阶段)
阶段 0:检测现有的 Sepay webhook 路由(路径 + 框架 + 处理函数结构)
↓
阶段 0.5:检测客户数据存储(来自 /biz-setup-sepay-payment 的 lib/leads-kv.ts)
→ 是:使用实际的 KV 查找
→ 否:提供 2 个选项(原始模式 或者 先运行 /biz-setup-sepay-payment)
↓
阶段 1:引导用户创建 Telegram 机器人(BotFather → token → chat_id)
↓ [关卡——等待用户提供 token + chat_id]
↓
阶段 2:草拟消息格式 → 向用户显示预览 → 确认
↓
阶段 3:集成代码(lib/telegram.ts 辅助函数 + 集成到 webhook 处理函数 + 环境变量)
↓
阶段 4:测试计划(直接 curl 测试机器人 → 模拟 Sepay 负载 → 生产环境验证)
阶段 1 和阶段 4 有 等待用户的关卡,因为需要用户手动操作(获取 token、验证生产环境)。阶段 0/0.5/2/3 为技能主动执行。
阶段 0 — 检测现有的 Sepay webhook
此技能集成到现有 webhook,不创建新路由。首先定位处理函数。
查找 webhook 路由——根据常见关键词进行 grep 搜索:
# 在项目根目录下
sepay --include="*.ts" --include="*.js" --include="*.tsx" -l
grep -r "transferAmount|transferType|payment-success|payment-webhook" --include="*.ts" --include="*.js" -l
grep -r "transferAmount|transferType|payment-success|payment-webhook"
3 种常见场景:
| 场景 | 已有路径 | 操作 |
|---|---|---|
用户使用 biz-email-setup 流程 | app/api/payment-success/route.ts(或 pages/api/payment-success.ts)已有调用 sendCustomerEmail + sendOwnerEmail 的 Resend 代码 | 将 sendTelegramNotification 通过 Promise.allSettled 集成到同一位置 |
| 用户有 Sepay 特定路由 | app/api/sepay-webhook/route.ts 或 app/api/webhook/sepay/route.ts | 在解析并验证 Sepay 负载后集成(transferType === 'in' 分支) |
| 未找到任何路由 | — | 停止。向用户报告:“我还没有找到接收 Sepay 的 webhook 路由。请先设置 Sepay webhook(将银行账户链接到 Sepay → 创建指向您域名的 webhook URL),然后回来让我集成 Telegram。或者,如果 webhook 在其他端点,请告诉我路径。” |
读取现有处理函数以了解其结构:
- 是否对 Sepay 请求进行身份验证?(
Authorization头Apikey或 HMAC) - 是否根据
id或referenceCode进行去重?(如果 Sepay 收到非 200 响应会重试 → 需要去重以避免重复通知) - 是否根据
content(包含订单 ID 的转账备注)查找客户? - 返回给 Sepay 的响应格式:
{ success: true }还是纯文本200 OK?
如果现有处理函数 尚未去重——警告用户但不阻止。此技能仅集成 Telegram,去重是支付逻辑的职责。如有需要,建议用户单独添加去重逻辑。
阶段 0.5 — 检测客户数据存储(关键)
Sepay webhook 仅发送 content(客户转账时填写的备注)、transferAmount 和 referenceCode。Sepay 不知道客户是谁——必须根据 content(包含订单 ID)在本地存储中进行查找。
技能检查 3 项内容:
# 1. 检查客户数据存储辅助函数是否存在
test -f lib/leads-kv.ts || test -f lib/leads-kv.js && echo "lead store: ✓"
# 2. 检查 Vercel KV 环境变量
grep -E "KV_REST_API_URL|KV_REST_API_TOKEN" .env.local && echo "KV env: ✓"
# 3. 检查 @vercel/kv 是否已安装
grep '"@vercel/kv"' package.json && echo "KV package: ✓"
3 种场景:
| 场景 | 操作 |
|---|---|
| 3 项 ✓ — 客户数据存储已就绪 | 继续执行阶段 1,在 webhook 处理函数中使用 lib/leads-kv.ts 的 getLeadByOrderId() |
| 缺少 1+ 项 — 尚未设置 | 停止,向用户提供 2 个选项(见下文) |
如果缺少,询问用户:
我发现您还没有客户数据存储(Vercel KV)。Sepay webhook 不知道客户是谁——只有转账备注和金额。您希望:
选项 1 — 通过
/biz-setup-sepay-payment设置完整客户数据存储(推荐):
- 该技能将设置 Vercel KV、构建 lib/leads-kv.ts,并连接表单提交以在用户注册时保存客户信息
- 完成后回到这里,我将集成 Telegram,查找完整的客户姓名/电话/电子邮件
- Telegram 告警将显示:“Nguyễn Văn A — 0901234567 — [email protected] — 499K — AI Agent 课程”
选项 2 — 原始模式:直接发送 Sepay 负载到 Telegram,不查找客户:
- Telegram 仅显示:“Vietcombank — 499,000đ — 内容: DH001 0901234567 — 14:32”
- 所有者通过内容自行判断客户,并与 Resend 邮件日志交叉核对
- 简单,可立即运行,但用户体验较差
请选择 1 或 2。
如果用户选择 1 → 停止此技能,建议使用 /biz-setup-sepay-payment。用户完成设置后 → 再回到此技能。
如果用户选择 2 → 继续执行阶段 1,使用原始模板(参见 templates/lib-telegram-raw.ts)。
阶段 1 — 引导用户创建 Telegram 机器人
参考 references/telegram-bot-setup.md 获取带有文字截图的详细指南。向用户简要说明 3 个步骤:
步骤 1 — 通过 @BotFather 创建新机器人
1. 打开 Telegram(移动端/桌面端)
2. 搜索:@BotFather → 点击开始
3. 输入命令:/newbot
4. BotFather 询问机器人名称 — 输入显示名称(例如:“Brand X 订单通知”)
5. BotFather 询问用户名 — 输入以 "_bot" 结尾的用户名(例如:brandx_orders_bot)— 必须是全局唯一的
6. BotFather 返回一条消息,其中包含一行:
"Use this token to access the HTTP API: 1234567890:AAxxxxxxxxxxxxxxxxxxxxxx"
7. 复制该 token,粘贴给我。
安全注意事项:此 token 等于对机器人的完全控制权。不要提交到 Git,不要公开分享。如果不慎泄露 → 返回 @BotFather → /token → /revoke 以生成新 token。
步骤 2 — 获取 chat_id
一对一聊天(仅所有者接收) — 推荐用于个人创业者:
1. 在 Telegram 中,搜索:@userinfobot → 点击开始
2. 机器人返回一条消息,包含 "Id: 123456789" — 复制该数字。
3. 这就是您的 chat_id。
群组聊天(销售团队共同接收) — 推荐用于团队:
1. 创建一个新的 Telegram 群组(或使用现有群组)
2. 点击群组名称 → 添加成员 → 搜索刚创建的机器人(根据用户名 @brandx_orders_bot)→ 添加
3. 在群组中,输入:/start@brandx_orders_bot(或任何消息)
4. 打开浏览器,访问:
https://api.telegram.org/bot<TOKEN>/getUpdates
(将 <TOKEN> 替换为步骤 1 中的 token)
5. 在 JSON 响应中,找到 "chat":{"id":-1001234567890,...} — 复制该数字(如果包含负号则保留)
6. 这就是群组的 chat_id。
频道(单向广播,不讨论)— chat_id 以 -100 开头。获取方式与群组相同,但需将机器人添加为频道管理员。
步骤 3 — 提供给技能
等待用户提供:
TELEGRAM_BOT_TOKEN=1234567890:AAxxxxxxxxxxxxxxxxxxxxxx
TELEGRAM_CHAT_ID=123456789
重要关卡:在用户提供这两个值之前,不要进入阶段 2。如果用户说“我已经有了,从其他地方”——仍然要求提供,因为需要在阶段 3 中将其注入到 .env.local。
阶段 2 — 草拟消息格式
默认越南语格式,适合移动端显示(Telegram 渲染 HTML):
🎉 <b>新订单</b>
👤 <b>{{name}}</b>
📞 {{phone}}
📧 {{email}}
💰 <b>{{amount}}</b> — {{product_name}}
🕐 {{time_vn}}
🔗 交易码:<code>{{reference_code}}</code>
变量注入后效果:
🎉 新订单
👤 Nguyễn Văn A
📞 0901 234 567
📧 [email protected]
💰 499,000đ — AI Agent 课程
🕐 14:32 14/05/2026
🔗 交易码:FT26134567890
向用户显示预览,并询问:
- 此格式是否满意?是否需要添加/删除某些字段?(例如:添加 UTM 来源、当日订单数量、客户 Zalo 链接)
- 表情符号是否太多?(部分用户更喜欢纯文本)
- 是否希望添加一个内联按钮(CTA)链接到 CRM/Sheet?(高级功能——技能可以添加内联键盘)
格式规则:
- HTML 模式(
parse_mode: "HTML")— Telegram 支持<b>、<i>、<u>、<code>、<pre>、<a href>。不支持<br>— 使用\n。 - 越南盾金额格式:
499,000đ(千位分隔符用逗号,末尾加đ)或更美观的499K/1.99M。如果存在,从offer.json的voice设置中读取。 - 电话号码格式:
0901 234 567(3-3-3 分隔)— 便于在手机上快速浏览。 - 越南时间:
HH:mm dd/mm/yyyy— 设置为越南时间(UTC+7),而非 UTC。 - 避免过度使用表情符号(单条消息超过 5 个表情符号会降低专业感)。
阶段 3 — 集成代码
3 项修改:
3.1 创建辅助函数 lib/telegram.ts
参考 templates/lib-telegram.ts(TypeScript)或 templates/lib-telegram.js(vanilla JS)。内容:
// lib/telegram.ts
type TelegramPayload = {
name: string;
phone: string;
email: string;
amount: number; // VND, raw number (e.g., 499000)
productName: string;
referenceCode?: string;
};
export async function sendTelegramNotification(payload: TelegramPayload): Promise<void> {
const token = process.env.TELEGRAM_BOT_TOKEN;
const chatId = process.env.TELEGRAM_CHAT_ID;
if (!token || !chatId) {
console.error('[telegram] Missing TELEGRAM_BOT_TOKEN or TELEGRAM_CHAT_ID');
return; // 不抛出异常 — Telegram 失败不能阻止支付响应
}
const text = formatMessage(payload);
try {
const res = await fetch(`https://api.telegram.org/bot${token}/sendMessage`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
chat_id: chatId,
text,
parse_mode: 'HTML',
disable_web_page_preview: true,
}),
});
if (!res.ok) {
const errBody = await res.text();
console.error(`[telegram] Send failed: ${res.status} ${errBody}`);
}
} catch (err) {
console.error('[telegram] Network error:', err);
// 吞掉异常 — 不要让 Telegram 失败导致 Sepay 重试
}
}
function formatMessage(p: TelegramPayload): string {
const amountStr = p.amount.toLocaleString('vi-VN') + 'đ';
const phoneStr = p.phone.replace(/(\d{4})(\d{3})(\d{3})/, '$1 $2 $3');
const now = new Date().toLocaleString('vi-VN', {
timeZone: 'Asia/Ho_Chi_Minh',
day: '2-digit', month: '2-digit', year: 'numeric',
hour: '2-digit', minute: '2-digit',
});
return [
'🎉 <b>新订单</b>',
'',
`👤 <b>${escapeHtml(p.name)}</b>`,
`📞 ${phoneStr}`,
`📧 ${escapeHtml(p.email)}`,
`💰 <b>${amountStr}</b> — ${escapeHtml(p.productName)}`,
`🕐 ${now}`,
p.referenceCode ? `\n🔗 交易码:<code>${escapeHtml(p.referenceCode)}</code>` : '',
].filter(Boolean).join('\n');
}
function escapeHtml(s: string): string {
return s.replace(/[<>&]/g, c => ({ '<': '<', '>': '>', '&': '&' }[c]!));
}
为何吞掉异常:如果 Sepay 收到非 200 响应,它会重试 webhook。如果 Telegram 超时/宕机导致 webhook 崩溃 → Sepay 重试 → 每次重试都会再次发送邮件并记录支付 → 用户会为同一笔订单收到 5 封邮件和 5 条 Telegram 告警。Telegram 是 尽力而为的辅助通道,而非关键路径。
3.2 集成到 Sepay webhook 处理函数
读取现有处理函数,定位处理支付成功的代码段(在解析 Sepay 负载并验证 transferType === 'in' 之后)。添加:
// app/api/payment-success/route.ts (或类似路径)
import { sendTelegramNotification } from '@/lib/telegram';
import { sendCustomerEmail, sendOwnerEmail } from '@/lib/resend'; // 如果已有
export async function POST(req: Request) {
const payload = await req.json();
// [现有] 验证 Sepay 身份验证 + 解析负载
if (payload.transferType !== 'in') {
return Response.json({ success: true }); // 忽略转出交易
}
// [现有] 根据备注内容(包含订单 ID)或 referenceCode 查找客户
const lead = await lookupLeadByTransferContent(payload.content);
// [新] 并行展开 3 个副作用 — Telegram 失败不会阻塞邮件
await Promise.allSettled([
sendCustomerEmail(lead),
sendOwnerEmail(lead),
sendTelegramNotification({
name: lead.name,
phone: lead.phone,
email: lead.email,
amount: payload.transferAmount,
productName: lead.productName,
referenceCode: payload.referenceCode,
}),
]);
return Response.json({ success: true });
}
使用 Promise.allSettled 而非 Promise.all 的原因:
Promise.all在 任何 promise 失败时都会 reject → 处理函数抛出异常 → Sepay 收到 500 → 重试 → 重复邮件。Promise.allSettled会等待所有任务完成(resolve 或 reject),不会抛出异常。
3.3 更新环境变量和 .gitignore
添加到 .env.local:
# Telegram Bot Notification
TELEGRAM_BOT_TOKEN=<paste_token>
TELEGRAM_CHAT_ID=<paste_chat_id>
验证 .gitignore 包含 .env.local:
# 如果不存在,添加
".env.local" >> .gitignore
echo "
# 如果不存在,添加
.vercel" >> .gitignore
重要:不要在代码中硬编码 token/chat_id。如果 token 泄露:
- 任何人都可以代表您的机器人发送消息。
- 恶意行为者可以向您的聊天发送垃圾消息。
- 步骤:返回 @BotFather →
/token→/revoke→ 生成新 token → 更新.env.local。
阶段 4 — 测试计划
三级测试,从独立到集成:
级别 1 — 独立测试(curl)
在应用启动之前,在终端中直接测试 Telegram API:
# 语法:curl -X POST "https://api.telegram.org/bot<TOKEN>/sendMessage" -d "..."
TOKEN=1234567890:AAxxxxxxxxxxxxxxxxxx
CHAT_ID=123456789
curl -X POST "https://api.telegram.org/bot$TOKEN/sendMessage" \
-H "Content-Type: application/json" \
-d '{
"chat_id": "$CHAT_ID",
"text": "🧪 <b>测试通知</b>\n\n从终端发送\n📆 $(date +\"%d/%m/%Y %H:%M\")",
"parse_mode": "HTML"
}'
期望结果:您在 Telegram 聊天中收到一条带有终端时间戳的消息。如果您没有收到:
- 检查 token 是否正确(大小写敏感)
- 检查 chat_id 是否正确(如果是一对一聊天,必须是正数且从 @userinfobot 获取;如果是群组/频道,通常包含负号)
- 确保机器人是群组/频道的成员(对于群组和频道)
级别 2 — 集成测试(模拟 Sepay)
启动开发服务器,发送模拟的 Sepay 负载:
# 替换为您的 webhook URL
WEBHOOK_URL="http://localhost:3000/api/payment-success"
curl -X POST "$WEBHOOK_URL" \
-H "Content-Type: application/json" \
-d '{
"id": 99999,
"referenceCode": "TEST99999",
"transferType": "in",
"transferAmount": 499000,
"content": "DH001 0901234567",
"transactionDate": "14/05/2026 14:32:00",
"accountNumber": "1234567890"
}'
检查:
- webhook 返回
{ "success": true }(200) - 检查服务器日志:
[telegram] Sent successfully(或错误原因) - 检查 Telegram 聊天:您必须收到一条格式正确的消息,包含测试金额、虚拟姓名(如果找不到客户则显示“系统客户”)
如果 webhook 返回 500:检查服务器日志中是否有错误。常见原因:
- 环境变量未加载 → 重新启动 dev server
lib/telegram.ts有语法错误 → 检查导入和类型- 在查找客户阶段的
lead为 null → 原始模式应处理此情况
级别 3 — 生产环境验证
部署后,进行真实转账测试(最低 1,000 VND 或最小金额):
- 从您的银行账户向链接到 Sepay 的账户转账 1,000 VND
- 在转账备注中,输入正确的订单 ID 格式(例如:DH001)——确保与 webhook 中的
content格式匹配 - 等待 30-60 秒
- 检查 Telegram:您必须收到一条通知,包含:付款金额(1,000đ)、根据备注查找的客户信息(姓名、电话、电子邮件)、产品名称(如果 lookupLeadByTransferContent 成功)、交易码、时间戳
- 检查电子邮件:客户和所有者都应该收到邮件(如果集成正确的话)
- 检查 Sepay 仪表盘(https://sepay.vn):加载 webhook 日志 — 确保只有 1 个成功请求(200),没有重试(如果有重试,说明您的处理函数在某些分支返回了 500)
常见错误:
| 错误 | 原因 | 修复 |
|---|---|---|
| 收到多条 Telegram 消息(2-5 条) | Sepay 重试因为处理函数返回 500 | 检查日志,处理所有分支(Promise.allSettled 不会抛出异常) |
| 收到消息但金额为 0 | 负载中 transferAmount 为 null/undefined | 检查 webhook 字段名称(一些 Sepay 版本使用 amountIn) |
| 未收到消息,服务器日志显示“未找到 token” | .env.local 未在生产环境中部署 | 检查 Vercel 项目设置 → 环境变量 |
| 消息格式混乱(未解析的 HTML 标签) | Telegram API 不支持嵌套 HTML | 仅允许 <b>、<i>、<u>、<code>、<pre>、<a>,不要嵌套 |
安全与故障排除
Token 泄露场景
如果您怀疑有人访问了您的 token:
- 返回 @BotFather → 输入
/token→ 选择您的机器人 → 选择/revoke→ 确认 - BotFather 返回新 token
- 更新
.env.local中的TELEGRAM_BOT_TOKEN - 重新部署
Telegram API 速率限制
Telegram 对通过 sendMessage 发送的消息数量有限制(大约每个 chat_id 每秒 20 条消息)。在正常使用中(每笔订单 1 条消息),很难达到限制。但是,如果您在 1 秒内收到多笔订单(例如促销活动、系统通知),请注意:
- 发送后的响应不等待(fire-and-forget)
- 如果达到限制,Telegram 返回
429 Too Many Requests,并带有retry_after字段(秒数) - 当前实现会吞掉此错误,因此您不会收到消息,但支付流程不受影响
- 解决方案:添加一个基本队列(例如,使用
p-limitnpm 包),限制为每秒 10 条消息
Webhook 重试与去重
Sepay 的超时窗口约为 10 秒。如果您的处理函数运行时间超过 10 秒或返回 5xx,Sepay 将重试(最多 5 次,间隔递增:15 秒、60 秒、5 分钟、30 分钟、2 小时)。
这是最大的风险:每次重试都会发送通知 → 客户会收到 5 封邮件 + 5 条 Telegram 消息。
为什么当前设计是安全的:
Promise.allSettled不会抛出异常 → 处理函数始终返回 200(假设没有未捕获的错误)- Telegram 超时(fetch 默认 30 秒?)不会影响 webhook 响应
- 如果 Sepay 负载导致查找客户出错,封装的 try/catch 会捕获错误并返回 200,带有错误状态
建议:在 webhook 处理函数中添加一个简单的去重逻辑(基于 referenceCode + id):
// 在主处理函数顶部
const dedupKey = `payment:${payload.referenceCode}`;
const alreadyProcessed = await kv.get(dedupKey);
if (alreadyProcessed) {
console.log('[payment] Duplicate webhook, skipping:', payload.referenceCode);
return Response.json({ success: true });
}
await kv.set(dedupKey, '1', { ex: 86400 }); // 24 小时后过期
消息模板备注
越南通用字符
Telegram HTML 完全支持 Unicode,包括越南语字符(â、ê、ô、ơ、ư、ă、đ)。但是,在生产环境测试中,一些旧版 Telegram 客户端可能无法正确渲染某些字符。
安全做法:
- 为越南语文本使用 Unicode(NFC 标准化)
- 避免在 HTML 标签内使用组合字符
- 使用
utf8编码发送请求(库默认处理此问题)
电话号码格式化
formatMessage 中的正则表达式 (\d{4})(\d{3})(\d{3}) 设计用于标准的 10 位越南电话号码(例如:0901234567 → 0901 234 567)。
如果电话号码:
- 少于 10 位:不应用格式化,返回原始字符串
- 带有国家代码(例如:+84901234567):正则表达式将无法正确匹配 → 考虑添加辅助函数进行标准化
- 座机号码(例如:02812345678 — 11 位):格式错误 → 可以忽略或添加条件分支
越南盾金额格式化
toLocaleString('vi-VN') 返回的格式为 499.000(千位分隔符用点号,与美国格式不同)。我们添加了 đ 符号,因此最终字符串为 499.000đ。
如果需要更美观的格式(499K 代替 499.000đ),添加一个辅助函数:
function formatAmount(amount: number): string {
if (amount >= 1_000_000) return (amount / 1_000_000).toFixed(2) + 'M';
if (amount >= 1_000) return Math.round(amount / 1_000) + 'K';
return amount.toString();
}
项目结构
集成后,添加了以下文件(或根据现有模式调整):
project-root/
├── lib/
│ └── telegram.ts # [新] Telegram 通知辅助函数
├── app/api/payment-success/
│ └── route.ts # [修改] 添加了对 sendTelegramNotification 的调用
├── .env.local # [修改] 添加了 TELEGRAM_BOT_TOKEN + TELEGRAM_CHAT_ID
└── .gitignore # [验证] 确保 .env.local 被忽略
技能还可能会要求您创建 references/telegram-bot-setup.md,其中包含设置指南的文本版本(用于后续的 AI 参考)。
先决条件
您的项目必须满足:
- Next.js(App Router 或 Pages Router)已与 TypeScript 或 JavaScript 一起设置
- Sepay webhook 端点正在运行(例如
/api/sepay-webhook、/api/payment-success)——处理来自 Sepay 的 POST 请求 - Vercel KV(或类似的数据存储)用于根据转账备注查找客户(原始模式可以跳过)
- Resend(或其他邮件服务)用于面向客户/所有者的邮件通知(可选但推荐与 Telegram 并行使用)
注意:此技能不验证您是否已有 Resend,但建议从 biz-email-setup 开始,这样您就可以同时拥有邮件和 Telegram 通知。