From 6af2fe2e23ec6266d3d3c65fa25ca9d9bb9363ab Mon Sep 17 00:00:00 2001 From: yml2213 Date: Tue, 7 Jul 2026 14:59:22 +0800 Subject: [PATCH] =?UTF-8?q?=E6=8B=86=E5=88=86feifei=E9=85=8D=E7=BD=AE?= =?UTF-8?q?=E5=B9=B6=E4=BF=AE=E5=A4=8D=E5=95=86=E5=93=81=E6=9F=A5=E8=AF=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../admin/platform-config/kuaishou-feifei.ts | 54 ++ .../kuaishou-feifei-service.ts | 79 ++ .../kuaishou-feifei/http-client.test.ts | 12 +- .../platforms/kuaishou-feifei/http-client.ts | 9 +- .../kuaishou-feifei/order-service.ts | 61 ++ apps/frontend/src/components.d.ts | 1 + .../admin/platform-config/kuaishou-feifei.ts | 42 + apps/frontend/src/types/admin/index.ts | 3 + .../src/types/admin/platform-config/index.ts | 3 + .../admin/platform-config/kuaishou-feifei.ts | 45 + .../AdminKuaishouFeifeiFulfillmentView.vue | 409 +++++---- .../platform-shops/AdminPlatformShopsView.vue | 6 +- .../AdminPlatformKuaishouFeifeiSection.vue | 814 ++++++++++++++++++ 13 files changed, 1317 insertions(+), 221 deletions(-) create mode 100644 apps/frontend/src/views/admin/platform-shops/components/kuaishou-feifei/AdminPlatformKuaishouFeifeiSection.vue diff --git a/apps/backend/src/routes/admin/platform-config/kuaishou-feifei.ts b/apps/backend/src/routes/admin/platform-config/kuaishou-feifei.ts index 56390a85..93c43546 100644 --- a/apps/backend/src/routes/admin/platform-config/kuaishou-feifei.ts +++ b/apps/backend/src/routes/admin/platform-config/kuaishou-feifei.ts @@ -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; diff --git a/apps/backend/src/services/admin/platform-config/kuaishou-feifei-service.ts b/apps/backend/src/services/admin/platform-config/kuaishou-feifei-service.ts index ef45adff..04bea363 100644 --- a/apps/backend/src/services/admin/platform-config/kuaishou-feifei-service.ts +++ b/apps/backend/src/services/admin/platform-config/kuaishou-feifei-service.ts @@ -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 @@ -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[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 { 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) +}) diff --git a/apps/backend/src/services/platforms/kuaishou-feifei/http-client.ts b/apps/backend/src/services/platforms/kuaishou-feifei/http-client.ts index ff3f037e..f876ca9c 100644 --- a/apps/backend/src/services/platforms/kuaishou-feifei/http-client.ts +++ b/apps/backend/src/services/platforms/kuaishou-feifei/http-client.ts @@ -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 || '{}') diff --git a/apps/backend/src/services/platforms/kuaishou-feifei/order-service.ts b/apps/backend/src/services/platforms/kuaishou-feifei/order-service.ts index 602938da..c2d72470 100644 --- a/apps/backend/src/services/platforms/kuaishou-feifei/order-service.ts +++ b/apps/backend/src/services/platforms/kuaishou-feifei/order-service.ts @@ -3,6 +3,30 @@ import { kuaishouFeifeiRequest } from './http-client.js' type JsonObject = Record +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, + } +} diff --git a/apps/frontend/src/components.d.ts b/apps/frontend/src/components.d.ts index 16e1ba1f..86fba41d 100644 --- a/apps/frontend/src/components.d.ts +++ b/apps/frontend/src/components.d.ts @@ -33,6 +33,7 @@ declare module 'vue' { ElIcon: typeof import('element-plus/es')['ElIcon'] ElInput: typeof import('element-plus/es')['ElInput'] ElInputNumber: typeof import('element-plus/es')['ElInputNumber'] + ElLink: typeof import('element-plus/es')['ElLink'] ElMenu: typeof import('element-plus/es')['ElMenu'] ElMenuItem: typeof import('element-plus/es')['ElMenuItem'] ElOption: typeof import('element-plus/es')['ElOption'] diff --git a/apps/frontend/src/services/admin/platform-config/kuaishou-feifei.ts b/apps/frontend/src/services/admin/platform-config/kuaishou-feifei.ts index 2ec372c1..76282c0b 100644 --- a/apps/frontend/src/services/admin/platform-config/kuaishou-feifei.ts +++ b/apps/frontend/src/services/admin/platform-config/kuaishou-feifei.ts @@ -3,6 +3,8 @@ import type { AdminKuaishouFeifeiConfig, AdminKuaishouFeifeiConfigResponse, AdminKuaishouFeifeiMatchResult, + AdminKuaishouFeifeiOrderResult, + AdminKuaishouFeifeiProductListResult, } from '@/types/admin' export function fetchAdminKuaishouFeifeiConfig() { @@ -24,3 +26,43 @@ export function matchAdminKuaishouFeifeiProduct(productName: string) { { productName }, ) } + +export function fetchAdminKuaishouFeifeiProducts(payload: { + page?: number + perPage?: number + status?: string + supplyProductName?: string +}) { + return apiPost( + '/api/v1/admin/platform-config/kuaishou-feifei/products', + payload, + ) +} + +export function createAdminKuaishouFeifeiTestOrder(payload: { + platformOrderNo: string + productCode: string + platformBuyNum?: number + platformAmount?: number | null + playerAccount?: string + playerGameRegion?: string + playerGameSrv?: string + playerGameRole?: string + submitPlayer?: boolean + notifyUrl?: string +}) { + return apiPost( + '/api/v1/admin/platform-config/kuaishou-feifei/test-order', + payload, + ) +} + +export function queryAdminKuaishouFeifeiOrder(payload: { + platformOrderNo?: string + orderNo?: string +}) { + return apiPost( + '/api/v1/admin/platform-config/kuaishou-feifei/query-order', + payload, + ) +} diff --git a/apps/frontend/src/types/admin/index.ts b/apps/frontend/src/types/admin/index.ts index 26efc0a2..2a3b0d89 100644 --- a/apps/frontend/src/types/admin/index.ts +++ b/apps/frontend/src/types/admin/index.ts @@ -56,6 +56,9 @@ export type { AdminKuaishouFeifeiConfigResponse, AdminKuaishouFeifeiProductMatch, AdminKuaishouFeifeiMatchResult, + AdminKuaishouFeifeiProductItem, + AdminKuaishouFeifeiProductListResult, + AdminKuaishouFeifeiOrderResult, AdminCloudtentaclesSourceItem, AdminCloudtentaclesSourcesConfig, AdminCloudtentaclesSourceConfigResponse, diff --git a/apps/frontend/src/types/admin/platform-config/index.ts b/apps/frontend/src/types/admin/platform-config/index.ts index 87e8e93d..bd2ff06e 100644 --- a/apps/frontend/src/types/admin/platform-config/index.ts +++ b/apps/frontend/src/types/admin/platform-config/index.ts @@ -37,6 +37,9 @@ export type { AdminKuaishouFeifeiConfigResponse, AdminKuaishouFeifeiProductMatch, AdminKuaishouFeifeiMatchResult, + AdminKuaishouFeifeiProductItem, + AdminKuaishouFeifeiProductListResult, + AdminKuaishouFeifeiOrderResult, } from './kuaishou-feifei' export type { diff --git a/apps/frontend/src/types/admin/platform-config/kuaishou-feifei.ts b/apps/frontend/src/types/admin/platform-config/kuaishou-feifei.ts index d7c8434e..a36980ca 100644 --- a/apps/frontend/src/types/admin/platform-config/kuaishou-feifei.ts +++ b/apps/frontend/src/types/admin/platform-config/kuaishou-feifei.ts @@ -48,3 +48,48 @@ export interface AdminKuaishouFeifeiMatchResult { matched: boolean match: AdminKuaishouFeifeiProductMatch | null } + +export interface AdminKuaishouFeifeiProductItem { + productCode: string + name: string + channel: string + externalProductId: string + imageUrl: string + status: string + supplyStatus: string + supplyPricePoints: number + feePoints: number + unitCostPoints: number + maxOrderQuantity: number + salePricePoints: number | null + raw: Record +} + +export interface AdminKuaishouFeifeiProductListResult { + items: AdminKuaishouFeifeiProductItem[] + total: number + page: number + perPage: number + raw: Record +} + +export interface AdminKuaishouFeifeiOrderResult { + orderNo: string + platformOrderNo: string + productCode: string + productName: string + rechargeStatus: number + rechargeStatusLabel: string + pointsCharged: number + playerAccount: string + platformBuyNum: number + rechargeResultMessage: string + createdAt: string + updatedAt: string + rechargeFinishAt: string + h5: { + entryUrl: string + rechargeUrl: string + } + raw: Record +} diff --git a/apps/frontend/src/views/admin/fulfillment/kuaishou-feifei/AdminKuaishouFeifeiFulfillmentView.vue b/apps/frontend/src/views/admin/fulfillment/kuaishou-feifei/AdminKuaishouFeifeiFulfillmentView.vue index e5bbd84f..d7ee9a4f 100644 --- a/apps/frontend/src/views/admin/fulfillment/kuaishou-feifei/AdminKuaishouFeifeiFulfillmentView.vue +++ b/apps/frontend/src/views/admin/fulfillment/kuaishou-feifei/AdminKuaishouFeifeiFulfillmentView.vue @@ -11,7 +11,6 @@ import { import type { AdminKuaishouFeifeiConfig, AdminKuaishouFeifeiConfigResponse, - AdminKuaishouFeifeiEffectiveConfig, AdminKuaishouFeifeiMatchResult, AdminKuaishouFeifeiProductRule, } from '@/types/admin' @@ -26,16 +25,7 @@ const errorMessage = ref('') const filePath = ref('') const matchInput = ref('') const matchResult = ref(null) -const effective = ref({ - enabled: true, - baseUrl: '', - timeoutMs: 10000, - notifyUrl: '', - hasAppKey: false, - hasAppSecret: false, - productRuleCount: 0, -}) -const form = reactive({ +const sourceConfig = reactive({ enabled: true, baseUrl: '', appKey: '', @@ -47,8 +37,27 @@ const form = reactive({ const rules = ref([]) const enabledRuleCount = computed(() => rules.value.filter((rule) => rule.enabled !== false).length) -const credentialReady = computed(() => Boolean(form.appKey.trim() && form.appSecret.trim())) -const effectiveReady = computed(() => effective.value.enabled && effective.value.hasAppKey && effective.value.hasAppSecret) +const configuredRuleCount = computed( + () => rules.value.filter((rule) => rule.productName.trim() && rule.productCode.trim()).length, +) +const incompleteRuleCount = computed(() => Math.max(0, rules.value.length - configuredRuleCount.value)) +const metrics = computed(() => [ + { + label: '启用规则', + value: String(enabledRuleCount.value), + detail: `共 ${rules.value.length} 条`, + }, + { + label: '已配置映射', + value: String(configuredRuleCount.value), + detail: incompleteRuleCount.value > 0 ? `待补全 ${incompleteRuleCount.value} 条` : '无缺失', + }, + { + label: '配置文件', + value: filePath.value ? 'GUI' : '默认', + detail: filePath.value || 'data/kuaishou-feifei-config.json', + }, +]) onMounted(loadConfig) @@ -65,29 +74,29 @@ async function loadConfig() { const response = await fetchAdminKuaishouFeifeiConfig() hydrateConfig(response.data) } catch (error) { - errorMessage.value = error instanceof Error ? error.message : '读取 kuaishou-feifei 配置失败' + errorMessage.value = error instanceof Error ? error.message : '读取 kuaishou-feifei 履约配置失败' } finally { loading.value = false } } -async function saveConfig() { +async function saveRules() { saving.value = true errorMessage.value = '' try { const response = await saveAdminKuaishouFeifeiConfig(buildPayload()) hydrateConfig(response.data) - showSuccess(`kuaishou-feifei 配置已保存,启用规则 ${enabledRuleCount.value} 条`) + showSuccess(`kuaishou-feifei 履约规则已保存,启用 ${enabledRuleCount.value} 条`) } catch (error) { - errorMessage.value = error instanceof Error ? error.message : '保存 kuaishou-feifei 配置失败' + errorMessage.value = error instanceof Error ? error.message : '保存 kuaishou-feifei 履约配置失败' showError(errorMessage.value) } finally { saving.value = false } } -async function runMatchTest() { +async function previewMatch() { const productName = matchInput.value.trim() if (!productName) { showError('请输入 91 商品名') @@ -101,7 +110,7 @@ async function runMatchTest() { const response = await matchAdminKuaishouFeifeiProduct(productName) matchResult.value = response.data } catch (error) { - showError(error instanceof Error ? error.message : '匹配 kuaishou-feifei 商品失败') + showError(error instanceof Error ? error.message : '预览 kuaishou-feifei 映射失败') } finally { matching.value = false } @@ -109,26 +118,24 @@ async function runMatchTest() { function hydrateConfig(data: AdminKuaishouFeifeiConfigResponse) { filePath.value = data.filePath || '' - effective.value = data.effective - form.enabled = data.source.enabled !== false - form.baseUrl = data.source.baseUrl || '' - form.appKey = data.source.appKey || '' - form.appSecret = data.source.appSecret || '' - form.timeoutMs = Number(data.source.timeoutMs || 10000) || 10000 - form.notifyUrl = data.source.notifyUrl || '' - rules.value = Array.isArray(data.source.productRules) - ? data.source.productRules.map((rule) => normalizeEditableRule(rule)) - : [] + sourceConfig.enabled = data.source.enabled !== false + sourceConfig.baseUrl = data.source.baseUrl || '' + sourceConfig.appKey = data.source.appKey || '' + sourceConfig.appSecret = data.source.appSecret || '' + sourceConfig.timeoutMs = Number(data.source.timeoutMs || 10000) || 10000 + sourceConfig.notifyUrl = data.source.notifyUrl || '' + sourceConfig.productRules = Array.isArray(data.source.productRules) ? data.source.productRules : [] + rules.value = sourceConfig.productRules.map((rule) => normalizeEditableRule(rule)) } function buildPayload(): AdminKuaishouFeifeiConfig { return { - enabled: form.enabled, - baseUrl: form.baseUrl.trim(), - appKey: form.appKey.trim(), - appSecret: form.appSecret.trim(), - timeoutMs: Number(form.timeoutMs || 10000) || 10000, - notifyUrl: form.notifyUrl.trim(), + enabled: sourceConfig.enabled, + baseUrl: sourceConfig.baseUrl, + appKey: sourceConfig.appKey, + appSecret: sourceConfig.appSecret, + timeoutMs: Number(sourceConfig.timeoutMs || 10000) || 10000, + notifyUrl: sourceConfig.notifyUrl, productRules: rules.value.map((rule) => normalizeEditableRule(rule)), } } @@ -172,217 +179,180 @@ function createRuleId() {