跑在 Cloudflare Workers 上的虚拟商品自动发卡站。买家付款后自动秒发卡密,卖家零运维——没有服务器、没有数据库实例要管,小站每月成本可以是 0 元。
🔗 在线预览:https://shop.muyear.com —— 用本项目搭建的站点,可浏览前台实际效果。
支持支付宝(个人收款码 / 当面付 / 易支付)、USDT(TRC20 · Polygon · Arbitrum)、Binance Pay。
买家下单 → 支付 → 自动对账 → 秒发卡密 → 邮件送达
适合
- 卖虚拟商品(会员账号、激活码、软件授权、教程),需要 7×24 自动发货
- 不想租服务器、不想配 Nginx / MySQL / 备份
- 想收 USDT,或者只有个人收款码、没有商户号
不适合
- 需要实物物流、发票、复杂售后流程
- 需要高度定制的多商户平台
成本:Cloudflare 免费档就能跑起来;正式收款建议开 Workers 付费档($5/月)。域名另算(约 ¥70/年,也可以直接用免费的 *.workers.dev)。
买家侧
- 商品浏览、分类、多规格(月卡/季卡/年卡)、批发阶梯价
- 下单支付,付款后自动发货,卡密即时展示并发送到邮箱
- 凭「订单号 + 邮箱」随时查单取货,可另设查询密码
- 站内公告、服务条款
| 商品页与收银台 | 订单查询取货 |
|---|---|
卖家后台 /admin
| 模块 | 能力 |
|---|---|
| 仪表盘 | 销售额、订单数、库存预警 |
| 商品 | 分类、多规格、阶梯价、上下架、自动/人工发货 |
| 卡密 | 批量导入、库存统计、加密存储、按规格分库 |
| 订单 | 全状态查询、查看完整邮箱、已发货订单可复制卡密 |
| 支付渠道 | 逐个启用配置,密钥走 Secrets 不落库 |
| 公告 / 设置 | 站点文案、USDT 汇率、订单超时、维护模式 |
| 仪表盘 | 商品管理 |
|---|---|
| 卡密批量导入 | 订单管理(截图中邮箱已打码) |
后台已适配手机,出门在外也能查单补货。
优惠券的数据表与下单抵扣字段已就位,但后台管理页尚未实现,目前需直接操作数据库。欢迎 PR。
自动化
- 每分钟轮询链上到账、关闭超时订单
- 整点检查低库存并邮件告警
- 发货邮件通过 Resend 发送
点上面的 Deploy to Cloudflare 按钮,Cloudflare 会:
- 把本仓库 fork 到你的 GitHub
- 自动创建 D1 数据库、KV 命名空间、R2 存储桶,并回填配置
- 构建并部署到你的 Workers
部署完成后,还有 3 步必须手动做,否则站点不可用:
Cloudflare 会创建空数据库,但不会自动建表:
git clone https://github.com/<你的用户名>/fakawang.git
cd fakawang && npm install
npx wrangler login
npm run db:migrate:remote # 建表
npm run db:seed:remote # 可选:灌入演示商品,熟悉后可在后台删掉npx wrangler secret put CARD_ENC_KEY # 卡密加密主密钥
npx wrangler secret put SESSION_SECRET # 后台会话签名
npx wrangler secret put CRON_SECRET # 保护定时任务接口
npx wrangler secret put TURNSTILE_SECRET # 人机验证每个都用强随机值:
openssl rand -hex 32
⚠️ CARD_ENC_KEY丢失后所有卡密永久无法解密,务必离线备份。
NEXT_PUBLIC_SITE_URL 是构建期注入的,必须改完重新部署:
# 编辑 .env.production,把 NEXT_PUBLIC_SITE_URL 改成你的实际地址
# 例如 https://fakawang.你的子域.workers.dev
npm run deploy打开 https://你的地址/admin —— 首次登录输入的账号密码会自动注册为管理员,请立刻设强密码。
不想用按钮,或者想完全掌控每一步:
git clone https://github.com/certhon/fakawang.git
cd fakawang
npm install
npx wrangler login1. 创建资源
npx wrangler d1 create fakawang-db # 记下返回的 database_id
npx wrangler kv namespace create FAKA_KV # 记下返回的 id
npx wrangler r2 bucket create fakawang-assets # 可选,用于商品图2. 填进 wrangler.jsonc
3. 建表 + 配密钥 + 部署
npm run db:migrate:remote
npx wrangler secret put CARD_ENC_KEY
npx wrangler secret put SESSION_SECRET
npx wrangler secret put CRON_SECRET
npx wrangler secret put TURNSTILE_SECRET
# 改 .env.production 里的 NEXT_PUBLIC_SITE_URL
npm run deploy需要 Node.js 20+(推荐 22)。不需要任何真实支付账号。
npm install
cp .dev.vars.example .dev.vars # 本地密钥,已 gitignore
npm run db:migrate:local # 建本地表
npm run db:seed:local # 灌演示数据
npm run dev # http://localhost:3000开箱即用的两个便利:
- 内置 Turnstile 测试密钥,人机验证自动通过
- 内置 Mock 支付渠道,点一下「模拟支付成功」就能走完 下单 → 对账 → 发货 → 邮件 全链路
想在真实 Workers 运行时里联调支付回调:npm run preview(workerd 本地运行)。
| 类型 | 放哪 | 什么时候生效 | 举例 |
|---|---|---|---|
| 公开变量 | .env.production |
构建期注入,改完必须重新部署 | NEXT_PUBLIC_SITE_URLNEXT_PUBLIC_TURNSTILE_SITE_KEY |
| 密钥 | wrangler secret put |
运行时读取,改完即时生效 | CARD_ENC_KEY、各渠道密钥 |
| 业务配置 | 后台页面(存数据库) | 即时生效 | 站点名称、USDT 汇率、收款地址 |
最常见的坑:改了
NEXT_PUBLIC_SITE_URL却没重新部署,导致支付回调仍指向旧地址。
| 名称 | 必需 | 用途 |
|---|---|---|
CARD_ENC_KEY |
✅ | 卡密 AES-256-GCM 加密。丢失 = 卡密全废 |
SESSION_SECRET |
✅ | 后台会话签名 |
CRON_SECRET |
✅ | 保护定时任务接口 |
TURNSTILE_SECRET |
✅ | 人机验证服务端校验 |
RESEND_API_KEY |
发货邮件(不配则不发信) | |
MAIL_FROM |
发件人地址 | |
ALIPAY_PUSH_SECRET |
支付宝个人码上报签名 | |
EPAY_KEY |
易支付 | |
ALIPAY_PRIVATE_KEY ALIPAY_PUBLIC_KEY |
支付宝当面付 | |
BINANCE_API_KEY BINANCE_SECRET |
Binance Pay | |
TRONGRID_API_KEY |
USDT TRON 链查账 | |
ETHERSCAN_API_KEY |
USDT Polygon / Arbitrum 查账 |
完整说明见 .dev.vars.example。
不配的话默认用测试密钥,等于没有防护,会被脚本刷单。
- Cloudflare 控制台 → Turnstile → 新建 Widget
- Hostnames 填你的域名 ← 漏了会导致验证码静默失败、用户无法下单
- 「站点密钥」填进
.env.production的NEXT_PUBLIC_TURNSTILE_SITE_KEY - 「密钥」用
npx wrangler secret put TURNSTILE_SECRET写入 npm run deploy重新部署
前提:域名的 DNS 已托管在 Cloudflare。取消 wrangler.jsonc 末尾的注释:
"routes": [
{ "pattern": "shop.example.com", "custom_domain": true }
]部署时会自动创建 DNS 记录并签发证书。别忘了同步改 .env.production 的 NEXT_PUBLIC_SITE_URL,并把新域名加进 Turnstile 的 Hostnames。
后台 支付渠道 页逐个启用。敏感密钥走 wrangler secret,页面里只存非敏感配置。
| 渠道 | provider | 配置 JSON | 需要的 Secrets | 对账方式 |
|---|---|---|---|---|
| 模拟支付 | mock |
{} |
— | 手动点击(仅开发) |
| 易支付 | epay |
{"gateway":"https://pay.example.com","pid":"1000","type":"alipay"} |
EPAY_KEY |
Webhook |
| 支付宝当面付 | alipay_f2f |
{"appId":"2021..."} |
ALIPAY_PRIVATE_KEYALIPAY_PUBLIC_KEY |
Webhook |
| 支付宝个人码 | alipay_personal |
{"account":"main","qrContent":"https://qr.alipay.com/..."} |
ALIPAY_PUSH_SECRET |
安卓端推送 |
| USDT | usdt |
{"chains":[{"chain":"TRC20","address":"T...","enabled":true}]} |
TRONGRID_API_KEYETHERSCAN_API_KEY |
链上轮询 |
| Binance Pay | binance_pay |
{} |
BINANCE_API_KEYBINANCE_SECRET |
Webhook |
USDT 汇率在后台「系统设置」的 usdtRate 配置。
实现 PaymentProvider 接口,三个方法按需实现:
interface PaymentProvider {
code: ProviderCode;
createPayment(ctx, order, config): Promise<CreatePaymentResult>; // 建单
handleNotify?(ctx, req, config): Promise<{ notification, ack }>; // Webhook 型:验签
poll?(ctx, pendingOrders, config): Promise<VerifiedNotification[]>; // 轮询型:主动查账
}在 src/payments/registry.ts 注册即可。验签后返回统一的 VerifiedNotification,之后的幂等、金额校验、发货、发信由公共结算管线接管,新渠道不用重复实现。
没有商户号、只有个人收款码时可以用这套。
支付宝个人收款通知只有金额、没有订单号,所以用「唯一金额法」把钱和订单对上:
用户下单 ¥12.00
└→ 服务端占一个未被使用的金额位(¥12.00,被占了就试 ¥12.01、¥12.02…)
└→ 用户扫你的个人收款码,付这个精确金额
└→ 支付宝弹通知「支付宝成功收款12.00元」
└→ 安卓 App 捕获通知,HMAC 签名后上报
└→ 服务端按金额反查订单 → 结算发货
金额位由数据库部分唯一索引保证同一时刻不重复:
CREATE UNIQUE INDEX alipay_lock_amount_uq
ON alipay_amount_locks (account, amount_cents) WHERE released = 0;从原价开始逐分递增探测,所以绝大多数订单就是原价,只有同价订单并发时才 +1 分、+2 分。
需要一台安卓手机常驻登录支付宝,装一个通知监听 App。
📦 现成 APK 下载:Release · android-v1.0
最低 Android 8.0,已正式签名。装好后在 App 内填「站点地址 / 账号标识 / 共享密钥」,授予通知使用权并忽略电池优化(不做会被系统杀掉导致漏单),再点「连通性自检」验证即可。界面上会实时显示权限状态、配置完整性与待上报队列积压,出问题一眼能看出卡在哪。
也可以按下面的契约自己实现——协议很简单,一个 NotificationListenerService 加一次签名 POST。
接口契约:
POST /api/pay/notify/alipay_personal
{ "account": "main", "amountCents": 1200, "bizId": "<幂等键>",
"ts": 1785500000000, "sign": "<hex>", "rawText": "支付宝成功收款12.00元" }
签名 = HMAC-SHA256(ALIPAY_PUSH_SECRET, "account|amountCents|bizId|ts")
服务端会校验:ts 在 ±5 分钟内 → 常量时间比对签名 → account 匹配 → 按金额查活跃锁 → 结算。
实现要点(都是踩过的坑):
bizId必须由通知内容派生(如hash(通知时间+金额+文本)),不能用随机值——否则重推会被当成新的一笔- 到账先落本地持久化队列,上报成功才出队,断网才不会漏单
ts和签名必须在发送瞬间生成,队列里的旧记录不能沿用入队时间,否则超时间窗永远推不上去- 通知要按「收款/到账」白名单 +「退款/支出」黑名单过滤,别把付款当收入
本地联调:node scripts/alipay-push-test.mjs 12.00 可模拟一次上报。
- 合规:个人收款码用于经营性收款违反支付宝服务协议,存在限额、冻结、封号风险。
- 可靠性:链路依赖「手机不掉线、通知不被杀、网络通畅」。务必保留手动确认收款的兜底手段,并定期核对未结订单。
- 支付宝改版可能改通知措辞,届时需调整解析关键词与正则。
后台密码忘了?
删掉管理员记录,下次登录输入的账号密码会重新注册为管理员:
npx wrangler d1 execute DB --remote --command "DELETE FROM admins;"验证码一直转圈 / 点了没反应?
九成是 Turnstile 的 Hostnames 没加你的域名。去 Cloudflare → Turnstile → 对应 Widget → Hostnames 补上。
换了域名后支付跳到旧地址?
NEXT_PUBLIC_SITE_URL 是构建期注入的,改完 .env.production 必须重新 npm run deploy。
订单一直待支付,钱明明付了?
按渠道排查:
- USDT → 看定时任务是否在跑,转账金额是否完全一致(多付少付都不认)
- Webhook 渠道 → 看渠道后台的回调日志、验签是否通过
- 支付宝个人码 → 看安卓 App 的运行日志、待上报队列是否积压
也可以查 payment_events 表看事件到没到、result 是什么。
部署后打开是 500?
多半是数据库没建表。执行 npm run db:migrate:remote。
D1 读取行数涨得很快?
多半是缺索引导致全表扫描。免费档每天 500 万行读取,超了会被限流但不扣费。用 EXPLAIN QUERY PLAN 排查高频查询。
能不能不用 Cloudflare?
数据层深度依赖 D1/KV/R2 绑定,迁移需要改 src/db 与 src/lib/cf.ts,工作量不小。
二次开发前建议先读懂这几处。
单条 SQL 原子占用卡密,配合 D1 写入串行化,天然不会超卖:
UPDATE cards SET status = 'sold', order_id = ?, sold_at = ?
WHERE id IN (
SELECT id FROM cards
WHERE product_id = ? AND status = 'available' AND variant_id = ?
ORDER BY id LIMIT ?
) RETURNING id;RETURNING 行数即实际占用数;数量不足时按 order_id + sold_at 精确回滚本单,不误伤其它订单。
payment_events 表上 UNIQUE(provider, event_id) 作幂等门:重复回调、轮询与回调撞车,插入即冲突直接判重。订单状态用 CAS 推进(WHERE status = 'pending'),保证只有一次真正结算。
已过期订单收到付款会转 manual_review 人工复核,不会静默吞钱。
USDT 与支付宝个人码共用:单收款地址 + 唯一金额区分订单,靠部分唯一索引原子占位,结算或过期时释放。USDT 全程 BigInt 归一到链上最小单位,避免浮点误差。
AES-256-GCM 加密入库,密钥只存 Workers Secrets。查单需「订单号 + 邮箱」双因子,可另设查询密码。
src/
├── app/
│ ├── (store)/ 前台:首页 / 商品 / 收银台 / 订单查询 / 公告 / 条款
│ ├── admin/ 后台:登录 + 仪表盘 / 商品 · 分类 · 卡密 · 订单 · 渠道 · 公告 · 设置
│ └── api/ checkout / pay·notify·[provider] / orders / cron / health
├── db/ Drizzle schema 与实例
├── payments/
│ ├── providers/ mock · epay · alipay-f2f · alipay-personal · usdt · binance-pay
│ ├── watchers/ 链上到账抓取:tron-grid · evm-scan
│ ├── types.ts PaymentProvider 接口
│ └── registry.ts 驱动注册表
├── services/
│ ├── settlement.ts ★ 统一结算管线(幂等 → 校验 → CAS → 发货 → 发信)
│ ├── order.ts 下单、唯一金额分配
│ ├── delivery.ts 原子发货
│ ├── expiry.ts 订单过期与金额锁释放
│ └── cron.ts 定时任务入口
├── lib/ crypto / session / settings / turnstile / utils
└── components/ 前后台 UI 组件
worker/index.ts Worker 入口(fetch + Cron scheduled)
scripts/ seed 数据、支付宝上报联调脚本
drizzle/ 数据库迁移
技术栈:Next.js 16(App Router)+ React 19 + TypeScript · Cloudflare Workers(@opennextjs/cloudflare)· D1 + Drizzle ORM · Tailwind CSS v4 · Zod · Resend
设计文档:PLAN.md · PLAN-FRONTEND.md
CARD_ENC_KEY丢失 = 所有已入库卡密永久无法解密,请离线备份- 所有密钥用
wrangler secret管理,不要写进.env或提交到仓库 .dev.vars已在.gitignore中,别取消- 正式收款前确认 Turnstile 已在你的域名生效,否则会被脚本刷单
- 后台首次登录即注册管理员,部署后请立刻访问
/admin占位,别让别人抢先 - 定期导出订单与卡密数据做备份
本项目仅为技术实现,请自行确保经营内容与收款方式合法合规。使用个人收款码收款存在违反支付宝服务协议的风险,由使用者自行承担。
LINUX DO - 新的理想型社区