# 预下单 data 与 dbAuthorization

本文说明业务服务端如何为智能服务前端 `requestOrder` 生成 `data` 和 `dbAuthorization`。这两个参数必须由服务端基于可信订单生成；前端只负责原样透传，不负责计算金额、拼装订单或持有开发者私钥。

`requestOrder` 是前端支付 API，不是开发者直接调用的服务端 OpenAPI。本文中的 `POST /requestOrder` 是签名时使用的 method 和签名上下文路径，不是要由业务服务端发送的 HTTP Endpoint。

完整的证书申请、通用待签名串和回调验签规则读取 [签名认证及加密传输](../security/signature-authentication-and-encryption.md)。

## 参数与其它鉴权信息的区别

| 名称 | 出现位置 | 作用 |
| --- | --- | --- |
| `data` | 前端 `requestOrder` 入参 | 预下单业务数据序列化后的 JSON 字符串 |
| `dbAuthorization` | 前端 `requestOrder` 入参 | 与本次 `data` 绑定的签名授权串 |
| `db_authorization` | 业务服务端与前端之间自定义 HTTP 契约中常见的字段名 | `dbAuthorization` 的 snake_case 传输命名；前端适配后传给 SDK |
| `X-DB-Authorization` | 平台 HTTP 请求或回调 Header | 通用签名认证 Header；回调时由平台生成、开发者验签 |
| `x-DB-AccessToken` | 订单查询、退款和履约等服务端 OpenAPI Header | 应用级调用凭证，不替代 `dbAuthorization` 或回调签名 |

OpenAPI 响应外层的 `data` 对象、敏感信息加密流程中的 `cipher_data`，都不是本文的预下单 `data`。

## data 结构

`requestOrder.data` 是以下对象序列化后的 JSON 字符串。下表以当前 `flow/developer_open_trade` 的运行时校验为准，不直接照抄 Thrift 的 `required` 标记：嵌在字符串中的 JSON 会先反序列化，再由服务端执行字段校验和默认值填充。

### RequestOrderData

| 字段 | 类型 | 当前服务端要求 | 说明 |
| --- | --- | --- | --- |
| `skuList` | `SkuItem[]` | 可省略或为空 | 非空时逐项校验，且所有商品项 `totalAmount` 之和必须等于订单 `totalAmount`；为空时服务端生成一条默认商品明细 |
| `outOrderNo` | `string` | 必须非空，最多 64 个字符 | 开发者业务订单号；同一应用内已存在时创建订单会失败 |
| `totalAmount` | `integer` | 不得小于 0 | 订单总金额，使用最小货币单位；当前格式校验允许 0，不能据此推断下游支持零元支付 |
| `orderName` | `string` | 必须非空 | 订单名称；`skuList` 为空时也用作默认商品标题 |
| `orderDesc` | `string` | 必须非空 | 订单描述 |
| `currency` | `string` | 可省略或传空字符串 | 省略或为空时服务端填充 `CNY`；当前只接受 `CNY` |
| `payExpireSeconds` | `integer` | 可省略或传 `0` | 省略或为 `0` 时服务端填充 300 秒；其它值必须大于 0 且不超过 172800 秒（48 小时） |
| `payNotifyUrl` | `string` | 可省略或传空字符串 | 非空时必须通过服务端 URL 合法性校验；需要异步支付结果闭环时应提供当前环境可达地址 |
| `merchantUid` | `string` | 必须非空，最多 64 个字符 | 收款商户 UID；目标应用必须已绑定该商户，不能伪造 |

普通正式调用会校验应用与 `merchantUid` 的绑定关系。部分 sandbox 上下文会由平台显式跳过绑定检查，这只用于沙箱联调，不能据此省略正式商户配置。

### SkuItem

`skuList` 非空时，每个元素都必须满足以下规则：

| 字段 | 类型 | 当前服务端要求 | 说明 |
| --- | --- | --- | --- |
| `skuId` | `string` | 必须非空，最多 64 个字符 | 业务商品标识 |
| `price` | `integer` | 必须大于 0 | 商品单价，使用最小货币单位 |
| `quantity` | `integer` | 必须大于 0 | 数量 |
| `title` | `string` | 必须非空，最多 256 个字符 | 商品名称 |
| `totalAmount` | `integer` | 必须等于 `price * quantity` | 该商品项总金额，使用最小货币单位 |

当 `skuList` 为空时，当前服务端会生成一条默认商品：商品 ID 为内部默认值，标题取 `orderName`，单价和小计取订单 `totalAmount`，数量为 1。该兼容行为不改变信任边界：服务端仍要从可信业务数据计算订单金额，不接受前端或 MCP Tool 提交的金额作为最终扣款依据。

完整示例：

```json
{
  "skuList": [
    {
      "skuId": "sku-1",
      "price": 100,
      "quantity": 2,
      "title": "示例商品",
      "totalAmount": 200
    }
  ],
  "outOrderNo": "order-20260714-0001",
  "totalAmount": 200,
  "orderName": "示例订单",
  "orderDesc": "示例商品 x2",
  "currency": "CNY",
  "payExpireSeconds": 300,
  "payNotifyUrl": "https://example.com/callback/payment",
  "merchantUid": "merchant-uid"
}
```

示例值只说明结构。真实订单号、金额、回调地址和商户 UID 必须来自当前环境的业务配置。

## 服务端生成流程

