diff --git a/apps/backend/data/khhao-sources.json b/apps/backend/data/khhao-sources.json index 0588940d..cde28bcf 100644 --- a/apps/backend/data/khhao-sources.json +++ b/apps/backend/data/khhao-sources.json @@ -5,7 +5,7 @@ "password": "order_check_test1", "maxCaptchaAttempts": 3, "autoSync": { - "enabled": true, + "enabled": false, "intervalMinutes": 3, "pages": 2, "pageSize": 20 diff --git a/apps/backend/data/khhao-sync-state.json b/apps/backend/data/khhao-sync-state.json index d5896f03..b0190558 100644 --- a/apps/backend/data/khhao-sync-state.json +++ b/apps/backend/data/khhao-sync-state.json @@ -1,13 +1,13 @@ { "syncFromCreatedAt": "2026-05-02T10:53:28.550Z", - "lastRunStartedAt": "2026-05-04T14:03:19.832Z", - "lastRunFinishedAt": "2026-05-04T14:03:20.530Z", + "lastRunStartedAt": "2026-05-11T14:09:54.989Z", + "lastRunFinishedAt": "2026-05-11T14:09:56.241Z", "lastRunStatus": "success", "lastErrorMessage": "", "fetchedCount": 40, "syncedCount": 0, "ignoredCount": 40, - "lastOrderCreatedAt": "2026-05-04T14:02:50.000Z", + "lastOrderCreatedAt": "2026-05-11T14:09:50.000Z", "watchMode": "normal", "watchOrders": [] } diff --git a/apps/backend/src/services/open-91/order-create-service.js b/apps/backend/src/services/open-91/order-create-service.js index a3a4105b..c1a676b8 100644 --- a/apps/backend/src/services/open-91/order-create-service.js +++ b/apps/backend/src/services/open-91/order-create-service.js @@ -24,7 +24,7 @@ export async function createOpen91Order(payload = {}, { requestId = '' } = {}) { assertOpen91CreatePayload(normalized, config) assertOpen91Timestamp(normalized.timestamp, config.timestampToleranceSeconds) - assertOpen91Signature(normalized, config) + assertOpen91Signature(payload, config) const now = nowIso() const items = [ diff --git a/apps/backend/src/services/open-91/order-query-service.js b/apps/backend/src/services/open-91/order-query-service.js index b5b09788..530ce791 100644 --- a/apps/backend/src/services/open-91/order-query-service.js +++ b/apps/backend/src/services/open-91/order-query-service.js @@ -26,7 +26,7 @@ export async function queryOpen91Order(payload = {}, { requestId = '' } = {}) { assertOpen91QueryPayload(normalized, config) assertOpen91Timestamp(normalized.timestamp, config.timestampToleranceSeconds) - assertOpen91Signature(normalized, config) + assertOpen91Signature(payload, config) const order = await findOrderByPlatformOrderId({ provider: OPEN_91_PROVIDER, diff --git a/apps/backend/src/services/open-91/shared.js b/apps/backend/src/services/open-91/shared.js index 1e56e02a..c17e8f2b 100644 --- a/apps/backend/src/services/open-91/shared.js +++ b/apps/backend/src/services/open-91/shared.js @@ -28,13 +28,6 @@ export function getOpen91Config() { export function assertOpen91Config() { const config = getOpen91Config() - if (!config.userId) { - throw createHttpError('91卡券 userId 未配置', { - statusCode: 500, - errorCode: 'open91_missing_user_id', - }) - } - if (!config.secret) { throw createHttpError('91卡券 secret 未配置', { statusCode: 500, @@ -154,13 +147,15 @@ export function assertOpen91Timestamp(timestamp, toleranceSeconds = assertOpen91 } } -export function buildOpen91SignSource(params = {}, { userId, secret } = assertOpen91Config()) { - const entries = Object.entries({ - ...params, - userId: normalizeOpen91String(userId), - }) +export function buildOpen91SignSource(params = {}, { secret } = assertOpen91Config()) { + const entries = Object.entries(params) .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 .map(([key, value]) => `${key}=${stringifyOpen91SignValue(value)}`) diff --git a/apps/backend/src/services/open-91/shared.test.js b/apps/backend/src/services/open-91/shared.test.js index c2e8c1ce..e35df9a7 100644 --- a/apps/backend/src/services/open-91/shared.test.js +++ b/apps/backend/src/services/open-91/shared.test.js @@ -10,7 +10,7 @@ import { verifyOpen91Signature, } from './shared.js' -test('signOpen91Payload matches documented md5 signing rule', () => { +test('signOpen91Payload signs only request body fields', () => { const config = { userId: '1001', secret: 'rste57w8rsubsnxsb384ur3u9kn5fzhr', @@ -28,7 +28,7 @@ test('signOpen91Payload matches documented md5 signing rule', () => { const source = buildOpen91SignSource(payload, config) assert.equal( 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) @@ -36,6 +36,26 @@ test('signOpen91Payload matches documented md5 signing rule', () => { 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', () => { const cards = [ { diff --git a/deploy/data/backend/.gitkeep b/deploy/data/backend/.gitkeep deleted file mode 100644 index 8b137891..00000000 --- a/deploy/data/backend/.gitkeep +++ /dev/null @@ -1 +0,0 @@ - diff --git a/docs/91卡券/3.异步卡密下单.md b/docs/91卡券/3.异步卡密下单.md index fcc68668..196d7ec7 100644 --- a/docs/91卡券/3.异步卡密下单.md +++ b/docs/91卡券/3.异步卡密下单.md @@ -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` 方式拼接; - 前后拼接商户密钥; - 取 `MD5`,输出 32 位大写字符串; -- 空值参数参与签名。 +- 已传入的空值参数参与签名; +- 不要额外加入 `userId`、商户号、内部配置项或未传入的可选字段。 ## 6. 请求参数 @@ -77,13 +79,12 @@ POST https://221329.cc.cd/api/v1/open/91/orders/create 假设: -- `userId = 1001` - 商户密钥为:`your_secret_key` 则源串示例如下: ```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. 业务处理规则 diff --git a/docs/91卡券/4.查询订单接口.md b/docs/91卡券/4.查询订单接口.md index 87d755d6..f9df2080 100644 --- a/docs/91卡券/4.查询订单接口.md +++ b/docs/91卡券/4.查询订单接口.md @@ -33,11 +33,13 @@ POST https://221329.cc.cd/api/v1/open/91/orders/query 签名规则采用 2.签名规则示例 中的约定: -- 除 `sign` 外,所有参数按字段名 ASCII 升序排序; +- 仅使用请求 JSON body 中实际传入的参数参与签名; +- 除 `sign` 外,所有 body 参数按字段名 ASCII 升序排序; - 使用 `key=value&key=value` 方式拼接; - 前后拼接商户密钥; - 取 `MD5`,输出 32 位大写字符串; -- 空值参数参与签名。 +- 已传入的空值参数参与签名; +- 不要额外加入 `userId`、商户号、内部配置项或未传入的可选字段。 ## 6. 请求参数 @@ -63,13 +65,12 @@ POST https://221329.cc.cd/api/v1/open/91/orders/query 假设: -- `userId = 1001` - 商户密钥为:`your_secret_key` 则源串示例如下: ```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. 业务处理规则 diff --git a/docs/91卡券/5.当前项目对接配置.md b/docs/91卡券/5.当前项目对接配置.md index 3345bed8..1def5983 100644 --- a/docs/91卡券/5.当前项目对接配置.md +++ b/docs/91卡券/5.当前项目对接配置.md @@ -37,7 +37,8 @@ https://你的域名/api/v1/open/91 签名规则采用当前文档里的示例规则: -- 除 `sign` 外,所有参数按字段名 ASCII 升序排序; +- 仅使用请求 JSON body 中实际传入的参数参与签名; +- 除 `sign` 外,所有 body 参数按字段名 ASCII 升序排序; - 使用 `key=value&key=value` 方式拼接; - 前后拼接商户密钥; - 取 `MD5`,输出 32 位大写字符串。 @@ -48,7 +49,8 @@ https://你的域名/api/v1/open/91 - `charset`: `UTF-8` - `timestamp`: 10 位秒级 Unix 时间戳 - `version`: `1.0` -- 空值参数是否参与签名:`参与` +- 已传入的空值参数是否参与签名:`参与` +- 不参与签名:`sign`、未传入的可选字段、`userId`、商户号、内部配置项 ## 5. 业务字段映射 @@ -296,7 +298,6 @@ Content-Type: application/json;charset=utf-8 - 异步卡密下单 URL - 查询订单 URL -- `userId` - 商户密钥 - 签名算法是否按本文档固定 - `cards` 中 `cardNo` 直接展示链接是否按预期显示 diff --git a/docs/91卡券/6.开发设计.md b/docs/91卡券/6.开发设计.md index 6d5d0093..f6dd3665 100644 --- a/docs/91卡券/6.开发设计.md +++ b/docs/91卡券/6.开发设计.md @@ -109,12 +109,13 @@ platforms: { 签名规则沿用文档约定: -1. 除 `sign` 外,所有参数按 ASCII 升序排序; -2. 拼接为 QueryString; -3. 前后加商户密钥; -4. 计算大写 MD5; -5. `userId` 不在请求体中,但参与签名; -6. 空值参数参与签名。 +1. 仅使用请求 JSON body 中实际传入的参数参与签名; +2. 除 `sign` 外,所有 body 参数按 ASCII 升序排序; +3. 拼接为 QueryString; +4. 前后加商户密钥; +5. 计算大写 MD5; +6. 已传入的空值参数参与签名; +7. 不额外加入 `userId`、商户号、内部配置项或未传入的可选字段。 需要实现: