签名成功
This commit is contained in:
@@ -5,7 +5,7 @@
|
|||||||
"password": "order_check_test1",
|
"password": "order_check_test1",
|
||||||
"maxCaptchaAttempts": 3,
|
"maxCaptchaAttempts": 3,
|
||||||
"autoSync": {
|
"autoSync": {
|
||||||
"enabled": true,
|
"enabled": false,
|
||||||
"intervalMinutes": 3,
|
"intervalMinutes": 3,
|
||||||
"pages": 2,
|
"pages": 2,
|
||||||
"pageSize": 20
|
"pageSize": 20
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
{
|
{
|
||||||
"syncFromCreatedAt": "2026-05-02T10:53:28.550Z",
|
"syncFromCreatedAt": "2026-05-02T10:53:28.550Z",
|
||||||
"lastRunStartedAt": "2026-05-04T14:03:19.832Z",
|
"lastRunStartedAt": "2026-05-11T14:09:54.989Z",
|
||||||
"lastRunFinishedAt": "2026-05-04T14:03:20.530Z",
|
"lastRunFinishedAt": "2026-05-11T14:09:56.241Z",
|
||||||
"lastRunStatus": "success",
|
"lastRunStatus": "success",
|
||||||
"lastErrorMessage": "",
|
"lastErrorMessage": "",
|
||||||
"fetchedCount": 40,
|
"fetchedCount": 40,
|
||||||
"syncedCount": 0,
|
"syncedCount": 0,
|
||||||
"ignoredCount": 40,
|
"ignoredCount": 40,
|
||||||
"lastOrderCreatedAt": "2026-05-04T14:02:50.000Z",
|
"lastOrderCreatedAt": "2026-05-11T14:09:50.000Z",
|
||||||
"watchMode": "normal",
|
"watchMode": "normal",
|
||||||
"watchOrders": []
|
"watchOrders": []
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ export async function createOpen91Order(payload = {}, { requestId = '' } = {}) {
|
|||||||
|
|
||||||
assertOpen91CreatePayload(normalized, config)
|
assertOpen91CreatePayload(normalized, config)
|
||||||
assertOpen91Timestamp(normalized.timestamp, config.timestampToleranceSeconds)
|
assertOpen91Timestamp(normalized.timestamp, config.timestampToleranceSeconds)
|
||||||
assertOpen91Signature(normalized, config)
|
assertOpen91Signature(payload, config)
|
||||||
|
|
||||||
const now = nowIso()
|
const now = nowIso()
|
||||||
const items = [
|
const items = [
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ export async function queryOpen91Order(payload = {}, { requestId = '' } = {}) {
|
|||||||
|
|
||||||
assertOpen91QueryPayload(normalized, config)
|
assertOpen91QueryPayload(normalized, config)
|
||||||
assertOpen91Timestamp(normalized.timestamp, config.timestampToleranceSeconds)
|
assertOpen91Timestamp(normalized.timestamp, config.timestampToleranceSeconds)
|
||||||
assertOpen91Signature(normalized, config)
|
assertOpen91Signature(payload, config)
|
||||||
|
|
||||||
const order = await findOrderByPlatformOrderId({
|
const order = await findOrderByPlatformOrderId({
|
||||||
provider: OPEN_91_PROVIDER,
|
provider: OPEN_91_PROVIDER,
|
||||||
|
|||||||
@@ -28,13 +28,6 @@ export function getOpen91Config() {
|
|||||||
export function assertOpen91Config() {
|
export function assertOpen91Config() {
|
||||||
const config = getOpen91Config()
|
const config = getOpen91Config()
|
||||||
|
|
||||||
if (!config.userId) {
|
|
||||||
throw createHttpError('91卡券 userId 未配置', {
|
|
||||||
statusCode: 500,
|
|
||||||
errorCode: 'open91_missing_user_id',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!config.secret) {
|
if (!config.secret) {
|
||||||
throw createHttpError('91卡券 secret 未配置', {
|
throw createHttpError('91卡券 secret 未配置', {
|
||||||
statusCode: 500,
|
statusCode: 500,
|
||||||
@@ -154,13 +147,15 @@ export function assertOpen91Timestamp(timestamp, toleranceSeconds = assertOpen91
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function buildOpen91SignSource(params = {}, { userId, secret } = assertOpen91Config()) {
|
export function buildOpen91SignSource(params = {}, { secret } = assertOpen91Config()) {
|
||||||
const entries = Object.entries({
|
const entries = Object.entries(params)
|
||||||
...params,
|
|
||||||
userId: normalizeOpen91String(userId),
|
|
||||||
})
|
|
||||||
.filter(([key]) => key !== 'sign')
|
.filter(([key]) => key !== 'sign')
|
||||||
.sort(([left], [right]) => left.localeCompare(right))
|
.sort(([left], [right]) => {
|
||||||
|
if (left === right) {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return left < right ? -1 : 1
|
||||||
|
})
|
||||||
|
|
||||||
const queryString = entries
|
const queryString = entries
|
||||||
.map(([key, value]) => `${key}=${stringifyOpen91SignValue(value)}`)
|
.map(([key, value]) => `${key}=${stringifyOpen91SignValue(value)}`)
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import {
|
|||||||
verifyOpen91Signature,
|
verifyOpen91Signature,
|
||||||
} from './shared.js'
|
} from './shared.js'
|
||||||
|
|
||||||
test('signOpen91Payload matches documented md5 signing rule', () => {
|
test('signOpen91Payload signs only request body fields', () => {
|
||||||
const config = {
|
const config = {
|
||||||
userId: '1001',
|
userId: '1001',
|
||||||
secret: 'rste57w8rsubsnxsb384ur3u9kn5fzhr',
|
secret: 'rste57w8rsubsnxsb384ur3u9kn5fzhr',
|
||||||
@@ -28,7 +28,7 @@ test('signOpen91Payload matches documented md5 signing rule', () => {
|
|||||||
const source = buildOpen91SignSource(payload, config)
|
const source = buildOpen91SignSource(payload, config)
|
||||||
assert.equal(
|
assert.equal(
|
||||||
source,
|
source,
|
||||||
'rste57w8rsubsnxsb384ur3u9kn5fzhrbuyNum=1&callbackUrl=https://cb.example.com/notify/91/order&maxAmount=0.0000&orderNo=P91KS202605040001&productNo=KS-CLOUD-SKU-001×tamp=1777867200&userId=1001&version=1.0rste57w8rsubsnxsb384ur3u9kn5fzhr',
|
'rste57w8rsubsnxsb384ur3u9kn5fzhrbuyNum=1&callbackUrl=https://cb.example.com/notify/91/order&maxAmount=0.0000&orderNo=P91KS202605040001&productNo=KS-CLOUD-SKU-001×tamp=1777867200&version=1.0rste57w8rsubsnxsb384ur3u9kn5fzhr',
|
||||||
)
|
)
|
||||||
|
|
||||||
const sign = signOpen91Payload(payload, config)
|
const sign = signOpen91Payload(payload, config)
|
||||||
@@ -36,6 +36,26 @@ test('signOpen91Payload matches documented md5 signing rule', () => {
|
|||||||
assert.equal(verifyOpen91Signature({ ...payload, sign }, config), true)
|
assert.equal(verifyOpen91Signature({ ...payload, sign }, config), true)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('signOpen91Payload does not add missing optional fields or configured userId', () => {
|
||||||
|
const config = {
|
||||||
|
userId: '1001',
|
||||||
|
secret: 'rste57w8rsubsnxsb384ur3u9kn5fzhr',
|
||||||
|
}
|
||||||
|
const payload = {
|
||||||
|
orderNo: '2613300083829561',
|
||||||
|
productNo: '183999074510936',
|
||||||
|
buyNum: 1,
|
||||||
|
maxAmount: '0.01',
|
||||||
|
timestamp: 1778670308,
|
||||||
|
version: '1.0',
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.equal(
|
||||||
|
buildOpen91SignSource(payload, config),
|
||||||
|
'rste57w8rsubsnxsb384ur3u9kn5fzhrbuyNum=1&maxAmount=0.01&orderNo=2613300083829561&productNo=183999074510936×tamp=1778670308&version=1.0rste57w8rsubsnxsb384ur3u9kn5fzhr',
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
test('encryptOpen91Cards matches documented AES ECB base64 sample', () => {
|
test('encryptOpen91Cards matches documented AES ECB base64 sample', () => {
|
||||||
const cards = [
|
const cards = [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
|
|
||||||
@@ -39,11 +39,13 @@ POST https://221329.cc.cd/api/v1/open/91/orders/create
|
|||||||
|
|
||||||
签名规则采用 签名规则示例 中的约定:
|
签名规则采用 签名规则示例 中的约定:
|
||||||
|
|
||||||
- 除 `sign` 外,所有参数按字段名 ASCII 升序排序;
|
- 仅使用请求 JSON body 中实际传入的参数参与签名;
|
||||||
|
- 除 `sign` 外,所有 body 参数按字段名 ASCII 升序排序;
|
||||||
- 使用 `key=value&key=value` 方式拼接;
|
- 使用 `key=value&key=value` 方式拼接;
|
||||||
- 前后拼接商户密钥;
|
- 前后拼接商户密钥;
|
||||||
- 取 `MD5`,输出 32 位大写字符串;
|
- 取 `MD5`,输出 32 位大写字符串;
|
||||||
- 空值参数参与签名。
|
- 已传入的空值参数参与签名;
|
||||||
|
- 不要额外加入 `userId`、商户号、内部配置项或未传入的可选字段。
|
||||||
|
|
||||||
## 6. 请求参数
|
## 6. 请求参数
|
||||||
|
|
||||||
@@ -77,13 +79,12 @@ POST https://221329.cc.cd/api/v1/open/91/orders/create
|
|||||||
|
|
||||||
假设:
|
假设:
|
||||||
|
|
||||||
- `userId = 1001`
|
|
||||||
- 商户密钥为:`your_secret_key`
|
- 商户密钥为:`your_secret_key`
|
||||||
|
|
||||||
则源串示例如下:
|
则源串示例如下:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
your_secret_keybuyNum=1&callbackUrl=https://cb.example.com/notify/91/order&maxAmount=0.0000&orderNo=P91KS202605040001&productNo=KS-CLOUD-SKU-001×tamp=1777867200&userId=1001&version=1.0your_secret_key
|
your_secret_keybuyNum=1&callbackUrl=https://cb.example.com/notify/91/order&maxAmount=0.0000&orderNo=P91KS202605040001&productNo=KS-CLOUD-SKU-001×tamp=1777867200&version=1.0your_secret_key
|
||||||
```
|
```
|
||||||
|
|
||||||
## 9. 业务处理规则
|
## 9. 业务处理规则
|
||||||
|
|||||||
@@ -33,11 +33,13 @@ POST https://221329.cc.cd/api/v1/open/91/orders/query
|
|||||||
|
|
||||||
签名规则采用 2.签名规则示例 中的约定:
|
签名规则采用 2.签名规则示例 中的约定:
|
||||||
|
|
||||||
- 除 `sign` 外,所有参数按字段名 ASCII 升序排序;
|
- 仅使用请求 JSON body 中实际传入的参数参与签名;
|
||||||
|
- 除 `sign` 外,所有 body 参数按字段名 ASCII 升序排序;
|
||||||
- 使用 `key=value&key=value` 方式拼接;
|
- 使用 `key=value&key=value` 方式拼接;
|
||||||
- 前后拼接商户密钥;
|
- 前后拼接商户密钥;
|
||||||
- 取 `MD5`,输出 32 位大写字符串;
|
- 取 `MD5`,输出 32 位大写字符串;
|
||||||
- 空值参数参与签名。
|
- 已传入的空值参数参与签名;
|
||||||
|
- 不要额外加入 `userId`、商户号、内部配置项或未传入的可选字段。
|
||||||
|
|
||||||
## 6. 请求参数
|
## 6. 请求参数
|
||||||
|
|
||||||
@@ -63,13 +65,12 @@ POST https://221329.cc.cd/api/v1/open/91/orders/query
|
|||||||
|
|
||||||
假设:
|
假设:
|
||||||
|
|
||||||
- `userId = 1001`
|
|
||||||
- 商户密钥为:`your_secret_key`
|
- 商户密钥为:`your_secret_key`
|
||||||
|
|
||||||
则源串示例如下:
|
则源串示例如下:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
your_secret_keyorderNo=P91KS202605040001×tamp=1777867260&userId=1001&version=1.0your_secret_key
|
your_secret_keyorderNo=P91KS202605040001×tamp=1777867260&version=1.0your_secret_key
|
||||||
```
|
```
|
||||||
|
|
||||||
## 9. 业务处理规则
|
## 9. 业务处理规则
|
||||||
|
|||||||
@@ -37,7 +37,8 @@ https://你的域名/api/v1/open/91
|
|||||||
|
|
||||||
签名规则采用当前文档里的示例规则:
|
签名规则采用当前文档里的示例规则:
|
||||||
|
|
||||||
- 除 `sign` 外,所有参数按字段名 ASCII 升序排序;
|
- 仅使用请求 JSON body 中实际传入的参数参与签名;
|
||||||
|
- 除 `sign` 外,所有 body 参数按字段名 ASCII 升序排序;
|
||||||
- 使用 `key=value&key=value` 方式拼接;
|
- 使用 `key=value&key=value` 方式拼接;
|
||||||
- 前后拼接商户密钥;
|
- 前后拼接商户密钥;
|
||||||
- 取 `MD5`,输出 32 位大写字符串。
|
- 取 `MD5`,输出 32 位大写字符串。
|
||||||
@@ -48,7 +49,8 @@ https://你的域名/api/v1/open/91
|
|||||||
- `charset`: `UTF-8`
|
- `charset`: `UTF-8`
|
||||||
- `timestamp`: 10 位秒级 Unix 时间戳
|
- `timestamp`: 10 位秒级 Unix 时间戳
|
||||||
- `version`: `1.0`
|
- `version`: `1.0`
|
||||||
- 空值参数是否参与签名:`参与`
|
- 已传入的空值参数是否参与签名:`参与`
|
||||||
|
- 不参与签名:`sign`、未传入的可选字段、`userId`、商户号、内部配置项
|
||||||
|
|
||||||
## 5. 业务字段映射
|
## 5. 业务字段映射
|
||||||
|
|
||||||
@@ -296,7 +298,6 @@ Content-Type: application/json;charset=utf-8
|
|||||||
|
|
||||||
- 异步卡密下单 URL
|
- 异步卡密下单 URL
|
||||||
- 查询订单 URL
|
- 查询订单 URL
|
||||||
- `userId`
|
|
||||||
- 商户密钥
|
- 商户密钥
|
||||||
- 签名算法是否按本文档固定
|
- 签名算法是否按本文档固定
|
||||||
- `cards` 中 `cardNo` 直接展示链接是否按预期显示
|
- `cards` 中 `cardNo` 直接展示链接是否按预期显示
|
||||||
|
|||||||
+7
-6
@@ -109,12 +109,13 @@ platforms: {
|
|||||||
|
|
||||||
签名规则沿用文档约定:
|
签名规则沿用文档约定:
|
||||||
|
|
||||||
1. 除 `sign` 外,所有参数按 ASCII 升序排序;
|
1. 仅使用请求 JSON body 中实际传入的参数参与签名;
|
||||||
2. 拼接为 QueryString;
|
2. 除 `sign` 外,所有 body 参数按 ASCII 升序排序;
|
||||||
3. 前后加商户密钥;
|
3. 拼接为 QueryString;
|
||||||
4. 计算大写 MD5;
|
4. 前后加商户密钥;
|
||||||
5. `userId` 不在请求体中,但参与签名;
|
5. 计算大写 MD5;
|
||||||
6. 空值参数参与签名。
|
6. 已传入的空值参数参与签名;
|
||||||
|
7. 不额外加入 `userId`、商户号、内部配置项或未传入的可选字段。
|
||||||
|
|
||||||
需要实现:
|
需要实现:
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user