1. 按业务订单号读取已持久化的订单、商品快照和当前状态。
2. 从可信商品、数量、优惠和订单规则重新确认金额及商户信息。
3. 构造 `RequestOrderData`，按上表应用非空、长度、默认值、商户绑定和金额一致性规则；不要仅依赖 Thrift 的 `required` 标记。
4. 将对象一次性序列化为 JSON，保留这份最终 UTF-8 字节作为 `dataBytes`。
5. 使用以下签名上下文生成授权串：

   ```text
   HTTP_METHOD = POST
   REQUEST_PATH = /requestOrder
   CANONICAL_QUERY = 空字符串
   BODY = dataBytes
   ```

6. 生成秒级 timestamp 和随机 nonce，按通用签名规则构造待签名串。
7. 使用开发者 ECC P-256 私钥生成 ASN.1 DER 签名，再按 [通用签名规范](../security/signature-authentication-and-encryption.md#ecdsa-der-与-signature-编码) 使用保留 `=` padding 的 URL-safe Base64 编码；不得使用固定长度 `r || s` 或 Raw Base64URL。
8. 使用开发者证书序列号组装单行授权信息，不插入额外空格、换行或折行：

   ```text
   algorithm="ECDSA_SHA256",version="1",signature="...",cert_serial="...",nonce="...",timestamp="..."
   ```

9. 业务服务端向前端返回同一份 `data` 字符串和生成的 `dbAuthorization`。
10. 前端将二者原样传给当前 SDK 的 `requestOrder`，成功取得 `orderId` 后再调用 `getOrderPayment`。

服务端返回示例：

```json
{
  "data": "{\"skuList\":[{\"skuId\":\"sku-1\",\"price\":100,\"quantity\":2,\"title\":\"示例商品\",\"totalAmount\":200}],\"outOrderNo\":\"order-20260714-0001\",\"totalAmount\":200,\"orderName\":\"示例订单\",\"orderDesc\":\"示例商品 x2\",\"currency\":\"CNY\",\"payExpireSeconds\":300,\"payNotifyUrl\":\"https://example.com/callback/payment\",\"merchantUid\":\"merchant-uid\"}",
  "dbAuthorization": "algorithm=\"ECDSA_SHA256\",version=\"1\",signature=\"...\",cert_serial=\"...\",nonce=\"...\",timestamp=\"...\""
}
```

业务接口可以使用 `db_authorization` 作为 JSON 字段名，但必须在前端明确映射到 SDK 的 `dbAuthorization`；不要同时生成两份不同的授权串。Signature 末尾是否出现一个或两个 `=` 由 DER 长度决定，前端必须原样透传，不能删除 padding。

## 必须保持的字节一致性

签名保护的是请求语义及 Body 字节。参与 `BODY_SHA256_HEX` 计算的 `dataBytes` 必须和前端最终传给 `requestOrder` 的 `data` 完全一致。

前端不得对 `data` 执行以下操作：

- `JSON.parse` 后再次 `JSON.stringify`。
- 修改字段、金额、空格、字段顺序、转义或数字表示。
- 把另一个订单的 `data` 与当前 `dbAuthorization` 组合。

即使两份 JSON 解析后的对象相同，只要原始字节不同，签名也可能验证失败。业务服务端也应让签名函数直接接收最终序列化字节，避免“签名一份、返回另一份”。

## 公私钥方向

| 通信方向 | 签名方与私钥 | `cert_serial` 指向 | 验证方与公钥 |
| --- | --- | --- | --- |
| 开发者生成 `data + dbAuthorization` | 开发者服务端使用开发者私钥 | 开发者证书 | 平台使用开发者证书中的公钥验签 |
| 平台发送支付、退款或签约回调 | 平台使用平台私钥 | 平台证书 | 开发者服务端使用平台证书中的公钥验签 |

私钥只用于签名并留在各自服务端；公钥用于验签。证书序列号只是选择证书的索引，不是密钥。开发者证书和平台证书不能混用。

服务端启动时应校验开发者私钥与开发者证书公钥匹配。证书轮换期间允许受信任的新旧证书并存，签名使用当前有效的开发者证书，验签按 `cert_serial` 精确选择平台证书；未知序列号不得回退到任意证书尝试。

## 与回调验签的关系

`dbAuthorization` 解决开发者发起预下单时的身份和数据完整性；支付结果回调则由平台在 `X-DB-Authorization` Header 中提供签名，方向相反。

回调处理必须保留原始 Body，使用实际 method、path、query 和原始 Body Hash 重建待签名串，再用 `cert_serial` 对应的平台证书公钥验签。验签通过后才能解析 Body 并处理订单。详细步骤读取 [签名认证及加密传输](../security/signature-authentication-and-encryption.md) 和 [支付结果回调](payment-result-callback.md)。

## 实现与测试要求

- 签名算法使用 ECC P-256 / ECDSA-SHA256；签名编码是 ASN.1 DER 后 padded Base64URL。Go 使用 `base64.URLEncoding`，不得使用 `base64.RawURLEncoding`；其它语言映射读取通用签名规范。
- 加密库对“传原文并内部 SHA-256”和“传预计算摘要”的 API 定义不同，只选择其中一种，避免对待签名串重复哈希。
- timestamp、nonce、signature、完整 `data` 和完整 `dbAuthorization` 不进入普通业务日志；排障使用脱敏订单号、证书序列号和平台 log ID。
- 单测至少覆盖：正确 DER 签名、需要零/一个/两个 padding 的 Signature、Body 被篡改、签名路径错误、Raw Base64URL、固定长度 `r || s`、证书与私钥不匹配、过期 timestamp，以及 `data` 被重新序列化后验签失败。
- fixture、sandbox 和正式环境分别使用各自的密钥、证书、订单和回调地址，不得混用。
