拆分feifei配置并修复商品查询
This commit is contained in:
@@ -1,8 +1,11 @@
|
|||||||
import { Router } from "express";
|
import { Router } from "express";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
createAdminKuaishouFeifeiTestOrder,
|
||||||
getAdminKuaishouFeifeiConfig,
|
getAdminKuaishouFeifeiConfig,
|
||||||
|
listAdminKuaishouFeifeiProducts,
|
||||||
matchAdminKuaishouFeifeiProduct,
|
matchAdminKuaishouFeifeiProduct,
|
||||||
|
queryAdminKuaishouFeifeiTestOrder,
|
||||||
updateAdminKuaishouFeifeiConfig,
|
updateAdminKuaishouFeifeiConfig,
|
||||||
} from "../../../services/admin/platform-config/kuaishou-feifei-service.js";
|
} from "../../../services/admin/platform-config/kuaishou-feifei-service.js";
|
||||||
import { createJsonHandler } from "../session.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;
|
export default router;
|
||||||
|
|||||||
@@ -1,4 +1,9 @@
|
|||||||
import { getKuaishouFeifeiConfig } from '../../platforms/kuaishou-feifei/config.js'
|
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 { resolveKuaishouFeifeiProductByName } from '../../platforms/kuaishou-feifei/product-rule-service.js'
|
||||||
import {
|
import {
|
||||||
getKuaishouFeifeiConfigFilePath,
|
getKuaishouFeifeiConfigFilePath,
|
||||||
@@ -8,6 +13,7 @@ import {
|
|||||||
saveKuaishouFeifeiSourceConfig,
|
saveKuaishouFeifeiSourceConfig,
|
||||||
} from '../../platforms/kuaishou-feifei/source-config-service.js'
|
} from '../../platforms/kuaishou-feifei/source-config-service.js'
|
||||||
import { normalizeCloudtentaclesMatchName } from '../../order/cloudtentacles-match-utils.js'
|
import { normalizeCloudtentaclesMatchName } from '../../order/cloudtentacles-match-utils.js'
|
||||||
|
import { createHttpError } from '../../../utils/http.js'
|
||||||
|
|
||||||
type JsonObject = Record<string, any>
|
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() {
|
function getAdminEditableKuaishouFeifeiConfig() {
|
||||||
if (hasKuaishouFeifeiConfigFile()) {
|
if (hasKuaishouFeifeiConfigFile()) {
|
||||||
return getKuaishouFeifeiSourceConfig()
|
return getKuaishouFeifeiSourceConfig()
|
||||||
@@ -68,3 +138,12 @@ function mapEffectiveKuaishouFeifeiConfig(config: ReturnType<typeof getKuaishouF
|
|||||||
productRuleCount: config.productRules.length,
|
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 test from 'node:test'
|
||||||
import assert from 'node:assert/strict'
|
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', () => {
|
test('signKuaishouFeifeiPayload signs app key, timestamp and raw body with HMAC SHA256', () => {
|
||||||
const input = {
|
const input = {
|
||||||
@@ -19,3 +22,10 @@ test('signKuaishouFeifeiPayload signs app key, timestamp and raw body with HMAC
|
|||||||
|
|
||||||
assert.equal(signKuaishouFeifeiPayload(input), expected)
|
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 业务失败'), {
|
throw createHttpError(String(json.message || 'kuaishou-feifei 业务失败'), {
|
||||||
statusCode: 502,
|
statusCode: 502,
|
||||||
errorCode: 'kuaishou_feifei_business_failed',
|
errorCode: 'kuaishou_feifei_business_failed',
|
||||||
@@ -85,6 +85,13 @@ export function signKuaishouFeifeiPayload({
|
|||||||
.toLowerCase()
|
.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 {
|
function parseJsonObject(text: string): JsonObject {
|
||||||
try {
|
try {
|
||||||
const parsed = JSON.parse(text || '{}')
|
const parsed = JSON.parse(text || '{}')
|
||||||
|
|||||||
@@ -3,6 +3,30 @@ import { kuaishouFeifeiRequest } from './http-client.js'
|
|||||||
|
|
||||||
type JsonObject = Record<string, any>
|
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: {
|
export async function createKuaishouFeifeiOrder(input: {
|
||||||
platformOrderNo: string
|
platformOrderNo: string
|
||||||
productCode: string
|
productCode: string
|
||||||
@@ -73,3 +97,40 @@ export function mapKuaishouFeifeiOrder(value: unknown) {
|
|||||||
raw: source,
|
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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Vendored
+1
@@ -33,6 +33,7 @@ declare module 'vue' {
|
|||||||
ElIcon: typeof import('element-plus/es')['ElIcon']
|
ElIcon: typeof import('element-plus/es')['ElIcon']
|
||||||
ElInput: typeof import('element-plus/es')['ElInput']
|
ElInput: typeof import('element-plus/es')['ElInput']
|
||||||
ElInputNumber: typeof import('element-plus/es')['ElInputNumber']
|
ElInputNumber: typeof import('element-plus/es')['ElInputNumber']
|
||||||
|
ElLink: typeof import('element-plus/es')['ElLink']
|
||||||
ElMenu: typeof import('element-plus/es')['ElMenu']
|
ElMenu: typeof import('element-plus/es')['ElMenu']
|
||||||
ElMenuItem: typeof import('element-plus/es')['ElMenuItem']
|
ElMenuItem: typeof import('element-plus/es')['ElMenuItem']
|
||||||
ElOption: typeof import('element-plus/es')['ElOption']
|
ElOption: typeof import('element-plus/es')['ElOption']
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ import type {
|
|||||||
AdminKuaishouFeifeiConfig,
|
AdminKuaishouFeifeiConfig,
|
||||||
AdminKuaishouFeifeiConfigResponse,
|
AdminKuaishouFeifeiConfigResponse,
|
||||||
AdminKuaishouFeifeiMatchResult,
|
AdminKuaishouFeifeiMatchResult,
|
||||||
|
AdminKuaishouFeifeiOrderResult,
|
||||||
|
AdminKuaishouFeifeiProductListResult,
|
||||||
} from '@/types/admin'
|
} from '@/types/admin'
|
||||||
|
|
||||||
export function fetchAdminKuaishouFeifeiConfig() {
|
export function fetchAdminKuaishouFeifeiConfig() {
|
||||||
@@ -24,3 +26,43 @@ export function matchAdminKuaishouFeifeiProduct(productName: string) {
|
|||||||
{ productName },
|
{ productName },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function fetchAdminKuaishouFeifeiProducts(payload: {
|
||||||
|
page?: number
|
||||||
|
perPage?: number
|
||||||
|
status?: string
|
||||||
|
supplyProductName?: string
|
||||||
|
}) {
|
||||||
|
return apiPost<AdminKuaishouFeifeiProductListResult>(
|
||||||
|
'/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<AdminKuaishouFeifeiOrderResult>(
|
||||||
|
'/api/v1/admin/platform-config/kuaishou-feifei/test-order',
|
||||||
|
payload,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function queryAdminKuaishouFeifeiOrder(payload: {
|
||||||
|
platformOrderNo?: string
|
||||||
|
orderNo?: string
|
||||||
|
}) {
|
||||||
|
return apiPost<AdminKuaishouFeifeiOrderResult>(
|
||||||
|
'/api/v1/admin/platform-config/kuaishou-feifei/query-order',
|
||||||
|
payload,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
@@ -56,6 +56,9 @@ export type {
|
|||||||
AdminKuaishouFeifeiConfigResponse,
|
AdminKuaishouFeifeiConfigResponse,
|
||||||
AdminKuaishouFeifeiProductMatch,
|
AdminKuaishouFeifeiProductMatch,
|
||||||
AdminKuaishouFeifeiMatchResult,
|
AdminKuaishouFeifeiMatchResult,
|
||||||
|
AdminKuaishouFeifeiProductItem,
|
||||||
|
AdminKuaishouFeifeiProductListResult,
|
||||||
|
AdminKuaishouFeifeiOrderResult,
|
||||||
AdminCloudtentaclesSourceItem,
|
AdminCloudtentaclesSourceItem,
|
||||||
AdminCloudtentaclesSourcesConfig,
|
AdminCloudtentaclesSourcesConfig,
|
||||||
AdminCloudtentaclesSourceConfigResponse,
|
AdminCloudtentaclesSourceConfigResponse,
|
||||||
|
|||||||
@@ -37,6 +37,9 @@ export type {
|
|||||||
AdminKuaishouFeifeiConfigResponse,
|
AdminKuaishouFeifeiConfigResponse,
|
||||||
AdminKuaishouFeifeiProductMatch,
|
AdminKuaishouFeifeiProductMatch,
|
||||||
AdminKuaishouFeifeiMatchResult,
|
AdminKuaishouFeifeiMatchResult,
|
||||||
|
AdminKuaishouFeifeiProductItem,
|
||||||
|
AdminKuaishouFeifeiProductListResult,
|
||||||
|
AdminKuaishouFeifeiOrderResult,
|
||||||
} from './kuaishou-feifei'
|
} from './kuaishou-feifei'
|
||||||
|
|
||||||
export type {
|
export type {
|
||||||
|
|||||||
@@ -48,3 +48,48 @@ export interface AdminKuaishouFeifeiMatchResult {
|
|||||||
matched: boolean
|
matched: boolean
|
||||||
match: AdminKuaishouFeifeiProductMatch | null
|
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<string, unknown>
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AdminKuaishouFeifeiProductListResult {
|
||||||
|
items: AdminKuaishouFeifeiProductItem[]
|
||||||
|
total: number
|
||||||
|
page: number
|
||||||
|
perPage: number
|
||||||
|
raw: Record<string, unknown>
|
||||||
|
}
|
||||||
|
|
||||||
|
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<string, unknown>
|
||||||
|
}
|
||||||
|
|||||||
+134
-159
@@ -11,7 +11,6 @@ import {
|
|||||||
import type {
|
import type {
|
||||||
AdminKuaishouFeifeiConfig,
|
AdminKuaishouFeifeiConfig,
|
||||||
AdminKuaishouFeifeiConfigResponse,
|
AdminKuaishouFeifeiConfigResponse,
|
||||||
AdminKuaishouFeifeiEffectiveConfig,
|
|
||||||
AdminKuaishouFeifeiMatchResult,
|
AdminKuaishouFeifeiMatchResult,
|
||||||
AdminKuaishouFeifeiProductRule,
|
AdminKuaishouFeifeiProductRule,
|
||||||
} from '@/types/admin'
|
} from '@/types/admin'
|
||||||
@@ -26,16 +25,7 @@ const errorMessage = ref('')
|
|||||||
const filePath = ref('')
|
const filePath = ref('')
|
||||||
const matchInput = ref('')
|
const matchInput = ref('')
|
||||||
const matchResult = ref<AdminKuaishouFeifeiMatchResult | null>(null)
|
const matchResult = ref<AdminKuaishouFeifeiMatchResult | null>(null)
|
||||||
const effective = ref<AdminKuaishouFeifeiEffectiveConfig>({
|
const sourceConfig = reactive<AdminKuaishouFeifeiConfig>({
|
||||||
enabled: true,
|
|
||||||
baseUrl: '',
|
|
||||||
timeoutMs: 10000,
|
|
||||||
notifyUrl: '',
|
|
||||||
hasAppKey: false,
|
|
||||||
hasAppSecret: false,
|
|
||||||
productRuleCount: 0,
|
|
||||||
})
|
|
||||||
const form = reactive<AdminKuaishouFeifeiConfig>({
|
|
||||||
enabled: true,
|
enabled: true,
|
||||||
baseUrl: '',
|
baseUrl: '',
|
||||||
appKey: '',
|
appKey: '',
|
||||||
@@ -47,8 +37,27 @@ const form = reactive<AdminKuaishouFeifeiConfig>({
|
|||||||
const rules = ref<EditableRule[]>([])
|
const rules = ref<EditableRule[]>([])
|
||||||
|
|
||||||
const enabledRuleCount = computed(() => rules.value.filter((rule) => rule.enabled !== false).length)
|
const enabledRuleCount = computed(() => rules.value.filter((rule) => rule.enabled !== false).length)
|
||||||
const credentialReady = computed(() => Boolean(form.appKey.trim() && form.appSecret.trim()))
|
const configuredRuleCount = computed(
|
||||||
const effectiveReady = computed(() => effective.value.enabled && effective.value.hasAppKey && effective.value.hasAppSecret)
|
() => 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)
|
onMounted(loadConfig)
|
||||||
|
|
||||||
@@ -65,29 +74,29 @@ async function loadConfig() {
|
|||||||
const response = await fetchAdminKuaishouFeifeiConfig()
|
const response = await fetchAdminKuaishouFeifeiConfig()
|
||||||
hydrateConfig(response.data)
|
hydrateConfig(response.data)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
errorMessage.value = error instanceof Error ? error.message : '读取 kuaishou-feifei 配置失败'
|
errorMessage.value = error instanceof Error ? error.message : '读取 kuaishou-feifei 履约配置失败'
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false
|
loading.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function saveConfig() {
|
async function saveRules() {
|
||||||
saving.value = true
|
saving.value = true
|
||||||
errorMessage.value = ''
|
errorMessage.value = ''
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await saveAdminKuaishouFeifeiConfig(buildPayload())
|
const response = await saveAdminKuaishouFeifeiConfig(buildPayload())
|
||||||
hydrateConfig(response.data)
|
hydrateConfig(response.data)
|
||||||
showSuccess(`kuaishou-feifei 配置已保存,启用规则 ${enabledRuleCount.value} 条`)
|
showSuccess(`kuaishou-feifei 履约规则已保存,启用 ${enabledRuleCount.value} 条`)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
errorMessage.value = error instanceof Error ? error.message : '保存 kuaishou-feifei 配置失败'
|
errorMessage.value = error instanceof Error ? error.message : '保存 kuaishou-feifei 履约配置失败'
|
||||||
showError(errorMessage.value)
|
showError(errorMessage.value)
|
||||||
} finally {
|
} finally {
|
||||||
saving.value = false
|
saving.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function runMatchTest() {
|
async function previewMatch() {
|
||||||
const productName = matchInput.value.trim()
|
const productName = matchInput.value.trim()
|
||||||
if (!productName) {
|
if (!productName) {
|
||||||
showError('请输入 91 商品名')
|
showError('请输入 91 商品名')
|
||||||
@@ -101,7 +110,7 @@ async function runMatchTest() {
|
|||||||
const response = await matchAdminKuaishouFeifeiProduct(productName)
|
const response = await matchAdminKuaishouFeifeiProduct(productName)
|
||||||
matchResult.value = response.data
|
matchResult.value = response.data
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
showError(error instanceof Error ? error.message : '匹配 kuaishou-feifei 商品失败')
|
showError(error instanceof Error ? error.message : '预览 kuaishou-feifei 映射失败')
|
||||||
} finally {
|
} finally {
|
||||||
matching.value = false
|
matching.value = false
|
||||||
}
|
}
|
||||||
@@ -109,26 +118,24 @@ async function runMatchTest() {
|
|||||||
|
|
||||||
function hydrateConfig(data: AdminKuaishouFeifeiConfigResponse) {
|
function hydrateConfig(data: AdminKuaishouFeifeiConfigResponse) {
|
||||||
filePath.value = data.filePath || ''
|
filePath.value = data.filePath || ''
|
||||||
effective.value = data.effective
|
sourceConfig.enabled = data.source.enabled !== false
|
||||||
form.enabled = data.source.enabled !== false
|
sourceConfig.baseUrl = data.source.baseUrl || ''
|
||||||
form.baseUrl = data.source.baseUrl || ''
|
sourceConfig.appKey = data.source.appKey || ''
|
||||||
form.appKey = data.source.appKey || ''
|
sourceConfig.appSecret = data.source.appSecret || ''
|
||||||
form.appSecret = data.source.appSecret || ''
|
sourceConfig.timeoutMs = Number(data.source.timeoutMs || 10000) || 10000
|
||||||
form.timeoutMs = Number(data.source.timeoutMs || 10000) || 10000
|
sourceConfig.notifyUrl = data.source.notifyUrl || ''
|
||||||
form.notifyUrl = data.source.notifyUrl || ''
|
sourceConfig.productRules = Array.isArray(data.source.productRules) ? data.source.productRules : []
|
||||||
rules.value = Array.isArray(data.source.productRules)
|
rules.value = sourceConfig.productRules.map((rule) => normalizeEditableRule(rule))
|
||||||
? data.source.productRules.map((rule) => normalizeEditableRule(rule))
|
|
||||||
: []
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildPayload(): AdminKuaishouFeifeiConfig {
|
function buildPayload(): AdminKuaishouFeifeiConfig {
|
||||||
return {
|
return {
|
||||||
enabled: form.enabled,
|
enabled: sourceConfig.enabled,
|
||||||
baseUrl: form.baseUrl.trim(),
|
baseUrl: sourceConfig.baseUrl,
|
||||||
appKey: form.appKey.trim(),
|
appKey: sourceConfig.appKey,
|
||||||
appSecret: form.appSecret.trim(),
|
appSecret: sourceConfig.appSecret,
|
||||||
timeoutMs: Number(form.timeoutMs || 10000) || 10000,
|
timeoutMs: Number(sourceConfig.timeoutMs || 10000) || 10000,
|
||||||
notifyUrl: form.notifyUrl.trim(),
|
notifyUrl: sourceConfig.notifyUrl,
|
||||||
productRules: rules.value.map((rule) => normalizeEditableRule(rule)),
|
productRules: rules.value.map((rule) => normalizeEditableRule(rule)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -172,7 +179,10 @@ function createRuleId() {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<section v-loading="loading" class="feifei-panel">
|
<section v-loading="loading" class="feifei-fulfillment-page">
|
||||||
|
<el-result v-if="!hasAdminRole('admin')" icon="warning" title="仅管理员可以查看履约配置" />
|
||||||
|
|
||||||
|
<template v-else>
|
||||||
<el-alert
|
<el-alert
|
||||||
v-if="errorMessage"
|
v-if="errorMessage"
|
||||||
type="error"
|
type="error"
|
||||||
@@ -181,87 +191,37 @@ function createRuleId() {
|
|||||||
:closable="false"
|
:closable="false"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div class="summary-grid">
|
<section class="rule-overview">
|
||||||
<div class="summary-item">
|
<div v-for="metric in metrics" :key="metric.label" class="metric-card">
|
||||||
<span>运行状态</span>
|
<span>{{ metric.label }}</span>
|
||||||
<strong>{{ effectiveReady ? '已就绪' : '待配置' }}</strong>
|
<strong>{{ metric.value }}</strong>
|
||||||
<p>{{ form.enabled ? '启用中' : '已停用' }}</p>
|
<small>{{ metric.detail }}</small>
|
||||||
</div>
|
</div>
|
||||||
<div class="summary-item">
|
<div class="overview-actions">
|
||||||
<span>商品规则</span>
|
<el-button :icon="RefreshRight" :loading="loading" @click="loadConfig">刷新</el-button>
|
||||||
<strong>{{ enabledRuleCount }}</strong>
|
<el-button type="primary" :icon="Check" :loading="saving" @click="saveRules">
|
||||||
<p>共 {{ rules.length }} 条</p>
|
保存规则
|
||||||
</div>
|
|
||||||
<div class="summary-item">
|
|
||||||
<span>凭据</span>
|
|
||||||
<strong>{{ credentialReady ? '已填写' : '缺失' }}</strong>
|
|
||||||
<p>生效配置 {{ effective.hasAppKey && effective.hasAppSecret ? '可用' : '待补全' }}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<section class="config-section">
|
|
||||||
<div class="section-head">
|
|
||||||
<div>
|
|
||||||
<h3>平台连接</h3>
|
|
||||||
<p>{{ filePath || 'data/kuaishou-feifei-config.json' }}</p>
|
|
||||||
</div>
|
|
||||||
<div class="section-actions">
|
|
||||||
<el-button :icon="RefreshRight" @click="loadConfig">刷新</el-button>
|
|
||||||
<el-button type="primary" :icon="Check" :loading="saving" @click="saveConfig">
|
|
||||||
保存配置
|
|
||||||
</el-button>
|
</el-button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<el-form label-position="top" class="config-form">
|
|
||||||
<el-form-item label="启用">
|
|
||||||
<el-switch
|
|
||||||
v-model="form.enabled"
|
|
||||||
inline-prompt
|
|
||||||
active-text="启用"
|
|
||||||
inactive-text="停用"
|
|
||||||
/>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="Base URL">
|
|
||||||
<el-input v-model="form.baseUrl" placeholder="http://skin-exchange.yiquyou.icu" />
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="App Key">
|
|
||||||
<el-input v-model="form.appKey" placeholder="kuaishou-feifei app key" />
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="App Secret">
|
|
||||||
<el-input
|
|
||||||
v-model="form.appSecret"
|
|
||||||
type="password"
|
|
||||||
show-password
|
|
||||||
placeholder="kuaishou-feifei app secret"
|
|
||||||
/>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="超时毫秒">
|
|
||||||
<el-input-number v-model="form.timeoutMs" :min="1000" :step="1000" controls-position="right" />
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="通知地址">
|
|
||||||
<el-input v-model="form.notifyUrl" placeholder="可选,feifei 回调通知地址" />
|
|
||||||
</el-form-item>
|
|
||||||
</el-form>
|
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="config-section">
|
<section class="rule-panel">
|
||||||
<div class="section-head">
|
<div class="section-head rule-head-title">
|
||||||
<div>
|
<div>
|
||||||
<h3>商品规则</h3>
|
<h3>商品履约规则</h3>
|
||||||
<p>91 商品名命中后会创建 kuaishou-feifei 履约任务。</p>
|
<p>91 商品名命中后会创建 kuaishou-feifei 履约任务。</p>
|
||||||
</div>
|
</div>
|
||||||
<el-button type="primary" plain :icon="Plus" @click="addRule">新增规则</el-button>
|
<el-button type="primary" plain :icon="Plus" @click="addRule">新增规则</el-button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="match-bar">
|
<div class="match-checker">
|
||||||
<el-input
|
<el-input
|
||||||
v-model="matchInput"
|
v-model="matchInput"
|
||||||
clearable
|
clearable
|
||||||
placeholder="输入 91 商品名,例如 套装-Alan Walker"
|
placeholder="91 商品名,例如 套装-Alan Walker"
|
||||||
@keyup.enter="runMatchTest"
|
@keyup.enter="previewMatch"
|
||||||
/>
|
/>
|
||||||
<el-button :icon="Search" :loading="matching" @click="runMatchTest">匹配测试</el-button>
|
<el-button :icon="Search" :loading="matching" @click="previewMatch">预览命中</el-button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="matchResult" class="match-result" :class="{ 'is-matched': matchResult.matched }">
|
<div v-if="matchResult" class="match-result" :class="{ 'is-matched': matchResult.matched }">
|
||||||
@@ -305,84 +265,94 @@ function createRuleId() {
|
|||||||
</article>
|
</article>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
</template>
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.feifei-panel {
|
.feifei-fulfillment-page {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: var(--space-4);
|
gap: var(--space-4);
|
||||||
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.summary-grid {
|
.rule-overview {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
grid-template-columns: repeat(3, minmax(0, 1fr)) minmax(150px, auto);
|
||||||
gap: var(--space-3);
|
gap: var(--space-3);
|
||||||
|
align-items: stretch;
|
||||||
}
|
}
|
||||||
|
|
||||||
.summary-item,
|
.metric-card,
|
||||||
.config-section,
|
.rule-panel,
|
||||||
.rule-item {
|
.rule-item {
|
||||||
border: 1px solid var(--el-border-color-light);
|
min-width: 0;
|
||||||
border-radius: 8px;
|
border: 1px solid var(--border-default);
|
||||||
background: var(--el-bg-color);
|
border-radius: var(--radius-md);
|
||||||
|
background: var(--bg-surface);
|
||||||
}
|
}
|
||||||
|
|
||||||
.summary-item {
|
.metric-card {
|
||||||
|
display: grid;
|
||||||
|
gap: var(--space-1);
|
||||||
padding: var(--space-4);
|
padding: var(--space-4);
|
||||||
}
|
}
|
||||||
|
|
||||||
.summary-item span,
|
.metric-card span,
|
||||||
.summary-item p,
|
.metric-card small,
|
||||||
.section-head p {
|
.section-head p {
|
||||||
color: var(--text-secondary);
|
color: var(--text-secondary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.summary-item strong {
|
.metric-card strong {
|
||||||
display: block;
|
color: var(--text-primary);
|
||||||
margin: 6px 0;
|
font-size: var(--text-2xl);
|
||||||
font-size: 22px;
|
line-height: 1.1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.summary-item p,
|
.overview-actions,
|
||||||
.section-head p {
|
.rule-head-title,
|
||||||
margin: 0;
|
.match-checker,
|
||||||
}
|
|
||||||
|
|
||||||
.config-section {
|
|
||||||
display: grid;
|
|
||||||
gap: var(--space-4);
|
|
||||||
padding: var(--space-4);
|
|
||||||
}
|
|
||||||
|
|
||||||
.section-head {
|
|
||||||
display: flex;
|
|
||||||
align-items: flex-start;
|
|
||||||
justify-content: space-between;
|
|
||||||
gap: var(--space-3);
|
|
||||||
}
|
|
||||||
|
|
||||||
.section-head h3 {
|
|
||||||
margin: 0 0 6px;
|
|
||||||
font-size: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.section-actions,
|
|
||||||
.match-bar,
|
|
||||||
.rule-head {
|
.rule-head {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: var(--space-2);
|
gap: var(--space-2);
|
||||||
}
|
}
|
||||||
|
|
||||||
.config-form,
|
.overview-actions {
|
||||||
.rule-form {
|
justify-content: flex-end;
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
||||||
gap: var(--space-3);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.match-bar {
|
.rule-panel,
|
||||||
|
.rule-item {
|
||||||
|
display: grid;
|
||||||
|
gap: var(--space-4);
|
||||||
|
padding: var(--space-4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-head {
|
||||||
|
display: grid;
|
||||||
|
gap: var(--space-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-head h3 {
|
||||||
|
margin: 0;
|
||||||
|
color: var(--text-primary);
|
||||||
|
font-size: var(--text-lg);
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-head p {
|
||||||
|
margin: 0;
|
||||||
|
font-size: var(--text-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.rule-head-title {
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.match-checker {
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -391,7 +361,7 @@ function createRuleId() {
|
|||||||
gap: var(--space-2);
|
gap: var(--space-2);
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 10px 12px;
|
padding: 10px 12px;
|
||||||
border-radius: 6px;
|
border-radius: var(--radius-sm);
|
||||||
background: var(--el-fill-color-light);
|
background: var(--el-fill-color-light);
|
||||||
color: var(--text-secondary);
|
color: var(--text-secondary);
|
||||||
}
|
}
|
||||||
@@ -406,25 +376,30 @@ function createRuleId() {
|
|||||||
gap: var(--space-3);
|
gap: var(--space-3);
|
||||||
}
|
}
|
||||||
|
|
||||||
.rule-item {
|
|
||||||
display: grid;
|
|
||||||
gap: var(--space-3);
|
|
||||||
padding: var(--space-3);
|
|
||||||
}
|
|
||||||
|
|
||||||
.rule-head {
|
.rule-head {
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.rule-form {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
gap: var(--space-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.rule-form :deep(.el-input),
|
||||||
|
.rule-form :deep(.el-select) {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: 900px) {
|
@media (max-width: 900px) {
|
||||||
.summary-grid,
|
.rule-overview,
|
||||||
.config-form,
|
|
||||||
.rule-form {
|
.rule-form {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
|
|
||||||
.section-head,
|
.overview-actions,
|
||||||
.match-bar {
|
.rule-head-title,
|
||||||
|
.match-checker {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,11 +12,11 @@ import {
|
|||||||
fetchAdminScheduledJobsConfig,
|
fetchAdminScheduledJobsConfig,
|
||||||
} from '@/services/admin'
|
} from '@/services/admin'
|
||||||
import { hasAdminRole } from '@/utils/admin-auth'
|
import { hasAdminRole } from '@/utils/admin-auth'
|
||||||
import AdminKuaishouFeifeiFulfillmentView from '../fulfillment/kuaishou-feifei/AdminKuaishouFeifeiFulfillmentView.vue'
|
|
||||||
import AdminPlatformCloudtentaclesSection from './components/cloudtentacles/AdminPlatformCloudtentaclesSection.vue'
|
import AdminPlatformCloudtentaclesSection from './components/cloudtentacles/AdminPlatformCloudtentaclesSection.vue'
|
||||||
import AdminPlatformNinetyoneSection from './components/AdminPlatformNinetyoneSection.vue'
|
import AdminPlatformNinetyoneSection from './components/AdminPlatformNinetyoneSection.vue'
|
||||||
import AdminPlatformNotificationSection from './components/notification/AdminPlatformNotificationSection.vue'
|
import AdminPlatformNotificationSection from './components/notification/AdminPlatformNotificationSection.vue'
|
||||||
import AdminPlatformKuaishouEticketSection from './components/kuaishou-eticket/AdminPlatformKuaishouEticketSection.vue'
|
import AdminPlatformKuaishouEticketSection from './components/kuaishou-eticket/AdminPlatformKuaishouEticketSection.vue'
|
||||||
|
import AdminPlatformKuaishouFeifeiSection from './components/kuaishou-feifei/AdminPlatformKuaishouFeifeiSection.vue'
|
||||||
import { useAdminCloudtentaclesPlatform } from './composables/useAdminCloudtentaclesPlatform'
|
import { useAdminCloudtentaclesPlatform } from './composables/useAdminCloudtentaclesPlatform'
|
||||||
import { useAdminNinetyonePlatform } from './composables/useAdminNinetyonePlatform'
|
import { useAdminNinetyonePlatform } from './composables/useAdminNinetyonePlatform'
|
||||||
import { useAdminNotificationPlatform } from './composables/useAdminNotificationPlatform'
|
import { useAdminNotificationPlatform } from './composables/useAdminNotificationPlatform'
|
||||||
@@ -392,8 +392,10 @@ onMounted(loadConfigs)
|
|||||||
:handle-kuaishou-eticket-consume="handleKuaishouEticketConsume"
|
:handle-kuaishou-eticket-consume="handleKuaishouEticketConsume"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<AdminKuaishouFeifeiFulfillmentView
|
<AdminPlatformKuaishouFeifeiSection
|
||||||
v-else-if="activePlatform === 'kuaishouFeifei'"
|
v-else-if="activePlatform === 'kuaishouFeifei'"
|
||||||
|
@loaded="kuaishouFeifeiConfig = $event"
|
||||||
|
@saved="kuaishouFeifeiConfig = $event"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<AdminPlatformCloudtentaclesSection
|
<AdminPlatformCloudtentaclesSection
|
||||||
|
|||||||
+814
@@ -0,0 +1,814 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, onMounted, reactive, ref } from 'vue'
|
||||||
|
import { Check, Plus, RefreshRight, Search } from '@element-plus/icons-vue'
|
||||||
|
|
||||||
|
import { showError, showSuccess } from '@/lib/feedback'
|
||||||
|
import {
|
||||||
|
createAdminKuaishouFeifeiTestOrder,
|
||||||
|
fetchAdminKuaishouFeifeiConfig,
|
||||||
|
fetchAdminKuaishouFeifeiProducts,
|
||||||
|
queryAdminKuaishouFeifeiOrder,
|
||||||
|
saveAdminKuaishouFeifeiConfig,
|
||||||
|
} from '@/services/admin'
|
||||||
|
import type {
|
||||||
|
AdminKuaishouFeifeiConfig,
|
||||||
|
AdminKuaishouFeifeiConfigResponse,
|
||||||
|
AdminKuaishouFeifeiEffectiveConfig,
|
||||||
|
AdminKuaishouFeifeiOrderResult,
|
||||||
|
AdminKuaishouFeifeiProductItem,
|
||||||
|
AdminKuaishouFeifeiProductRule,
|
||||||
|
} from '@/types/admin'
|
||||||
|
import { hasAdminRole } from '@/utils/admin-auth'
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
loaded: [data: AdminKuaishouFeifeiConfigResponse]
|
||||||
|
saved: [data: AdminKuaishouFeifeiConfigResponse]
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const loading = ref(true)
|
||||||
|
const saving = ref(false)
|
||||||
|
const productLoading = ref(false)
|
||||||
|
const orderCreating = ref(false)
|
||||||
|
const orderQuerying = ref(false)
|
||||||
|
const errorMessage = ref('')
|
||||||
|
const productErrorMessage = ref('')
|
||||||
|
const orderErrorMessage = ref('')
|
||||||
|
const filePath = ref('')
|
||||||
|
const products = ref<AdminKuaishouFeifeiProductItem[]>([])
|
||||||
|
const productTotal = ref(0)
|
||||||
|
const productRules = ref<AdminKuaishouFeifeiProductRule[]>([])
|
||||||
|
const lastOrder = ref<AdminKuaishouFeifeiOrderResult | null>(null)
|
||||||
|
const lastOrderAction = ref('')
|
||||||
|
const effective = ref<AdminKuaishouFeifeiEffectiveConfig>({
|
||||||
|
enabled: true,
|
||||||
|
baseUrl: '',
|
||||||
|
timeoutMs: 10000,
|
||||||
|
notifyUrl: '',
|
||||||
|
hasAppKey: false,
|
||||||
|
hasAppSecret: false,
|
||||||
|
productRuleCount: 0,
|
||||||
|
})
|
||||||
|
const form = reactive<Omit<AdminKuaishouFeifeiConfig, 'productRules'>>({
|
||||||
|
enabled: true,
|
||||||
|
baseUrl: '',
|
||||||
|
appKey: '',
|
||||||
|
appSecret: '',
|
||||||
|
timeoutMs: 10000,
|
||||||
|
notifyUrl: '',
|
||||||
|
})
|
||||||
|
const productQuery = reactive({
|
||||||
|
page: 1,
|
||||||
|
perPage: 20,
|
||||||
|
status: 'on_sale',
|
||||||
|
supplyProductName: '',
|
||||||
|
})
|
||||||
|
const testOrder = reactive({
|
||||||
|
platformOrderNo: createTestOrderNo(),
|
||||||
|
productCode: '',
|
||||||
|
platformBuyNum: 1,
|
||||||
|
platformAmount: null as number | null,
|
||||||
|
playerAccount: '',
|
||||||
|
playerGameRegion: '',
|
||||||
|
playerGameSrv: '',
|
||||||
|
playerGameRole: '',
|
||||||
|
submitPlayer: false,
|
||||||
|
notifyUrl: '',
|
||||||
|
})
|
||||||
|
const queryOrder = reactive({
|
||||||
|
platformOrderNo: '',
|
||||||
|
orderNo: '',
|
||||||
|
})
|
||||||
|
|
||||||
|
const credentialReady = computed(() => Boolean(form.appKey.trim() && form.appSecret.trim()))
|
||||||
|
const effectiveReady = computed(() => effective.value.enabled && effective.value.hasAppKey && effective.value.hasAppSecret)
|
||||||
|
const selectedProduct = computed(
|
||||||
|
() =>
|
||||||
|
products.value.find((product: AdminKuaishouFeifeiProductItem) => {
|
||||||
|
return product.productCode === testOrder.productCode
|
||||||
|
}) || null,
|
||||||
|
)
|
||||||
|
const lastOrderJson = computed(() => {
|
||||||
|
if (!lastOrder.value) {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
return JSON.stringify(lastOrder.value.raw || lastOrder.value, null, 2)
|
||||||
|
})
|
||||||
|
const metrics = computed(() => [
|
||||||
|
{
|
||||||
|
label: '运行状态',
|
||||||
|
value: effectiveReady.value ? '已就绪' : '待配置',
|
||||||
|
detail: form.enabled ? '启用中' : '已停用',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'API 凭据',
|
||||||
|
value: credentialReady.value ? '已填写' : '缺失',
|
||||||
|
detail: effective.value.hasAppKey && effective.value.hasAppSecret ? '生效配置可用' : '待补全',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '联调商品',
|
||||||
|
value: String(products.value.length),
|
||||||
|
detail: productTotal.value > 0 ? `总计 ${productTotal.value} 条` : '未加载',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '履约规则',
|
||||||
|
value: String(productRules.value.filter((rule) => rule.enabled !== false).length),
|
||||||
|
detail: `共 ${productRules.value.length} 条`,
|
||||||
|
},
|
||||||
|
])
|
||||||
|
|
||||||
|
onMounted(loadConfig)
|
||||||
|
|
||||||
|
async function loadConfig() {
|
||||||
|
if (!hasAdminRole('admin')) {
|
||||||
|
loading.value = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
loading.value = true
|
||||||
|
errorMessage.value = ''
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetchAdminKuaishouFeifeiConfig()
|
||||||
|
hydrateConfig(response.data)
|
||||||
|
emit('loaded', response.data)
|
||||||
|
} catch (error) {
|
||||||
|
errorMessage.value = error instanceof Error ? error.message : '读取 kuaishou-feifei 配置失败'
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveConfig() {
|
||||||
|
saving.value = true
|
||||||
|
errorMessage.value = ''
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await saveAdminKuaishouFeifeiConfig(buildPayload())
|
||||||
|
hydrateConfig(response.data)
|
||||||
|
emit('saved', response.data)
|
||||||
|
showSuccess('kuaishou-feifei 平台配置已保存')
|
||||||
|
} catch (error) {
|
||||||
|
errorMessage.value = error instanceof Error ? error.message : '保存 kuaishou-feifei 配置失败'
|
||||||
|
showError(errorMessage.value)
|
||||||
|
} finally {
|
||||||
|
saving.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadProducts() {
|
||||||
|
productLoading.value = true
|
||||||
|
productErrorMessage.value = ''
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetchAdminKuaishouFeifeiProducts({
|
||||||
|
page: productQuery.page,
|
||||||
|
perPage: productQuery.perPage,
|
||||||
|
status: productQuery.status,
|
||||||
|
supplyProductName: productQuery.supplyProductName.trim(),
|
||||||
|
})
|
||||||
|
products.value = Array.isArray(response.data.items) ? response.data.items : []
|
||||||
|
productTotal.value = Number(response.data.total || 0) || 0
|
||||||
|
} catch (error) {
|
||||||
|
products.value = []
|
||||||
|
productTotal.value = 0
|
||||||
|
productErrorMessage.value = error instanceof Error ? error.message : '查询 kuaishou-feifei 商品失败'
|
||||||
|
showError(productErrorMessage.value)
|
||||||
|
} finally {
|
||||||
|
productLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createTestOrder() {
|
||||||
|
if (!testOrder.platformOrderNo.trim()) {
|
||||||
|
showError('请输入测试业务单号')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!testOrder.productCode.trim()) {
|
||||||
|
showError('请输入 feifei 商品编码')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
orderCreating.value = true
|
||||||
|
orderErrorMessage.value = ''
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await createAdminKuaishouFeifeiTestOrder({
|
||||||
|
platformOrderNo: testOrder.platformOrderNo.trim(),
|
||||||
|
productCode: testOrder.productCode.trim(),
|
||||||
|
platformBuyNum: Number(testOrder.platformBuyNum || 1) || 1,
|
||||||
|
platformAmount: testOrder.platformAmount,
|
||||||
|
playerAccount: testOrder.playerAccount.trim(),
|
||||||
|
playerGameRegion: testOrder.playerGameRegion.trim(),
|
||||||
|
playerGameSrv: testOrder.playerGameSrv.trim(),
|
||||||
|
playerGameRole: testOrder.playerGameRole.trim(),
|
||||||
|
submitPlayer: testOrder.submitPlayer,
|
||||||
|
notifyUrl: testOrder.notifyUrl.trim(),
|
||||||
|
})
|
||||||
|
applyOrderResult(response.data, '创建结果')
|
||||||
|
showSuccess('kuaishou-feifei 测试单已创建')
|
||||||
|
} catch (error) {
|
||||||
|
orderErrorMessage.value = error instanceof Error ? error.message : '创建 kuaishou-feifei 测试单失败'
|
||||||
|
showError(orderErrorMessage.value)
|
||||||
|
} finally {
|
||||||
|
orderCreating.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function queryTestOrder() {
|
||||||
|
const platformOrderNo = queryOrder.platformOrderNo.trim() || testOrder.platformOrderNo.trim()
|
||||||
|
const orderNo = queryOrder.orderNo.trim()
|
||||||
|
|
||||||
|
if (!platformOrderNo && !orderNo) {
|
||||||
|
showError('请输入 feifei 平台单号或测试业务单号')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
orderQuerying.value = true
|
||||||
|
orderErrorMessage.value = ''
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await queryAdminKuaishouFeifeiOrder({
|
||||||
|
platformOrderNo,
|
||||||
|
orderNo,
|
||||||
|
})
|
||||||
|
applyOrderResult(response.data, '查询结果')
|
||||||
|
showSuccess('kuaishou-feifei 订单查询完成')
|
||||||
|
} catch (error) {
|
||||||
|
orderErrorMessage.value = error instanceof Error ? error.message : '查询 kuaishou-feifei 订单失败'
|
||||||
|
showError(orderErrorMessage.value)
|
||||||
|
} finally {
|
||||||
|
orderQuerying.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyOrderResult(order: AdminKuaishouFeifeiOrderResult, action: string) {
|
||||||
|
lastOrder.value = order
|
||||||
|
lastOrderAction.value = action
|
||||||
|
queryOrder.platformOrderNo = order.platformOrderNo || testOrder.platformOrderNo
|
||||||
|
queryOrder.orderNo = order.orderNo || queryOrder.orderNo
|
||||||
|
}
|
||||||
|
|
||||||
|
function useProductForTest(product: AdminKuaishouFeifeiProductItem) {
|
||||||
|
testOrder.productCode = product.productCode
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetTestOrderNo() {
|
||||||
|
testOrder.platformOrderNo = createTestOrderNo()
|
||||||
|
}
|
||||||
|
|
||||||
|
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 || ''
|
||||||
|
productRules.value = Array.isArray(data.source.productRules) ? data.source.productRules : []
|
||||||
|
if (!testOrder.notifyUrl) {
|
||||||
|
testOrder.notifyUrl = data.source.notifyUrl || ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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(),
|
||||||
|
productRules: productRules.value,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatPoints(value: unknown) {
|
||||||
|
const numberValue = Number(value)
|
||||||
|
return Number.isFinite(numberValue) ? numberValue.toLocaleString('zh-CN') : '-'
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatProductOption(product: AdminKuaishouFeifeiProductItem) {
|
||||||
|
return `${product.productCode} · ${product.name || '-'}`
|
||||||
|
}
|
||||||
|
|
||||||
|
function getProductTagType(product: AdminKuaishouFeifeiProductItem) {
|
||||||
|
return product.status === 'on_sale' && product.supplyStatus === 'on_sale' ? 'success' : 'warning'
|
||||||
|
}
|
||||||
|
|
||||||
|
function getOrderStatusType(status: unknown) {
|
||||||
|
const normalized = Number(status || 0)
|
||||||
|
if (normalized === 30) return 'success'
|
||||||
|
if ([40, 50, 60].includes(normalized)) return 'danger'
|
||||||
|
if ([17, 20].includes(normalized)) return 'warning'
|
||||||
|
return 'info'
|
||||||
|
}
|
||||||
|
|
||||||
|
function createTestOrderNo() {
|
||||||
|
const timestamp = new Date()
|
||||||
|
.toISOString()
|
||||||
|
.replace(/[-:TZ.]/g, '')
|
||||||
|
.slice(0, 14)
|
||||||
|
|
||||||
|
return `TEST-FEIFEI-${timestamp}-${Math.random().toString(36).slice(2, 6).toUpperCase()}`
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<section v-loading="loading" class="feifei-platform-panel">
|
||||||
|
<el-alert
|
||||||
|
v-if="errorMessage"
|
||||||
|
type="error"
|
||||||
|
:title="errorMessage"
|
||||||
|
show-icon
|
||||||
|
:closable="false"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<section class="feifei-overview">
|
||||||
|
<div v-for="metric in metrics" :key="metric.label" class="metric-card">
|
||||||
|
<span>{{ metric.label }}</span>
|
||||||
|
<strong>{{ metric.value }}</strong>
|
||||||
|
<small>{{ metric.detail }}</small>
|
||||||
|
</div>
|
||||||
|
<div class="overview-actions">
|
||||||
|
<el-button :icon="RefreshRight" :loading="loading" @click="loadConfig">刷新</el-button>
|
||||||
|
<el-button type="primary" :icon="Check" :loading="saving" @click="saveConfig">
|
||||||
|
保存平台配置
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="platform-layout">
|
||||||
|
<aside class="config-panel">
|
||||||
|
<div class="section-head">
|
||||||
|
<h3>平台连接</h3>
|
||||||
|
<p>{{ filePath || 'data/kuaishou-feifei-config.json' }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-form label-position="top" class="stack-form">
|
||||||
|
<el-form-item label="启用">
|
||||||
|
<el-switch
|
||||||
|
v-model="form.enabled"
|
||||||
|
inline-prompt
|
||||||
|
active-text="启用"
|
||||||
|
inactive-text="停用"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="Base URL">
|
||||||
|
<el-input v-model="form.baseUrl" placeholder="http://skin-exchange.yiquyou.icu" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="App Key">
|
||||||
|
<el-input v-model="form.appKey" placeholder="kuaishou-feifei app key" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="App Secret">
|
||||||
|
<el-input
|
||||||
|
v-model="form.appSecret"
|
||||||
|
type="password"
|
||||||
|
show-password
|
||||||
|
placeholder="kuaishou-feifei app secret"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="超时毫秒">
|
||||||
|
<el-input-number
|
||||||
|
v-model="form.timeoutMs"
|
||||||
|
:min="1000"
|
||||||
|
:step="1000"
|
||||||
|
controls-position="right"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="通知地址">
|
||||||
|
<el-input v-model="form.notifyUrl" placeholder="可选,feifei 回调通知地址" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<main class="test-panel">
|
||||||
|
<div class="section-head">
|
||||||
|
<h3>API 联调</h3>
|
||||||
|
<p>{{ effective.baseUrl || form.baseUrl || '-' }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-alert
|
||||||
|
v-if="productErrorMessage"
|
||||||
|
:title="productErrorMessage"
|
||||||
|
type="error"
|
||||||
|
show-icon
|
||||||
|
:closable="false"
|
||||||
|
/>
|
||||||
|
<el-alert
|
||||||
|
v-if="orderErrorMessage"
|
||||||
|
:title="orderErrorMessage"
|
||||||
|
type="error"
|
||||||
|
show-icon
|
||||||
|
:closable="false"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div class="product-toolbar">
|
||||||
|
<el-input
|
||||||
|
v-model="productQuery.supplyProductName"
|
||||||
|
clearable
|
||||||
|
placeholder="供货商品名称"
|
||||||
|
@keyup.enter="loadProducts"
|
||||||
|
/>
|
||||||
|
<el-select v-model="productQuery.status" class="status-select">
|
||||||
|
<el-option label="在售" value="on_sale" />
|
||||||
|
<el-option label="下架" value="off_sale" />
|
||||||
|
<el-option label="草稿" value="draft" />
|
||||||
|
<el-option label="全部" value="all" />
|
||||||
|
</el-select>
|
||||||
|
<el-input-number
|
||||||
|
v-model="productQuery.perPage"
|
||||||
|
:min="1"
|
||||||
|
:max="100"
|
||||||
|
controls-position="right"
|
||||||
|
/>
|
||||||
|
<el-button :icon="Search" :loading="productLoading" @click="loadProducts">
|
||||||
|
查询商品
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-table
|
||||||
|
v-loading="productLoading"
|
||||||
|
:data="products"
|
||||||
|
border
|
||||||
|
stripe
|
||||||
|
height="320"
|
||||||
|
empty-text="暂无商品"
|
||||||
|
class="product-table"
|
||||||
|
@row-click="useProductForTest"
|
||||||
|
>
|
||||||
|
<el-table-column label="商品" min-width="260">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<div class="product-cell">
|
||||||
|
<strong>{{ row.name || '-' }}</strong>
|
||||||
|
<small>{{ row.productCode || '-' }}</small>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="渠道" prop="channel" width="120" />
|
||||||
|
<el-table-column label="扣费积分" width="120">
|
||||||
|
<template #default="{ row }">{{ formatPoints(row.unitCostPoints) }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="单次数量" prop="maxOrderQuantity" width="110" />
|
||||||
|
<el-table-column label="状态" width="130">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tag :type="getProductTagType(row)" effect="light">
|
||||||
|
{{ row.status || '-' }} / {{ row.supplyStatus || '-' }}
|
||||||
|
</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="操作" width="110" fixed="right">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-button size="small" plain @click.stop="useProductForTest(row)">选用</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
|
||||||
|
<section class="order-grid">
|
||||||
|
<div class="order-panel">
|
||||||
|
<div class="section-head">
|
||||||
|
<h3>创建测试单</h3>
|
||||||
|
</div>
|
||||||
|
<el-form label-position="top" class="order-form">
|
||||||
|
<el-form-item label="测试业务单号">
|
||||||
|
<div class="inline-field">
|
||||||
|
<el-input v-model="testOrder.platformOrderNo" />
|
||||||
|
<el-button :icon="RefreshRight" @click="resetTestOrderNo" />
|
||||||
|
</div>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="feifei 商品编码">
|
||||||
|
<el-select
|
||||||
|
v-model="testOrder.productCode"
|
||||||
|
filterable
|
||||||
|
allow-create
|
||||||
|
default-first-option
|
||||||
|
placeholder="product_code"
|
||||||
|
>
|
||||||
|
<el-option
|
||||||
|
v-for="product in products"
|
||||||
|
:key="product.productCode"
|
||||||
|
:label="formatProductOption(product)"
|
||||||
|
:value="product.productCode"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="数量">
|
||||||
|
<el-input-number
|
||||||
|
v-model="testOrder.platformBuyNum"
|
||||||
|
:min="1"
|
||||||
|
:max="selectedProduct?.maxOrderQuantity || 999"
|
||||||
|
controls-position="right"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="记录金额">
|
||||||
|
<el-input-number
|
||||||
|
v-model="testOrder.platformAmount"
|
||||||
|
:min="0"
|
||||||
|
:precision="2"
|
||||||
|
controls-position="right"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="玩家 UID">
|
||||||
|
<el-input v-model="testOrder.playerAccount" clearable />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="区服-系统">
|
||||||
|
<el-input v-model="testOrder.playerGameRegion" clearable placeholder="安卓 / 苹果" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="区服-渠道">
|
||||||
|
<el-input v-model="testOrder.playerGameSrv" clearable placeholder="微信 / QQ" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="角色名">
|
||||||
|
<el-input v-model="testOrder.playerGameRole" clearable />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="直接提交玩家信息">
|
||||||
|
<el-switch v-model="testOrder.submitPlayer" active-text="提交" inactive-text="不提交" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="通知地址">
|
||||||
|
<el-input v-model="testOrder.notifyUrl" clearable />
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<el-button
|
||||||
|
type="primary"
|
||||||
|
:icon="Plus"
|
||||||
|
:loading="orderCreating"
|
||||||
|
@click="createTestOrder"
|
||||||
|
>
|
||||||
|
创建测试单
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="order-panel">
|
||||||
|
<div class="section-head">
|
||||||
|
<h3>查询订单</h3>
|
||||||
|
</div>
|
||||||
|
<el-form label-position="top" class="order-form">
|
||||||
|
<el-form-item label="测试业务单号">
|
||||||
|
<el-input v-model="queryOrder.platformOrderNo" clearable />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="feifei 平台单号">
|
||||||
|
<el-input v-model="queryOrder.orderNo" clearable />
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<el-button :icon="Search" :loading="orderQuerying" @click="queryTestOrder">
|
||||||
|
查询订单
|
||||||
|
</el-button>
|
||||||
|
|
||||||
|
<div v-if="lastOrder" class="order-result">
|
||||||
|
<div class="result-head">
|
||||||
|
<strong>{{ lastOrderAction || '订单结果' }}</strong>
|
||||||
|
<el-tag :type="getOrderStatusType(lastOrder.rechargeStatus)" effect="light">
|
||||||
|
{{ lastOrder.rechargeStatusLabel || lastOrder.rechargeStatus || '-' }}
|
||||||
|
</el-tag>
|
||||||
|
</div>
|
||||||
|
<dl>
|
||||||
|
<div>
|
||||||
|
<dt>平台单号</dt>
|
||||||
|
<dd>{{ lastOrder.orderNo || '-' }}</dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt>业务单号</dt>
|
||||||
|
<dd>{{ lastOrder.platformOrderNo || '-' }}</dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt>商品</dt>
|
||||||
|
<dd>{{ lastOrder.productName || lastOrder.productCode || '-' }}</dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt>扣除积分</dt>
|
||||||
|
<dd>{{ formatPoints(lastOrder.pointsCharged) }}</dd>
|
||||||
|
</div>
|
||||||
|
</dl>
|
||||||
|
<el-link
|
||||||
|
v-if="lastOrder.h5?.rechargeUrl"
|
||||||
|
:href="lastOrder.h5.rechargeUrl"
|
||||||
|
target="_blank"
|
||||||
|
type="primary"
|
||||||
|
>
|
||||||
|
打开领取链接
|
||||||
|
</el-link>
|
||||||
|
<pre v-if="lastOrderJson" class="json-preview">{{ lastOrderJson }}</pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
</section>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.feifei-platform-panel {
|
||||||
|
display: grid;
|
||||||
|
gap: var(--space-4);
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feifei-overview {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(4, minmax(0, 1fr)) minmax(160px, auto);
|
||||||
|
gap: var(--space-3);
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.metric-card,
|
||||||
|
.config-panel,
|
||||||
|
.test-panel,
|
||||||
|
.order-panel {
|
||||||
|
min-width: 0;
|
||||||
|
border: 1px solid var(--border-default);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
background: var(--bg-surface);
|
||||||
|
}
|
||||||
|
|
||||||
|
.metric-card {
|
||||||
|
display: grid;
|
||||||
|
gap: var(--space-1);
|
||||||
|
padding: var(--space-4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.metric-card span,
|
||||||
|
.metric-card small,
|
||||||
|
.section-head p,
|
||||||
|
.product-cell small,
|
||||||
|
.order-result dt {
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.metric-card strong {
|
||||||
|
color: var(--text-primary);
|
||||||
|
font-size: var(--text-2xl);
|
||||||
|
line-height: 1.1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.overview-actions,
|
||||||
|
.product-toolbar,
|
||||||
|
.inline-field,
|
||||||
|
.result-head {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.overview-actions {
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.platform-layout {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(280px, 340px) minmax(0, 1fr);
|
||||||
|
gap: var(--space-4);
|
||||||
|
align-items: start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-panel,
|
||||||
|
.test-panel,
|
||||||
|
.order-panel {
|
||||||
|
display: grid;
|
||||||
|
gap: var(--space-4);
|
||||||
|
padding: var(--space-4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-head {
|
||||||
|
display: grid;
|
||||||
|
gap: var(--space-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-head h3 {
|
||||||
|
margin: 0;
|
||||||
|
color: var(--text-primary);
|
||||||
|
font-size: var(--text-lg);
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-head p {
|
||||||
|
margin: 0;
|
||||||
|
font-size: var(--text-sm);
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stack-form,
|
||||||
|
.order-form {
|
||||||
|
display: grid;
|
||||||
|
gap: var(--space-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stack-form :deep(.el-input-number),
|
||||||
|
.order-form :deep(.el-input-number),
|
||||||
|
.order-form :deep(.el-select) {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-toolbar,
|
||||||
|
.inline-field {
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-select {
|
||||||
|
width: 120px;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-table {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-cell {
|
||||||
|
display: grid;
|
||||||
|
gap: 4px;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-cell strong,
|
||||||
|
.product-cell small {
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(0, 1.1fr) minmax(320px, 0.9fr);
|
||||||
|
gap: var(--space-4);
|
||||||
|
align-items: start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-form {
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
}
|
||||||
|
|
||||||
|
.inline-field .el-button {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-result {
|
||||||
|
display: grid;
|
||||||
|
gap: var(--space-3);
|
||||||
|
padding-top: var(--space-2);
|
||||||
|
border-top: 1px solid var(--border-default);
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-head {
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-result dl {
|
||||||
|
display: grid;
|
||||||
|
gap: var(--space-2);
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-result dl div {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 88px minmax(0, 1fr);
|
||||||
|
gap: var(--space-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-result dt,
|
||||||
|
.order-result dd {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-result dd {
|
||||||
|
min-width: 0;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
.json-preview {
|
||||||
|
max-height: 220px;
|
||||||
|
margin: 0;
|
||||||
|
padding: var(--space-3);
|
||||||
|
overflow: auto;
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
background: var(--el-fill-color-light);
|
||||||
|
color: var(--text-primary);
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1180px) {
|
||||||
|
.feifei-overview,
|
||||||
|
.platform-layout,
|
||||||
|
.order-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.overview-actions {
|
||||||
|
justify-content: flex-start;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 760px) {
|
||||||
|
.product-toolbar,
|
||||||
|
.overview-actions {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-select,
|
||||||
|
.order-form {
|
||||||
|
width: 100%;
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
Reference in New Issue
Block a user