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) }