91kaquan 初步介入

This commit is contained in:
yml
2026-05-04 22:05:54 +08:00
parent f4cc2ce623
commit b120924263
12 changed files with 1273 additions and 3 deletions
+9
View File
@@ -69,6 +69,15 @@ module.exports = {
shops: {},
},
},
ninetyone: {
userId: '',
secret: '',
version: '1.0',
shopId: '91kaquan',
shopName: '91卡券',
timestampToleranceSeconds: 600,
cardsEncoding: 'aes-256-ecb-base64',
},
khhao: {
baseUrl: 'https://admin.khhao.com',
timeoutMs: 5000,
+3 -3
View File
@@ -1,13 +1,13 @@
{
"syncFromCreatedAt": "2026-05-02T10:53:28.550Z",
"lastRunStartedAt": "2026-05-04T13:32:11.217Z",
"lastRunFinishedAt": "2026-05-04T13:32:11.824Z",
"lastRunStartedAt": "2026-05-04T14:03:19.832Z",
"lastRunFinishedAt": "2026-05-04T14:03:20.530Z",
"lastRunStatus": "success",
"lastErrorMessage": "",
"fetchedCount": 40,
"syncedCount": 0,
"ignoredCount": 40,
"lastOrderCreatedAt": "2026-05-04T13:32:06.000Z",
"lastOrderCreatedAt": "2026-05-04T14:02:50.000Z",
"watchMode": "normal",
"watchOrders": []
}
+35
View File
@@ -235,6 +235,41 @@ function applyEnvOverrides(baseConfig) {
nextConfig.platforms.agiso.messaging.enabled = agisoMessagingEnabled
}
const ninetyoneUserId = String(process.env.NINETYONE_USER_ID || '').trim()
if (ninetyoneUserId) {
nextConfig.platforms.ninetyone.userId = ninetyoneUserId
}
const ninetyoneSecret = String(process.env.NINETYONE_SECRET || '').trim()
if (ninetyoneSecret) {
nextConfig.platforms.ninetyone.secret = ninetyoneSecret
}
const ninetyoneVersion = String(process.env.NINETYONE_VERSION || '').trim()
if (ninetyoneVersion) {
nextConfig.platforms.ninetyone.version = ninetyoneVersion
}
const ninetyoneShopId = String(process.env.NINETYONE_SHOP_ID || '').trim()
if (ninetyoneShopId) {
nextConfig.platforms.ninetyone.shopId = ninetyoneShopId
}
const ninetyoneShopName = String(process.env.NINETYONE_SHOP_NAME || '').trim()
if (ninetyoneShopName) {
nextConfig.platforms.ninetyone.shopName = ninetyoneShopName
}
const ninetyoneTimestampToleranceSeconds = parseInteger(process.env.NINETYONE_TIMESTAMP_TOLERANCE_SECONDS)
if (ninetyoneTimestampToleranceSeconds !== null) {
nextConfig.platforms.ninetyone.timestampToleranceSeconds = ninetyoneTimestampToleranceSeconds
}
const ninetyoneCardsEncoding = String(process.env.NINETYONE_CARDS_ENCODING || '').trim()
if (ninetyoneCardsEncoding) {
nextConfig.platforms.ninetyone.cardsEncoding = ninetyoneCardsEncoding
}
const khhaoBaseUrl = String(process.env.KHHAO_BASE_URL || '').trim()
if (khhaoBaseUrl) {
nextConfig.platforms.khhao.baseUrl = khhaoBaseUrl
+2
View File
@@ -5,6 +5,7 @@ import { runtimeConfig } from './config/runtime.js'
import { runDatabaseMigrations } from './db/migrate.js'
import adminRouter from './routes/admin.js'
import claimsRouter from './routes/claims.js'
import open91Router from './routes/open-91.js'
import webhooksRouter from './routes/webhooks.js'
import { ensureAdminUsersBootstrapped } from './services/admin/admin-auth-service.js'
import { ensureFulfillmentCatalogBootstrapped } from './services/bootstrap/fulfillment-bootstrap-service.js'
@@ -118,6 +119,7 @@ app.use((req, res, next) => {
})
app.use('/api/v1/tencent', tencentRouter)
app.use('/api/v1/open/91', open91Router)
app.use('/api/v1/webhooks', webhooksRouter)
app.use('/api/v1/claim', claimsRouter)
app.use('/api/v1/admin', adminRouter)
+74
View File
@@ -0,0 +1,74 @@
import { Router } from 'express'
import { createOpen91Order } from '../services/open-91/order-create-service.js'
import { queryOpen91Order } from '../services/open-91/order-query-service.js'
import { buildOpen91ErrorResponse } from '../services/open-91/shared.js'
import { createRequestId, logWebhook } from '../utils/logger.js'
const router = Router()
router.post('/orders/create', async (req, res) => {
const requestId = createRequestId('91')
const startedAt = Date.now()
logWebhook('[open-91/create]', '收到 91卡券异步下单请求', {
requestId,
method: req.method,
originalUrl: req.originalUrl,
ip: req.ip,
body: req.body,
})
try {
const result = await createOpen91Order(req.body, { requestId })
logWebhook('[open-91/create]', '91卡券异步下单处理完成', {
requestId,
durationMs: Date.now() - startedAt,
result,
})
res.status(200).json(result)
} catch (error) {
const message = error instanceof Error ? error.message : '系统错误'
const code = Number(error?.statusCode || 500) >= 500 ? 500 : 400
logWebhook('[open-91/create]', '91卡券异步下单处理失败', {
requestId,
durationMs: Date.now() - startedAt,
error,
}, { level: 'error' })
res.status(200).json(buildOpen91ErrorResponse(message, code))
}
})
router.post('/orders/query', async (req, res) => {
const requestId = createRequestId('91')
const startedAt = Date.now()
logWebhook('[open-91/query]', '收到 91卡券订单查询请求', {
requestId,
method: req.method,
originalUrl: req.originalUrl,
ip: req.ip,
body: req.body,
})
try {
const result = await queryOpen91Order(req.body, { requestId })
logWebhook('[open-91/query]', '91卡券订单查询处理完成', {
requestId,
durationMs: Date.now() - startedAt,
result,
})
res.status(200).json(result)
} catch (error) {
const message = error instanceof Error ? error.message : '系统错误'
const code = Number(error?.statusCode || 500) >= 500 ? 500 : 400
logWebhook('[open-91/query]', '91卡券订单查询处理失败', {
requestId,
durationMs: Date.now() - startedAt,
error,
}, { level: 'error' })
res.status(200).json(buildOpen91ErrorResponse(message, code))
}
})
export default router
@@ -0,0 +1,129 @@
import { hasConfiguredOrderItems } from '../order/product-match-service.js'
import { upsertOrderFromSource } from '../order/order-service.js'
import { nowIso } from '../../utils/time.js'
import { logWebhook } from '../../utils/logger.js'
import { parseAmountToFen } from '../../utils/money.js'
import {
assertOpen91Config,
assertOpen91CreatePayload,
assertOpen91Signature,
assertOpen91Timestamp,
buildOpen91OrderCost,
buildOpen91OutTradeNo,
buildOpen91SuccessResponse,
normalizeOpen91CreatePayload,
OPEN_91_COST_EXCEED_FAIL_CODE,
OPEN_91_DEFAULT_FAIL_CODE,
OPEN_91_PLATFORM,
OPEN_91_PROVIDER,
} from './shared.js'
export async function createOpen91Order(payload = {}, { requestId = '' } = {}) {
const config = assertOpen91Config()
const normalized = normalizeOpen91CreatePayload(payload)
assertOpen91CreatePayload(normalized, config)
assertOpen91Timestamp(normalized.timestamp, config.timestampToleranceSeconds)
assertOpen91Signature(normalized, config)
const now = nowIso()
const items = [
{
externalItemId: normalized.productNo,
externalSkuCode: normalized.productNo,
externalSkuName: normalized.productNo,
quantity: normalized.buyNum,
spec: {
source: OPEN_91_PROVIDER,
},
snapshot: {
source: OPEN_91_PROVIDER,
orderNo: normalized.orderNo,
productNo: normalized.productNo,
callbackUrl: normalized.callbackUrl,
},
},
]
const configured = await hasConfiguredOrderItems({
provider: OPEN_91_PROVIDER,
platform: OPEN_91_PLATFORM,
shopId: config.shopId,
items,
})
if (!configured) {
return buildOpen91SuccessResponse({
orderNo: normalized.orderNo,
outTradeNo: '',
orderStatus: 30,
failCode: OPEN_91_DEFAULT_FAIL_CODE,
failReason: '商品未配置或未匹配到履约规则',
})
}
if (normalized.maxAmount) {
const maxAmountFen = parseAmountToFen(normalized.maxAmount)
if (maxAmountFen < 0) {
return buildOpen91SuccessResponse({
orderNo: normalized.orderNo,
outTradeNo: '',
orderStatus: 30,
failCode: OPEN_91_COST_EXCEED_FAIL_CODE,
failReason: '订单成本超出可接受范围',
})
}
}
const result = await upsertOrderFromSource({
provider: OPEN_91_PROVIDER,
platform: OPEN_91_PLATFORM,
shopId: config.shopId,
shopName: config.shopName,
platformOrderId: normalized.orderNo,
orderStatus: 'paid',
payStatus: 'paid',
buyerId: '',
buyerName: '',
receiverContact: '',
totalAmount: 0,
currency: 'CNY',
rawPayload: {
source: OPEN_91_PROVIDER,
...normalized,
},
paidAt: now,
items,
}, {
sourceLabel: 'open91',
})
logWebhook('[open-91/create]', '91卡券下单已处理', {
requestId,
orderNo: normalized.orderNo,
productNo: normalized.productNo,
buyNum: normalized.buyNum,
configured,
ignored: Boolean(result.ignored),
orderId: result.order?.id || null,
taskCount: Array.isArray(result.tasks) ? result.tasks.length : 0,
})
if (result.ignored || !result.order || !Array.isArray(result.tasks) || result.tasks.length === 0) {
return buildOpen91SuccessResponse({
orderNo: normalized.orderNo,
outTradeNo: '',
orderStatus: 30,
failCode: OPEN_91_DEFAULT_FAIL_CODE,
failReason: '商品未配置或订单无法履约',
})
}
return buildOpen91SuccessResponse({
orderNo: normalized.orderNo,
outTradeNo: buildOpen91OutTradeNo(result.order),
orderStatus: 10,
orderCost: buildOpen91OrderCost(0),
cards: '',
})
}
@@ -0,0 +1,125 @@
import { findOrderByPlatformOrderId } from '../../repositories/order-repo.js'
import { listTasksByOrderId } from '../../repositories/task-repo.js'
import { buildClaimUrl } from '../claim/claim-service.js'
import { ensureTaskClaimLink } from '../fulfillment/kuaishou-cloud-task-service.js'
import { logWebhook } from '../../utils/logger.js'
import {
assertOpen91Config,
assertOpen91QueryPayload,
assertOpen91Signature,
assertOpen91Timestamp,
buildOpen91CardItem,
buildOpen91Cards,
buildOpen91OrderCost,
buildOpen91OutTradeNo,
buildOpen91SuccessResponse,
normalizeOpen91QueryPayload,
OPEN_91_DEFAULT_FAIL_CODE,
OPEN_91_PLATFORM,
OPEN_91_PROVIDER,
resolveOpen91QueryState,
} from './shared.js'
export async function queryOpen91Order(payload = {}, { requestId = '' } = {}) {
const config = assertOpen91Config()
const normalized = normalizeOpen91QueryPayload(payload)
assertOpen91QueryPayload(normalized, config)
assertOpen91Timestamp(normalized.timestamp, config.timestampToleranceSeconds)
assertOpen91Signature(normalized, config)
const order = await findOrderByPlatformOrderId({
provider: OPEN_91_PROVIDER,
platform: OPEN_91_PLATFORM,
shopId: config.shopId,
platformOrderId: normalized.orderNo,
})
if (!order) {
return buildOpen91SuccessResponse({
orderNo: normalized.orderNo,
outTradeNo: '',
orderStatus: 30,
failCode: OPEN_91_DEFAULT_FAIL_CODE,
failReason: '订单不存在',
})
}
const tasks = await listTasksByOrderId(order.id)
const cardItems = []
const readyTaskIds = []
for (const task of tasks) {
let claimUrl = ''
let expireTime = String(task.primary_claim_expires_at || task.claim_expires_at || '').trim()
const primaryToken = String(task.primary_claim_token || task.claim_token || '').trim()
if (primaryToken) {
claimUrl = buildClaimUrl(primaryToken)
} else if (String(task.executor_key || '').trim() === 'kuaishou_ct_assisted') {
try {
const ensured = await ensureTaskClaimLink(task)
claimUrl = String(ensured.claimUrl || '').trim()
expireTime = String(ensured.expiredAt || expireTime).trim()
} catch {
claimUrl = ''
}
}
if (!claimUrl) {
continue
}
readyTaskIds.push(Number(task.id))
cardItems.push(buildOpen91CardItem({
claimUrl,
expireTime,
}))
}
const queryState = resolveOpen91QueryState({
tasks,
readyTaskIds,
})
logWebhook('[open-91/query]', '91卡券订单查询已处理', {
requestId,
orderNo: normalized.orderNo,
orderId: order.id,
taskCount: tasks.length,
readyTaskCount: readyTaskIds.length,
orderStatus: queryState.orderStatus,
})
if (queryState.orderStatus === 30) {
return buildOpen91SuccessResponse({
orderNo: normalized.orderNo,
outTradeNo: buildOpen91OutTradeNo(order),
orderStatus: 30,
failCode: queryState.failCode,
failReason: queryState.failReason,
})
}
if (queryState.orderStatus === 10) {
return buildOpen91SuccessResponse({
orderNo: normalized.orderNo,
outTradeNo: buildOpen91OutTradeNo(order),
orderStatus: 10,
failCode: 0,
failReason: '',
orderCost: buildOpen91OrderCost(0),
cards: '',
})
}
return buildOpen91SuccessResponse({
orderNo: normalized.orderNo,
outTradeNo: buildOpen91OutTradeNo(order),
orderStatus: 20,
failCode: 0,
failReason: '',
orderCost: buildOpen91OrderCost(0),
cards: buildOpen91Cards(cardItems, config),
})
}
+343
View File
@@ -0,0 +1,343 @@
import crypto from 'node:crypto'
import { runtimeConfig } from '../../config/runtime.js'
import { createHttpError } from '../../utils/http.js'
export const OPEN_91_PROVIDER = '91kaquan'
export const OPEN_91_PLATFORM = 'kuaishou'
export const OPEN_91_SUCCESS_MESSAGE = '接口调用成功'
export const OPEN_91_DEFAULT_FAIL_CODE = 1204
export const OPEN_91_COST_EXCEED_FAIL_CODE = 1220
export function getOpen91Config() {
const config = /** @type {import('../../types/runtime-config.js').RuntimeConfig['platforms']['ninetyone']} */ (
runtimeConfig.platforms?.ninetyone || {}
)
return {
userId: String(config.userId || '').trim(),
secret: String(config.secret || '').trim(),
version: String(config.version || '1.0').trim() || '1.0',
shopId: String(config.shopId || OPEN_91_PROVIDER).trim() || OPEN_91_PROVIDER,
shopName: String(config.shopName || '91卡券').trim() || '91卡券',
timestampToleranceSeconds: Number(config.timestampToleranceSeconds || 600) || 600,
cardsEncoding: String(config.cardsEncoding || 'aes-256-ecb-base64').trim() || 'aes-256-ecb-base64',
}
}
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,
errorCode: 'open91_missing_secret',
})
}
if (config.secret.length !== 32) {
throw createHttpError('91卡券 secret 长度必须为 32 个字符', {
statusCode: 500,
errorCode: 'open91_invalid_secret_length',
})
}
return config
}
export function normalizeOpen91String(value) {
return String(value || '').trim()
}
export function normalizeOpen91CreatePayload(payload = {}) {
return {
orderNo: normalizeOpen91String(payload.orderNo),
productNo: normalizeOpen91String(payload.productNo),
buyNum: normalizeOpen91BuyNum(payload.buyNum),
maxAmount: normalizeOpen91OptionalAmount(payload.maxAmount),
callbackUrl: normalizeOpen91String(payload.callbackUrl),
timestamp: normalizeOpen91Timestamp(payload.timestamp),
version: normalizeOpen91String(payload.version),
sign: normalizeOpen91String(payload.sign).toUpperCase(),
}
}
export function normalizeOpen91QueryPayload(payload = {}) {
return {
orderNo: normalizeOpen91String(payload.orderNo),
timestamp: normalizeOpen91Timestamp(payload.timestamp),
version: normalizeOpen91String(payload.version),
sign: normalizeOpen91String(payload.sign).toUpperCase(),
}
}
export function assertOpen91CreatePayload(payload, config = assertOpen91Config()) {
if (!payload.orderNo) {
throw createHttpError('缺少 orderNo', {
statusCode: 400,
errorCode: 'open91_missing_order_no',
})
}
if (!payload.productNo) {
throw createHttpError('缺少 productNo', {
statusCode: 400,
errorCode: 'open91_missing_product_no',
})
}
if (!Number.isInteger(payload.buyNum) || payload.buyNum <= 0) {
throw createHttpError('buyNum 必须是大于 0 的整数', {
statusCode: 400,
errorCode: 'open91_invalid_buy_num',
})
}
assertOpen91CommonPayload(payload, config)
}
export function assertOpen91QueryPayload(payload, config = assertOpen91Config()) {
if (!payload.orderNo) {
throw createHttpError('缺少 orderNo', {
statusCode: 400,
errorCode: 'open91_missing_order_no',
})
}
assertOpen91CommonPayload(payload, config)
}
function assertOpen91CommonPayload(payload, config) {
if (!payload.timestamp) {
throw createHttpError('缺少 timestamp', {
statusCode: 400,
errorCode: 'open91_missing_timestamp',
})
}
if (!payload.version) {
throw createHttpError('缺少 version', {
statusCode: 400,
errorCode: 'open91_missing_version',
})
}
if (payload.version !== config.version) {
throw createHttpError(`version 不匹配,当前仅支持 ${config.version}`, {
statusCode: 400,
errorCode: 'open91_invalid_version',
})
}
if (!payload.sign) {
throw createHttpError('缺少 sign', {
statusCode: 400,
errorCode: 'open91_missing_sign',
})
}
}
export function assertOpen91Timestamp(timestamp, toleranceSeconds = assertOpen91Config().timestampToleranceSeconds) {
const nowSeconds = Math.floor(Date.now() / 1000)
if (Math.abs(nowSeconds - Number(timestamp)) > Math.max(1, Number(toleranceSeconds) || 600)) {
throw createHttpError('timestamp 已过期', {
statusCode: 400,
errorCode: 'open91_expired_timestamp',
})
}
}
export function buildOpen91SignSource(params = {}, { userId, secret } = assertOpen91Config()) {
const entries = Object.entries({
...params,
userId: normalizeOpen91String(userId),
})
.filter(([key]) => key !== 'sign')
.sort(([left], [right]) => left.localeCompare(right))
const queryString = entries
.map(([key, value]) => `${key}=${stringifyOpen91SignValue(value)}`)
.join('&')
return `${secret}${queryString}${secret}`
}
export function signOpen91Payload(params = {}, config = assertOpen91Config()) {
return crypto
.createHash('md5')
.update(buildOpen91SignSource(params, config), 'utf8')
.digest('hex')
.toUpperCase()
}
export function verifyOpen91Signature(params = {}, config = assertOpen91Config()) {
const expected = signOpen91Payload(params, config)
const actual = normalizeOpen91String(params.sign).toUpperCase()
return expected === actual
}
export function assertOpen91Signature(params = {}, config = assertOpen91Config()) {
if (!verifyOpen91Signature(params, config)) {
throw createHttpError('验签失败', {
statusCode: 400,
errorCode: 'open91_invalid_signature',
})
}
}
export function encryptOpen91Cards(cards = [], secret = assertOpen91Config().secret) {
const normalizedSecret = normalizeOpen91String(secret)
if (normalizedSecret.length !== 32) {
throw createHttpError('91卡券 cards 加密密钥长度必须为 32 个字符', {
statusCode: 500,
errorCode: 'open91_invalid_cards_secret_length',
})
}
const plainText = JSON.stringify(Array.isArray(cards) ? cards : [])
const cipher = crypto.createCipheriv('aes-256-ecb', Buffer.from(normalizedSecret, 'utf8'), null)
cipher.setAutoPadding(true)
return `${cipher.update(plainText, 'utf8', 'base64')}${cipher.final('base64')}`
}
export function buildOpen91Cards(cards = [], config = assertOpen91Config()) {
const encoding = normalizeOpen91String(config.cardsEncoding).toLowerCase()
if (encoding && encoding !== 'aes-256-ecb-base64') {
throw createHttpError(`暂不支持的 cardsEncoding: ${config.cardsEncoding}`, {
statusCode: 500,
errorCode: 'open91_unsupported_cards_encoding',
})
}
return encryptOpen91Cards(cards, config.secret)
}
export function buildOpen91SuccessResponse(data, message = OPEN_91_SUCCESS_MESSAGE) {
return {
code: 200,
message,
data,
}
}
export function buildOpen91ErrorResponse(message, code = 500) {
return {
code,
message: normalizeOpen91String(message) || '系统错误',
data: null,
}
}
export function buildOpen91OutTradeNo(order) {
const orderId = Number(order?.id || 0)
return orderId > 0 ? `OS${String(orderId).padStart(10, '0')}` : ''
}
export function buildOpen91OrderCost(value = 0) {
const amount = Number(value)
return Number.isFinite(amount) ? amount.toFixed(4) : '0.0000'
}
export function buildOpen91CardItem({ claimUrl = '', expireTime = '' } = {}) {
const normalizedClaimUrl = normalizeOpen91String(claimUrl)
return {
cardNo: normalizedClaimUrl,
cardPwd: '',
expireTime: normalizeOpen91String(expireTime),
jumpLink: normalizedClaimUrl,
}
}
export function resolveOpen91QueryState({ tasks = [], readyTaskIds = [] } = {}) {
const normalizedTasks = Array.isArray(tasks) ? tasks : []
const readyTaskIdSet = new Set((Array.isArray(readyTaskIds) ? readyTaskIds : []).map((item) => Number(item)))
if (normalizedTasks.length === 0) {
return {
orderStatus: 30,
failCode: OPEN_91_DEFAULT_FAIL_CODE,
failReason: '订单未生成履约任务',
}
}
const failedTask = normalizedTasks.find((task) => {
const status = normalizeOpen91String(task?.task_status)
return ['failed', 'manual_review', 'closed'].includes(status) && !readyTaskIdSet.has(Number(task?.id || 0))
})
if (failedTask) {
return {
orderStatus: 30,
failCode: OPEN_91_DEFAULT_FAIL_CODE,
failReason: normalizeOpen91String(failedTask.last_error) || '订单无法履约',
}
}
if (readyTaskIdSet.size < normalizedTasks.length) {
return {
orderStatus: 10,
failCode: 0,
failReason: '',
}
}
return {
orderStatus: 20,
failCode: 0,
failReason: '',
}
}
export function stringifyOpen91SignValue(value) {
if (typeof value === 'number' && Number.isFinite(value)) {
return String(value)
}
if (typeof value === 'boolean') {
return value ? 'true' : 'false'
}
if (value == null) {
return ''
}
return String(value)
}
function normalizeOpen91BuyNum(value) {
const parsed = Number(value)
return Number.isInteger(parsed) ? parsed : 0
}
function normalizeOpen91OptionalAmount(value) {
const normalized = normalizeOpen91String(value)
if (!normalized) {
return ''
}
if (!/^\d+(?:\.\d+)?$/.test(normalized)) {
throw createHttpError('maxAmount 格式无效', {
statusCode: 400,
errorCode: 'open91_invalid_max_amount',
})
}
return normalized
}
function normalizeOpen91Timestamp(value) {
const normalized = normalizeOpen91String(value)
if (!/^\d{10}$/.test(normalized)) {
return 0
}
return Number(normalized)
}
@@ -0,0 +1,105 @@
import test from 'node:test'
import assert from 'node:assert/strict'
import {
buildOpen91CardItem,
buildOpen91SignSource,
encryptOpen91Cards,
resolveOpen91QueryState,
signOpen91Payload,
verifyOpen91Signature,
} from './shared.js'
test('signOpen91Payload matches documented md5 signing rule', () => {
const config = {
userId: '1001',
secret: 'rste57w8rsubsnxsb384ur3u9kn5fzhr',
}
const payload = {
orderNo: 'P91KS202605040001',
productNo: 'KS-CLOUD-SKU-001',
buyNum: 1,
maxAmount: '0.0000',
callbackUrl: 'https://cb.example.com/notify/91/order',
timestamp: 1777867200,
version: '1.0',
}
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&timestamp=1777867200&userId=1001&version=1.0rste57w8rsubsnxsb384ur3u9kn5fzhr',
)
const sign = signOpen91Payload(payload, config)
assert.equal(sign.length, 32)
assert.equal(verifyOpen91Signature({ ...payload, sign }, config), true)
})
test('encryptOpen91Cards matches documented AES ECB base64 sample', () => {
const cards = [
{
cardNo: '10001',
cardPwd: '123456',
expireTime: '2023-06-19 17:16:01',
},
]
assert.equal(
encryptOpen91Cards(cards, '0a091b3aa4324435aab703142518a8f7'),
'34APdvSJhWfr5wnzE4YzxAsI0gfI6pI/Njrj1UQMHJrZ8Dq9EWsDnln0pp08gn4KES4iQu9/f0Y8UMN4dGdSK67oi0Bc12/V8hrNxqoC3Lk=',
)
})
test('resolveOpen91QueryState returns success only when every task is ready', () => {
assert.deepEqual(
resolveOpen91QueryState({
tasks: [{ id: 1, task_status: 'pending_binding_prepare' }, { id: 2, task_status: 'waiting_binding' }],
readyTaskIds: [1, 2],
}),
{
orderStatus: 20,
failCode: 0,
failReason: '',
},
)
assert.deepEqual(
resolveOpen91QueryState({
tasks: [{ id: 1, task_status: 'pending_binding_prepare' }, { id: 2, task_status: 'waiting_binding' }],
readyTaskIds: [1],
}),
{
orderStatus: 10,
failCode: 0,
failReason: '',
},
)
assert.deepEqual(
resolveOpen91QueryState({
tasks: [{ id: 1, task_status: 'manual_review', last_error: '商品未配置' }],
readyTaskIds: [],
}),
{
orderStatus: 30,
failCode: 1204,
failReason: '商品未配置',
},
)
})
test('buildOpen91CardItem mirrors claim link into jumpLink and leaves password empty', () => {
assert.deepEqual(
buildOpen91CardItem({
claimUrl: 'https://221329.cc.cd/#/claim/abc123xyz',
expireTime: '2026-05-05 12:00:00',
}),
{
cardNo: 'https://221329.cc.cd/#/claim/abc123xyz',
cardPwd: '',
expireTime: '2026-05-05 12:00:00',
jumpLink: 'https://221329.cc.cd/#/claim/abc123xyz',
},
)
})
+9
View File
@@ -89,6 +89,15 @@ export {}
* shops: AgisoMessagingShopsConfig
* }
* }
* ninetyone: {
* userId: string
* secret: string
* version: string
* shopId: string
* shopName: string
* timestampToleranceSeconds: number
* cardsEncoding: string
* }
* khhao: {
* baseUrl: string
* timeoutMs: number