订单与支付
create-order、payment-notify、query-order 三个函数的完整支付流程、Hypay 集成与 confirm_payment 业务逻辑
本页面描述 VIP 购买的完整支付链路。三个 Edge Function 分别负责下单、回调确认和状态查询,共享 _shared/hypay.ts 签名模块与 Hypay 支付平台对接。
支付流程总览
┌──────────┐ ①创建订单 ┌──────────────┐ ②统一下单 ┌───────────┐
│ 客户端 │ ───────────────→ │ create-order │ ─────────────→ │ Hypay │
│ │ ←─────────────── │ │ ←───────────── │ 支付平台 │
└──────────┘ 返回 pay_url └──────────────┘ 返回支付链接 └───────────┘
│ │
│ ③用户跳转支付 │
│ ──────────────────────────────────────────────────────────→ │
│ │
│ ④异步回调 │
│ ┌────────────────┐ │
│ │ payment-notify │ ←──────────────────── │
│ └────────────────┘ │
│ │ │
│ ⑤confirm_payment RPC │
│ │ │
│ ┌──────┴──────┐ │
│ │ 更新订单 │ │
│ │ 续期 VIP │ │
│ │ 发放徽章 │ │
│ └─────────────┘ │
│ │
│ ⑥轮询订单状态 ┌──────────────┐ │
│ ───────────────→ │ query-order │ │
│ ←─────────────── │ │ ─── sync_hypay ──────→ │
│ 返回最新状态 └──────────────┘ (可选:兜底同步) │Hypay 集成(共享模块)
三个函数通过 _shared/hypay.ts 共享支付平台的签名与通信逻辑。
签名算法
Hypay V2 使用 RSA + SHA-256 签名方案:
构建待签名字符串:将所有参数(排除 sign 和 sign_type,排除空值)按 ASCII 字典序排序,以 key=value&key=value 格式拼接
请求签名:使用商户私钥(HYPAY_PRIVATE_KEY)对待签名字符串进行 RSASSA-PKCS1-v1_5 + SHA-256 签名,结果 Base64 编码
回调验签:使用平台公钥(HYPAY_PUBLIC_KEY)验证回调参数的签名,确认请求来自 Hypay
订单号生成
格式为 YYYYMMDDHHmmss + 6 位随机数,例如 20250615143052028491。由 generateTradeNo() 生成,作为 out_trade_no 传给 Hypay。
共享接口
Prop
Type
create-order
创建 VIP 购买订单并获取支付链接。
| 属性 | 值 |
|---|---|
| 方法 | POST |
| JWT 校验 | 是 |
| CORS | 支持(Access-Control-Allow-Origin: *) |
请求参数
Prop
Type
处理流程
身份验证:从 Authorization 头获取 JWT,调用 supabase.auth.getUser() 确认用户身份
查询商品:从 products 表读取商品信息,验证 is_active = true
库存检查:如果 stock_limit 不为 null,检查 sales_count 是否已达上限
计算价格:finalPrice × quantity,当前直接使用 product.price(预留促销逻辑)
创建商品快照:将下单时的商品信息(名称、价格、天数、类型)存入 product_snapshot(JSONB),确保后续即使商品信息变动,订单仍有据可查
生成订单号:调用 generateTradeNo() 生成唯一 trade_no
写入订单:向 orders 表插入一条 status = 'pending' 的记录,依赖 RLS 策略(用户只能创建自己的订单)
调用 Hypay 下单:将订单信息发送到 Hypay /api/pay/create,传入 notify_url(指向 payment-notify)和 return_url
处理 Hypay 响应:如果 code !== 0,将订单状态更新为 failed,返回 502;成功则将 hypay_trade_no 和 pay_url 写回订单
返回支付信息:将 pay_info(支付跳转 URL)返回给客户端
成功响应
{
"success": true,
"data": {
"order_id": "uuid",
"trade_no": "20250615143052028491",
"hypay_trade_no": "HP2025061500001",
"amount": "30.00",
"pay_type": "wxpay",
"pay_info": "https://hyzf.yuyuanai.top/pay/..."
}
}pay_info 的含义取决于请求时的 method 参数。当前固定为 jump(跳转模式),返回一个支付页面 URL,客户端直接跳转即可。
payment-notify
Hypay 支付成功后的异步回调端点。
| 属性 | 值 |
|---|---|
| 方法 | GET(Hypay 以 Query String 传参) |
| JWT 校验 | 否(verify_jwt: false) |
| 调用方 | Hypay 支付平台服务端 |
此函数不校验 JWT,通过 RSA 签名验证请求来源。PAYMENT_TEST_MODE 环境变量可跳过签名验证,生产环境务必设为 "false"。
回调参数
Hypay 以 URL Query String 方式传递以下参数:
Prop
Type
处理流程
参数校验:检查 trade_no、out_trade_no、trade_status 是否存在
签名验证:使用 Hypay 平台公钥验证回调签名(测试模式跳过)
状态检查:如果 trade_status !== 'TRADE_SUCCESS',直接返回 success(忽略非成功通知)
查询订单:使用 SERVICE_ROLE_KEY 从 orders 表查找对应订单
幂等性检查:如果订单 status 已经是 paid,直接返回 success,不重复处理
调用 confirm_payment:将 trade_no、hypay_no、real_amount 传给 RPC 函数执行核心业务逻辑
响应约定
- 返回 HTTP 200 + body
"success":Hypay 认为回调成功,停止重试 - 返回其他状态码或 body
"fail:...":Hypay 会重试回调
即使业务处理失败(如金额不匹配),函数仍返回 "success" + 200,避免 Hypay 无限重试。错误信息通过 console.error 记录到日志。
query-order
前端查询订单状态,支持主动同步 Hypay 以兜底回调丢失。
| 属性 | 值 |
|---|---|
| 方法 | POST |
| JWT 校验 | 是 |
| CORS | 支持 |
请求参数
Prop
Type
处理流程
查询本地订单(RLS 限制仅自己的订单)
→ status === 'paid'?
→ 是:直接返回(快速路径)
→ 否 & sync_hypay === true & status === 'pending'?
→ 查询 Hypay API
→ Hypay 显示已支付?
→ 调用 confirm_payment RPC(补偿回调丢失)
→ 返回更新后的订单 + synced: true
→ Hypay 未支付?
→ 返回本地订单 + hypay_status
→ 否:返回本地订单状态回调丢失兜底机制
当 sync_hypay = true 且本地订单仍为 pending 时,函数会主动调用 Hypay 查询接口。如果 Hypay 显示已支付但本地未更新,说明回调丢失,此时函数使用 SERVICE_ROLE_KEY 创建 admin 客户端,调用 confirm_payment RPC 完成支付确认。
sync_hypay 功能需要 SERVICE_ROLE_KEY,因为它要绕过 RLS 更新订单状态。此路径仅在 pending 状态下触发,已支付订单不会重复处理。
响应示例
{
"success": true,
"data": {
"id": "uuid",
"trade_no": "20250615143052028491",
"amount": "30.00",
"status": "paid",
"paid_at": "2025-06-15T14:32:00Z",
"product_snapshot": { "..." : "..." },
"hypay_status": 1
}
}{
"success": true,
"data": {
"id": "uuid",
"trade_no": "20250615143052028491",
"amount": "30.00",
"status": "pending",
"pay_url": "https://hyzf.yuyuanai.top/pay/..."
}
}{
"success": true,
"data": {
"id": "uuid",
"trade_no": "20250615143052028491",
"amount": "30.00",
"status": "paid",
"paid_at": "2025-06-15T14:35:00Z",
"hypay_status": 1,
"synced": true
}
}synced: true 表示此次状态更新是通过主动同步而非回调完成的。
confirm_payment RPC
支付确认的核心业务逻辑,由 payment-notify 和 query-order(兜底同步)调用。这是一个 PostgreSQL 函数,在数据库层面保证事务一致性。
参数
Prop
Type
执行逻辑
锁定订单:SELECT ... FOR UPDATE 防止并发重复确认
基础检查:订单是否存在、是否已支付(幂等)、金额是否匹配
读取快照:从 product_snapshot 中提取 days、type、quantity
更新订单:status → paid,写入 hypay_trade_no 和 paid_at
更新商品销量:products.sales_count += quantity
按商品类型分支处理(见下方)
商品类型处理
计算 总天数 = duration_days × quantity,更新 profiles.vip_expiration_date:
- 当前 VIP 未过期:在现有到期时间上追加天数
- 当前 VIP 已过期:从当前时间起算追加天数
vip_expiration_date =
CASE
WHEN vip_expiration_date > now()
THEN vip_expiration_date + total_days
ELSE
now() + total_days
END捐赠商品不按天数计算,而是根据累计捐赠金额授予永久 VIP:
| 累计捐赠 | VIP 到期时间 | 含义 |
|---|---|---|
| 不足 50 元 | 2055-12-31 | 基础永久 VIP |
| 达到 50 元 | 2155-12-31 | 高级永久 VIP |
保护逻辑:如果用户已是高级永久 VIP(到期时间 ≥ 2100 年),不会因后续小额捐赠被降级为基础永久。
额外操作:
- 更新
profiles.total_donated_amount - 自动发放捐赠徽章(
badge_id = 4),使用INSERT ... WHERE NOT EXISTS防止重复
捐赠通道已经于 2026-03-15 正式结束业务,代码逻辑依然保持,为之后的再开做准备。
返回值
// 订阅
{ "success": true, "type": "subscription", "days_added": 30, "quantity": 1 }
// 捐赠
{ "success": true, "type": "donation", "donated_amount": 30, "total_donated": 80 }
// 失败
{ "success": false, "msg": "Amount mismatch" }相关数据表
Prop
Type
order_status 枚举:pending(待支付)→ paid(已支付)→ refunded(已退款);下单失败为 failed。
Prop
Type
Secrets 依赖
| Secret | 用于 | 说明 |
|---|---|---|
HYPAY_PID | 全部三个函数 | Hypay 商户 ID |
HYPAY_PRIVATE_KEY | create-order, query-order | 商户 RSA 私钥,用于请求签名 |
HYPAY_PUBLIC_KEY | payment-notify | Hypay 平台 RSA 公钥,用于回调验签 |
PAYMENT_TEST_MODE | payment-notify | 测试模式开关,"false" 为生产模式(启用验签) |
SUPABASE_URL | 全部 | 系统自带 |
SUPABASE_ANON_KEY | create-order, query-order | 系统自带,配合用户 JWT 使用 |
SUPABASE_SERVICE_ROLE_KEY | payment-notify, query-order | 回调处理和兜底同步需要绕过 RLS |
安全注意事项
- 金额校验:
confirm_payment会比对orders.amount与实际支付金额p_real_amount,不匹配时拒绝确认并记录 CRITICAL 日志 - 幂等性:订单级别
FOR UPDATE行锁 + 状态检查,确保同一订单不会被重复确认 - 回调安全:
payment-notify不校验 JWT,完全依赖 RSA 签名验证。生产环境必须确保PAYMENT_TEST_MODE为"false" - 商品快照:下单时冻结商品信息到
product_snapshot,后续确认支付时从快照读取天数和类型,不受商品实时变更影响 - RLS 保护:
create-order和query-order使用ANON_KEY+ 用户 JWT,受 RLS 约束(用户只能操作自己的订单);payment-notify使用SERVICE_ROLE_KEY绕过 RLS