拆分feifei配置并修复商品查询

This commit is contained in:
yml2213
2026-07-07 14:59:22 +08:00
parent 83d501dc2d
commit 6af2fe2e23
13 changed files with 1317 additions and 221 deletions
@@ -1,8 +1,11 @@
import { Router } from "express";
import {
createAdminKuaishouFeifeiTestOrder,
getAdminKuaishouFeifeiConfig,
listAdminKuaishouFeifeiProducts,
matchAdminKuaishouFeifeiProduct,
queryAdminKuaishouFeifeiTestOrder,
updateAdminKuaishouFeifeiConfig,
} from "../../../services/admin/platform-config/kuaishou-feifei-service.js";
import { createJsonHandler } from "../session.js";
@@ -59,4 +62,55 @@ router.post(
)
);
router.post(
"/kuaishou-feifei/products",
createJsonHandler(
(req) => listAdminKuaishouFeifeiProducts(req.body as JsonRecord),
{
successMessage: "ok",
errorMessage: "查询 kuaishou-feifei 商品失败",
scope: "[admin/platform-config/kuaishou-feifei/products]",
}
)
);
router.post(
"/kuaishou-feifei/test-order",
createJsonHandler(
(req) => createAdminKuaishouFeifeiTestOrder(req.body as JsonRecord),
{
successMessage: "kuaishou-feifei 测试单已创建",
errorMessage: "创建 kuaishou-feifei 测试单失败",
scope: "[admin/platform-config/kuaishou-feifei/test-order]",
audit: (_req, data) => {
const result = data as JsonRecord;
return {
action: "platform_kuaishou_feifei_test_order_created",
targetType: "platform_config",
targetId: "kuaishou_feifei",
data: {
orderNo: String(result.orderNo || "").trim(),
platformOrderNo: String(result.platformOrderNo || "").trim(),
productCode: String(result.productCode || "").trim(),
rechargeStatus: Number(result.rechargeStatus || 0) || 0,
},
};
},
}
)
);
router.post(
"/kuaishou-feifei/query-order",
createJsonHandler(
(req) => queryAdminKuaishouFeifeiTestOrder(req.body as JsonRecord),
{
successMessage: "ok",
errorMessage: "查询 kuaishou-feifei 订单失败",
scope: "[admin/platform-config/kuaishou-feifei/query-order]",
}
)
);
export default router;
@@ -1,4 +1,9 @@
import { getKuaishouFeifeiConfig } from '../../platforms/kuaishou-feifei/config.js'
import {
createKuaishouFeifeiOrder,
listKuaishouFeifeiProducts,
queryKuaishouFeifeiOrder,
} from '../../platforms/kuaishou-feifei/order-service.js'
import { resolveKuaishouFeifeiProductByName } from '../../platforms/kuaishou-feifei/product-rule-service.js'
import {
getKuaishouFeifeiConfigFilePath,
@@ -8,6 +13,7 @@ import {
saveKuaishouFeifeiSourceConfig,
} from '../../platforms/kuaishou-feifei/source-config-service.js'
import { normalizeCloudtentaclesMatchName } from '../../order/cloudtentacles-match-utils.js'
import { createHttpError } from '../../../utils/http.js'
type JsonObject = Record<string, any>
@@ -49,6 +55,70 @@ export function matchAdminKuaishouFeifeiProduct(payload: JsonObject = {}) {
}
}
export async function listAdminKuaishouFeifeiProducts(payload: JsonObject = {}) {
return listKuaishouFeifeiProducts({
page: Number(payload.page || 1) || 1,
perPage: Number(payload.perPage || payload.per_page || 20) || 20,
status: String(payload.status || 'on_sale').trim(),
supplyProductName: String(payload.supplyProductName || payload.supply_product_name || '').trim(),
})
}
export async function createAdminKuaishouFeifeiTestOrder(payload: JsonObject = {}) {
const platformOrderNo = String(payload.platformOrderNo || payload.platform_order_no || '').trim()
const productCode = String(payload.productCode || payload.product_code || '').trim()
if (!platformOrderNo) {
throw createHttpError('测试业务单号不能为空', {
statusCode: 400,
errorCode: 'kuaishou_feifei_missing_platform_order_no',
})
}
if (!productCode) {
throw createHttpError('feifei 商品编码不能为空', {
statusCode: 400,
errorCode: 'kuaishou_feifei_missing_product_code',
})
}
const orderInput: Parameters<typeof createKuaishouFeifeiOrder>[0] = {
platformOrderNo,
productCode,
platformBuyNum: Number(payload.platformBuyNum || payload.platform_buy_num || 1) || 1,
playerAccount: String(payload.playerAccount || payload.player_account || '').trim(),
playerGameRegion: String(payload.playerGameRegion || payload.player_game_region || '').trim(),
playerGameSrv: String(payload.playerGameSrv || payload.player_game_srv || '').trim(),
playerGameRole: String(payload.playerGameRole || payload.player_game_role || '').trim(),
submitPlayer: payload.submitPlayer === true || payload.submit_player === true,
notifyUrl: String(payload.notifyUrl || payload.notify_url || '').trim(),
}
const platformAmount = normalizeOptionalNumber(payload.platformAmount ?? payload.platform_amount)
if (platformAmount !== undefined) {
orderInput.platformAmount = platformAmount
}
return createKuaishouFeifeiOrder(orderInput)
}
export async function queryAdminKuaishouFeifeiTestOrder(payload: JsonObject = {}) {
const platformOrderNo = String(payload.platformOrderNo || payload.platform_order_no || '').trim()
const orderNo = String(payload.orderNo || payload.order_no || '').trim()
if (!platformOrderNo && !orderNo) {
throw createHttpError('请填写 feifei 平台单号或测试业务单号', {
statusCode: 400,
errorCode: 'kuaishou_feifei_missing_order_query',
})
}
return queryKuaishouFeifeiOrder({
platformOrderNo,
orderNo,
})
}
function getAdminEditableKuaishouFeifeiConfig() {
if (hasKuaishouFeifeiConfigFile()) {
return getKuaishouFeifeiSourceConfig()
@@ -68,3 +138,12 @@ function mapEffectiveKuaishouFeifeiConfig(config: ReturnType<typeof getKuaishouF
productRuleCount: config.productRules.length,
}
}
function normalizeOptionalNumber(value: unknown) {
if (value == null || String(value).trim() === '') {
return undefined
}
const numberValue = Number(value)
return Number.isFinite(numberValue) ? numberValue : undefined
}
@@ -2,7 +2,10 @@ import crypto from 'node:crypto'
import test from 'node:test'
import assert from 'node:assert/strict'
import { signKuaishouFeifeiPayload } from './http-client.js'
import {
isKuaishouFeifeiSuccessResponse,
signKuaishouFeifeiPayload,
} from './http-client.js'
test('signKuaishouFeifeiPayload signs app key, timestamp and raw body with HMAC SHA256', () => {
const input = {
@@ -19,3 +22,10 @@ test('signKuaishouFeifeiPayload signs app key, timestamp and raw body with HMAC
assert.equal(signKuaishouFeifeiPayload(input), expected)
})
test('isKuaishouFeifeiSuccessResponse accepts documented and actual success codes', () => {
assert.equal(isKuaishouFeifeiSuccessResponse({ code: 0, message: 'success' }), true)
assert.equal(isKuaishouFeifeiSuccessResponse({ code: 10000, message: 'success' }), true)
assert.equal(isKuaishouFeifeiSuccessResponse({ code: 10000, message: '商品不存在' }), false)
assert.equal(isKuaishouFeifeiSuccessResponse({ code: 1, message: 'success' }), false)
})
@@ -44,7 +44,7 @@ export async function kuaishouFeifeiRequest(pathname: string, payload: JsonObjec
})
}
if (Number(json.code || 0) !== 0) {
if (!isKuaishouFeifeiSuccessResponse(json)) {
throw createHttpError(String(json.message || 'kuaishou-feifei 业务失败'), {
statusCode: 502,
errorCode: 'kuaishou_feifei_business_failed',
@@ -85,6 +85,13 @@ export function signKuaishouFeifeiPayload({
.toLowerCase()
}
export function isKuaishouFeifeiSuccessResponse(json: JsonObject) {
const code = Number(json.code ?? 0)
const message = String(json.message || json.msg || '').trim().toLowerCase()
return code === 0 || (code === 10000 && message === 'success')
}
function parseJsonObject(text: string): JsonObject {
try {
const parsed = JSON.parse(text || '{}')
@@ -3,6 +3,30 @@ import { kuaishouFeifeiRequest } from './http-client.js'
type JsonObject = Record<string, any>
export async function listKuaishouFeifeiProducts(input: {
page?: number
perPage?: number
status?: string
supplyProductName?: string
} = {}) {
const page = Math.max(1, Number(input.page || 1) || 1)
const perPage = Math.min(100, Math.max(1, Number(input.perPage || 20) || 20))
const filters: JsonObject = {}
const status = String(input.status || 'on_sale').trim()
const supplyProductName = String(input.supplyProductName || '').trim()
if (status) filters.status = status
if (supplyProductName) filters.supply_product_name = supplyProductName
const response = await kuaishouFeifeiRequest('/api/open/v1/products/index', {
page,
per_page: perPage,
filters,
})
return mapKuaishouFeifeiProductList(response.data)
}
export async function createKuaishouFeifeiOrder(input: {
platformOrderNo: string
productCode: string
@@ -73,3 +97,40 @@ export function mapKuaishouFeifeiOrder(value: unknown) {
raw: source,
}
}
export function mapKuaishouFeifeiProductList(value: unknown) {
const source = value && typeof value === 'object' && !Array.isArray(value)
? value as JsonObject
: {}
const list = Array.isArray(source.list) ? source.list : []
return {
items: list.map((item) => mapKuaishouFeifeiProduct(item)),
total: Number(source.total || list.length) || 0,
page: Number(source.page || 1) || 1,
perPage: Number(source.per_page || source.perPage || list.length || 20) || 20,
raw: source,
}
}
export function mapKuaishouFeifeiProduct(value: unknown) {
const source = value && typeof value === 'object' && !Array.isArray(value)
? value as JsonObject
: {}
return {
productCode: String(source.product_code || '').trim(),
name: String(source.name || '').trim(),
channel: String(source.channel || '').trim(),
externalProductId: String(source.external_product_id || '').trim(),
imageUrl: String(source.image_url || '').trim(),
status: String(source.status || '').trim(),
supplyStatus: String(source.supply_status || '').trim(),
supplyPricePoints: Number(source.supply_price_points || 0) || 0,
feePoints: Number(source.fee_points || 0) || 0,
unitCostPoints: Number(source.unit_cost_points || 0) || 0,
maxOrderQuantity: Number(source.max_order_quantity || 0) || 0,
salePricePoints: source.sale_price_points == null ? null : Number(source.sale_price_points || 0) || 0,
raw: source,
}
}