# 使用集成接入支付宝支付｜Creght

> 在 Creght 用支付宝集成收款：ctx.payment.alipay.pageUrl 发起电脑网站支付，verifyNotify 验签异步通知，call 调用查单与退款，多个收款账号用渠道 tag 区分。

[![Creght](https://ugc.talizen.com/_assets/site/2061660904709165056/1780797461299__creght_logo.png)API for AI](/)

[查看 llms.txt](/llms.txt)

概览

- [Creght AI 编程指南](/api.md)

AI 可发现性

- [如何优化 llms.txt](/api/optimize-llms-txt.md)

站点配置

- [实现基于域名的多语言路由](/api/domain-locale-routing.md)

后端

- [在服务端调用外部 API 并管理缓存](/api/ssr-external-api-cache.md)
- [使用 Func 构建站点后端能力](/api/func-backend.md)
- [JSON 表：定义、读写与查询](/api/func-json-tables.md)
- [上传文件：直传与 Func 内生成](/api/func-assets-upload.md)
- [超时配置与流式响应](/api/func-timeout-streaming.md)
- [使用 Func 接入支付宝电脑网站支付](/api/func-alipay-payment.md)

集成

- [使用集成发送邮件与验证码](/api/func-email-integration.md)
- [使用集成接入支付宝支付](/api/func-alipay-integration.md)

登录与用户

- [注册时验证邮箱](/api/auth-verified-registration.md)
- [实现找回密码与修改密码](/api/auth-password-reset.md)
- [在 Func 里实现登录](/api/auth-func-login.md)
- [在 Func 里查询用户](/api/func-user-directory.md)

本页目录

- [payment 是命名空间，不是统一接口](#namespace)
- [连接支付宝](#connect)
- [保存即校验做了什么](#verify)
- [订单表](#order-table)
- [发起支付](#create)
- [接收异步通知](#notify)
- [查单、退款与其他接口](#call)
- [一个项目挂多个收款账号](#channels)
- [平台不管的部分](#yours)
- [边界](#errors)

集成/使用集成接入支付宝支付

# 使用集成接入支付宝支付

连接支付宝集成后在 Func 里用 ctx.payment.alipay 发起电脑网站支付并验签异步通知：应用私钥留在服务端，签名、通知验签与内容加密由平台完成，订单、金额核对与幂等发货仍由站点代码负责。

复制 Markdown 链接

支付宝支付是托管集成：在编辑器里连接一次，之后在 Func 里用 `ctx.payment.alipay` 发起电脑网站支付、验签异步通知。应用私钥与 AES 密钥只保存在服务端，签名、验签和内容加密都由平台完成，代码里不会出现任何密钥。

**本文范围**

支付宝 **电脑网站支付**（ `alipay.trade.page.pay`）的公钥模式。手机网站支付、App 支付、当面付和证书模式还不支持；那些场景可以自己在 Func 里实现，见 [使用 Func 接入支付宝电脑网站支付](/api/func-alipay-payment.md)。

## payment 是命名空间，不是统一接口

方法名按支付宝自己的产品形状取， **刻意不做跨平台的统一支付接口**：各家支付的参数结构和回调语义差得很远（Stripe 是 Checkout Session、支付宝是表单签名跳转），硬抽一个 `createCheckout()` 只会变成一个更难懂的转发器。将来接别家是新增 `ctx.payment.<provider>`，已有代码不受影响。

分工是：平台负责 **密码学与凭据**，站点代码负责 **钱和货**。

| 平台保证 | 仍然由你写 |
| --- | --- |
| RSA2 签名，以及对表单原文的异步通知验签 | 金额来自服务端商品表 |
| 核对 `app_id` / `seller_id` 属于本渠道 | 订单表与订单状态 |
| 可选的 AES 内容加密 | 幂等发货 |
| OpenAPI 响应按响应原文验签 | 与本地订单核对金额 |
| 保存凭据时真实校验一次 | — |

## 连接支付宝

先在支付宝开放平台取参数：

1. 进入收款用的应用，在「应用详情」记下 `APPID`。
2. 打开「开发设置 → 接口加签方式」，用 [支付宝开发者工具](https://open.alipay.com/tool) 生成 RSA2 应用密钥对： **应用私钥自己保管**，只把应用公钥上传给支付宝；上传后在同一处复制支付宝生成的 **支付宝公钥**。
3. 记下实际收款账号的支付宝用户 ID（PID），它要与通知里的 `seller_id` 一致。
4. 如果开了「接口内容加密方式」，复制那个 Base64 的 AES 密钥；没开就不用管。

然后打开编辑器的 **后端 → 集成**，点 **支付宝**，按下表填写。保存时平台会真的调一次支付宝，配置不对当场报错，而不是等到有人付款才发现。

| 字段 | 填什么 | 必填 |
| --- | --- | --- |
| APPID | 应用详情里的 APPID | 是 |
| 应用私钥 | 你自己生成并保管的应用私钥， **不是** 应用公钥。PKCS#8 / PKCS#1 都行，带不带 PEM 头都行 | 是 |
| 支付宝公钥 | 上传应用公钥后 **由支付宝提供** 的那一串 | 是 |
| 收款账号 PID | 实际收款账号的支付宝用户 ID | 是 |
| 异步通知地址 | 已发布的 https Func 地址，例如 `https://example.com/func/alipay.notify` | 是 |
| 支付后返回地址 | 付款后跳回的页面，只影响体验 | 否 |
| AES 密钥 | 「接口内容加密方式」里的 Base64 字符串 | 开了内容加密才填 |
| 网关地址 | 留空走正式环境；沙箱填对应的沙箱网关 | 沙箱填 |

**最容易填错的是支付宝公钥**

「应用公钥」是你上传给支付宝的，「支付宝公钥」是支付宝给你的，两者不能互换。填错时保存会在「响应验签」这一步失败。

## 保存即校验做了什么

保存时平台用 `alipay.trade.query` 查一个随机订单号。这一次调用同时验四件事： `APPID` 存在且开通了接口；应用私钥与你上传的应用公钥是 **同一对**（签名被支付宝接受）；支付宝公钥正确（能验证支付宝的响应签名）；网关与 APPID 配套（正式 / 沙箱）。填了 AES 密钥时连它一起验。

**「查不到这笔交易」是成功信号**——查的本来就是一个不存在的订单号，能收到这个业务错误恰好证明请求过了签名和权限校验。

| 报错 | 该改哪一项 |
| --- | --- |
| `isv.invalid-signature` | 应用私钥与上传给支付宝的应用公钥不是一对 |
| `isv.invalid-app-id` | APPID 不对，或沙箱 APPID 配了正式网关 |
| 响应验签失败 | 支付宝公钥填错了（常见：填成了自己的应用公钥） |
| `isv.decrypt-error` | AES 密钥不对 |
| `isv.insufficient-isv-permissions` | 这个应用没签约对应产品，去开放平台的「产品签约」开通 |

## 订单表

支付状态存在你自己的表里。创建 `/platform/table/payment_orders.json`：

```json
{
  "name": "Payment orders",
  "desc": "Alipay order state",
  "json_schema": {
    "type": "object",
    "properties": {
      "userId": { "type": "string" },
      "productId": { "type": "string" },
      "amount": { "type": "string" },
      "outTradeNo": { "type": "string" },
      "status": { "type": "string", "enum": ["pending", "paid", "closed"] },
      "alipayTradeNo": { "type": "string" },
      "paidAt": { "type": "string" }
    },
    "required": ["userId", "productId", "amount", "outTradeNo", "status"]
  }
}
```

## 发起支付

创建 `/backend/func/alipay.ts`：

```typescript
import type { TalizenFuncContext } from 'talizen/func-runtime'

// 金额由服务端商品表决定，浏览器只传 productId。
const PRODUCTS = {
  starter: { subject: 'Starter plan', amount: '9.90' },
} as const

export function create(input: { productId?: string }, ctx: TalizenFuncContext) {
  const user = ctx.auth.requireUser()
  const productId = String(input.productId || '') as keyof typeof PRODUCTS
  const product = PRODUCTS[productId]
  if (!product) throw new Error('invalid product')

  // 订单号自己生成并先落库：它同时是订单表主键和异步通知的幂等键。
  const outTradeNo = 'C' + crypto.randomUUID().replace(/-/g, '')
  ctx.db.insert('payment_orders', {
    userId: String(user.id),
    productId,
    amount: product.amount,
    outTradeNo,
    status: 'pending',
  })

  const { payUrl } = ctx.payment.alipay.pageUrl({
    outTradeNo,
    subject: product.subject,
    amount: product.amount,
  })
  return { outTradeNo, payUrl }
}
```

`pageUrl` 返回 `{ payUrl, outTradeNo, amount, appId }`，浏览器拿到 `payUrl` 直接跳转：

```typescript
import { invoke } from 'talizen/func'

const { payUrl } = await invoke<{ payUrl: string }>('alipay.create', {
  productId: 'starter',
})
window.location.assign(payUrl)
```

| 参数 | 说明 |
| --- | --- |
| `outTradeNo` | 必填，商户订单号，64 位以内可见 ASCII。 **平台不代生成**：它是你订单表的主键和幂等键，必须先有订单行再跳转 |
| `subject` | 必填，商品标题 |
| `amount` | 必填，单位元、最多两位小数（ `9.9` 会归一成 `9.90`） |
| `body` | 可选，商品描述 |
| `returnUrl` | 可选，覆盖集成里配的返回地址（只影响页面体验） |

刻意 **不提供** `notifyUrl` 覆盖：支付事实只能从一个地方进来，异步通知地址在集成里配一次。

## 接收异步通知

集成里配的异步通知地址就指向这个方法—— `https://example.com/func/alipay.notify` 对应 `/backend/func/alipay.ts` 的 `notify`：

```typescript
export async function notify(_input: unknown, ctx: TalizenFuncContext) {
  // 验签失败会抛错，伪造的通知走不到下面任何一行。
  // 不要 catch 成 success——返回非 success 时支付宝会重投。
  const n = ctx.payment.alipay.verifyNotify(await ctx.request.text())
  if (!n.paid) return new Response('success') // 关单等其他状态

  const { list } = ctx.db.query('payment_orders', {
    where: { outTradeNo: n.outTradeNo },
    limit: 1,
  })
  const order = list[0]
  if (!order) return new Response('failure', { status: 400 })
  if (order.amount !== n.totalAmount) return new Response('failure', { status: 400 })
  if (order.status === 'paid') return new Response('success') // 重复通知

  ctx.db.update('payment_orders', order.id, {
    status: 'paid',
    alipayTradeNo: n.tradeNo,
    paidAt: new Date().toISOString(),
  })
  // 发放权益同样要以 outTradeNo / tradeNo 做幂等保护。
  return new Response('success')
}
```

`verifyNotify` 收的必须是 **原始表单正文**（ `await ctx.request.text()`）。不要自己解析再拼回去——签名覆盖的是原文，重拼一次就验不过。

平台在这一步完成：RSA2 验签、 `app_id` 与 `seller_id` 必须属于这个渠道、必填字段检查。 **任何一步不通过都直接抛错**，而不是返回一个可能被当成 falsy 忽略的值。

| 返回字段 | 说明 |
| --- | --- |
| `paid` | `TRADE_SUCCESS` 与 `TRADE_FINISHED` 都算已到账，直接用它，别自己判字符串 |
| `outTradeNo` / `tradeNo` | 你的订单号 / 支付宝交易号 |
| `totalAmount` | 实付金额，必须与自己订单表里的金额核对 |
| `tradeStatus` | 原始交易状态 |
| `notifyId` | 通知 id，可用来去重 |
| `buyerId` / `gmtPayment` / `subject` | 买家 ID、付款时间、标题 |
| `params` | 全部原始参数，读 `passback_params` 之类字段用它 |

**success 必须由你返回，幂等也必须由你保证**

Func 要返回支付宝要求的纯文本 `success`。返回别的内容（包括报错）会让支付宝按策略重投，所以同一个 `outTradeNo` 第二次进来时，直接返回 `success`。

## 查单、退款与其他接口

其余 OpenAPI 方法用 `call`，平台代签并对响应原文验签：

```typescript
// 查单：对账，或用户回到页面时主动确认一次
const r = ctx.payment.alipay.call('alipay.trade.query', { out_trade_no: 'C0001' })
if (r.trade_status === 'TRADE_SUCCESS') { /* ... */ }

// 退款
ctx.payment.alipay.call('alipay.trade.refund', {
  out_trade_no: 'C0001',
  refund_amount: '9.90',
  out_request_no: 'R0001', // 退款请求号，重复退款靠它幂等
})
```

返回的是验签通过后的业务响应节点（ `alipay_xxx_response` 里的内容）；业务码不是 `10000` 时抛错。 `alipay.trade.page.pay` 不能走 `call`——它是跳转流程，用 `pageUrl`。

## 一个项目挂多个收款账号

加两份支付宝集成，给它们不同的 **渠道 tag**，Func 里用 `via()` 选：

```typescript
ctx.payment.alipay.pageUrl({ ... })                   // 默认渠道，等价于 via('default')
ctx.payment.alipay.via('overseas').pageUrl({ ... })   // 另一个收款账号
ctx.payment.alipay.via('overseas').verifyNotify(raw)  // 通知要用同一个渠道
```

**与 ctx.email 有一处刻意的不同**

同一个 tag 命中多份支付集成会 **直接报错**，不会像发信那样随机挑一份。支付是一条跨请求的链——用 A 账号签的单，异步通知只会带着 A 的 `app_id` 回来；随机挑的后果是「用户付了钱，站点永远收不到通知」，这种问题没有任何本地症状。

所以两个收款账号就是两个 tag，并且发起支付和处理通知要用 **同一个** tag。

## 平台不管的部分

这几条是支付里最容易出事的地方，平台帮不了你：

- **金额由服务端商品表决定**，浏览器只传 `productId`。让浏览器传金额等于让人自己定价。
- **支付事实只来自验签通过的异步通知。** `returnUrl` 只是付款后跳回的页面，用户可以直接访问它，不能据此标记已支付。
- **验签通过后仍要核对本地订单和金额**，再看 `tradeNo` 是否与已记录的一致。
- **发放权益要幂等**，以 `outTradeNo` / `tradeNo` 为键——通知会重投。

## 边界

- 只支持电脑网站支付的公钥模式；手机网站支付、App 支付、当面付、证书模式还没做。
- 支付集成 **不能** 打开「把密钥暴露给 Func 代码」：收款私钥不进沙箱，由平台代签。想完全自己接就走另一条路——在 **后端 → 环境变量** 里自己配 `ALIPAY_*`，用 `fetch` \+ `crypto.subtle` 写自己的 Func，两条路互不干扰，做法见 [使用 Func 接入支付宝电脑网站支付](/api/func-alipay-payment.md)。
- 沙箱与正式环境用各自的应用、密钥和网关。 **上线前必须在正式环境真实收到过一笔通知**，这一步没有替代品。
- 异步通知地址必须是 **已发布** 的 https 地址：预览域名下的 Func 收不到线上通知。

**Func 基础能力**

文件、调用、数据表、鉴权、资源上传、超时和 SSE 见 [Func 后端完整指南](/api/func-backend.md)；邮件与验证码见 [使用集成发送邮件与验证码](/api/func-email-integration.md)。

![Creght](https://ugc.talizen.com/_assets/site/2061660904709165056/1780797461299__creght_logo.png)

此网站使用 [Creght](/) 创建

![微信客服](https://fsu.creght.com/site/2066727200882692096/1785119134612__image.png)

微信客服

## 链接

- [价格](/price.md)
- [解决方案](/solution.md)
- [客户案例](/customers.md)
- [帮助中心](/help.md)
- [联系我们](/contact.md)
- [更新记录 & 博客](/blogs.md)
- [退款说明](/tuikuan.md)

## 资源

- [全部资源](/resources.md)
- [模板](/templates.md)
- [组件库](https://creghtlib.site.creght.com)
- [动效库](/design/effects.md)
- [Figma to Creght](/figma2creght.md)
- [API](/api.md)

## 产品对比

- [对比上线了](/creght-vs-sxl.md)
- [对比凡科建站](/creght-vs-fkw.md)
- [自己写代码 vs Creght](/compare/self-coding.md)
- [外包 vs 自己做](/compare/outsourcing.md)

## 协议

- [用户协议](/legal/terms.md)
- [隐私政策](/legal/privacy.md)
- [可接受使用政策](/legal/acceptable-use.md)

## 社交媒体

- [小红书](https://www.xiaohongshu.com/user/profile/5a38606811be10715f4895b6)
- [哔哩哔哩](https://space.bilibili.com/513308095)

[蜀ICP备2023038192号-2](https://beian.miit.gov.cn)
