From 78d6c2f8a115624e20245844170b0a389cf95885 Mon Sep 17 00:00:00 2001 From: yml2213 Date: Fri, 14 Aug 2026 15:14:34 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96=E7=94=B5=E5=AD=90=E5=87=AD?= =?UTF-8?q?=E8=AF=81=E6=93=8D=E4=BD=9C=E4=B8=8E=E6=B5=8B=E8=AF=95=E6=95=B0?= =?UTF-8?q?=E6=8D=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/backend/src/routes/admin/dev-mock.ts | 18 +++ .../src/services/dev-mock/dev-mock-service.ts | 123 ++++++++++++++++++ .../src/pages/admin/AdminDevMockPage.tsx | 67 +++++++++- .../pages/admin/AdminKuaishouIndustryPage.tsx | 40 ++++-- apps/frontend/src/services/admin/dev-mock.ts | 17 +++ apps/frontend/src/styles/main.css | 12 ++ 6 files changed, 264 insertions(+), 13 deletions(-) diff --git a/apps/backend/src/routes/admin/dev-mock.ts b/apps/backend/src/routes/admin/dev-mock.ts index 8992fbe1..c362d0e1 100644 --- a/apps/backend/src/routes/admin/dev-mock.ts +++ b/apps/backend/src/routes/admin/dev-mock.ts @@ -4,6 +4,7 @@ import { buildOpen91QueryMock, createAffiliateDashMockClaim, createFeifeiMockClaim, + createKuaishouIndustryVoucherMockData, createLewanMockClaim, getDevMockStatus, isDevMockEnabled, @@ -101,6 +102,23 @@ router.post( }), ) +router.post( + '/dev-mock/kuaishou-industry/vouchers', + createJsonHandler((req) => createKuaishouIndustryVoucherMockData(req.body || {}), { + successMessage: '快手电子凭证 Mock 已生成', + errorMessage: '生成快手电子凭证 Mock 失败', + scope: '[admin/dev-mock/kuaishou-industry/vouchers]', + audit: (_req, data) => ({ + action: 'dev_mock_create_kuaishou_industry_vouchers', + targetType: 'dev_mock', + targetId: String((data as { sellerId?: string })?.sellerId || ''), + data: { + createdCount: (data as { createdCount?: number })?.createdCount || 0, + }, + }), + }), +) + router.post( '/dev-mock/open91/query', createJsonHandler((req) => buildOpen91QueryMock(req.body || {}), { diff --git a/apps/backend/src/services/dev-mock/dev-mock-service.ts b/apps/backend/src/services/dev-mock/dev-mock-service.ts index 8dc27e6a..4d375ea6 100644 --- a/apps/backend/src/services/dev-mock/dev-mock-service.ts +++ b/apps/backend/src/services/dev-mock/dev-mock-service.ts @@ -7,6 +7,7 @@ import crypto from 'node:crypto' import { createOrder, findOrderByPlatformOrderId } from '../../repositories/order-repo.js' import { replaceOrderItems } from '../../repositories/order-item-repo.js' import { createTask, updateTask } from '../../repositories/task-repo.js' +import { upsertKuaishouIndustryVoucher } from '../../repositories/kuaishou-industry-voucher-repo.js' import { getFulfillmentProfileByKey, upsertFulfillmentProfile, @@ -15,6 +16,10 @@ import { buildClaimUrl, createTaskClaimToken } from '../claim/claim-service.js' import { assertValidClaimUid, normalizeClaimUid } from '../claim/claim-identity.js' import { OPEN_91_PLATFORM, OPEN_91_PROVIDER, assertOpen91Config } from '../open-91/config.js' import { parseOpen91ProductNo } from '../platforms/ninetyone/order-service.js' +import { + getKuaishouIndustrySourceConfig, + listKuaishouIndustryShopConfigs, +} from '../platforms/kuaishou-industry/source-config-service.js' import { isProductionLike } from '../../config/runtime-validation.js' import { createHttpError } from '../../utils/http.js' import { addHours, nowIso } from '../../utils/time.js' @@ -40,6 +45,16 @@ export type DevMockCreateResult = { open91QueryHint: string } +export type DevMockKuaishouIndustryVoucherResult = { + createdCount: number + sellerId: string + vouchers: Array<{ + voucherCode: string + oid: string + status: string + }> +} + type DeliveryItem = { cloudSkuId: number cloudSkuName: string @@ -554,6 +569,102 @@ export async function createAffiliateDashMockClaim(input: { }) } +/** + * 生成电子凭证列表测试数据,不调用快手接口。 + * 覆盖未使用、已核销、已销毁和发码失败等运营页面常见状态。 + */ +export async function createKuaishouIndustryVoucherMockData(input: { + sellerId?: unknown +} = {}): Promise { + assertDevMockEnabled() + + const now = nowIso() + const timestamp = Date.now() + const sellerId = resolveMockIndustrySellerId(input.sellerId) + const specimens = [ + { status: 'UNUSED', callbackStatus: 'success', productName: '周卡 VIP 尊享礼包' }, + { + status: 'UNUSED', + callbackStatus: 'failed', + callbackError: '开发 Mock 模拟发码回调失败', + productName: '幸运盲盒 限定款', + }, + { + status: 'CONSUMED', + callbackStatus: 'success', + consumeSerialNum: `CONSUME-MOCK-${timestamp}-3`, + consumedAt: now, + productName: '星轨通行证月卡', + }, + { status: 'UNUSED', callbackStatus: 'success', productName: '精英荣耀专属礼盒' }, + { + status: 'DESTROYED', + callbackStatus: 'success', + destroyedAt: now, + productName: '退款销毁测试券', + }, + { + status: 'CONSUMED', + callbackStatus: 'success', + consumeSerialNum: `CONSUME-MOCK-${timestamp}-6`, + consumedAt: now, + productName: '浪漫天命皮肤礼包', + }, + ] + + const vouchers: DevMockKuaishouIndustryVoucherResult['vouchers'] = [] + for (const [index, specimen] of specimens.entries()) { + const oid = `MOCK-KS-${timestamp}-${index + 1}` + const voucher = await upsertKuaishouIndustryVoucher({ + oid, + unitIndex: 1, + sellerId, + token: `mock-token-${timestamp}-${index + 1}`, + eticketType: 'GAME_OPEN_TICKET_CONSUME', + status: specimen.status, + validStartTime: timestamp - 60 * 60 * 1000, + validEndTime: timestamp + 7 * 24 * 60 * 60 * 1000, + consumeSerialNum: specimen.consumeSerialNum || '', + consumedAt: specimen.consumedAt || null, + destroyedAt: specimen.destroyedAt || null, + sendCallbackStatus: specimen.callbackStatus, + sendCallbackAttemptCount: specimen.callbackStatus === 'failed' ? 1 : 0, + sendCallbackLastError: specimen.callbackError || '', + sendCallbackResponseJson: { + mock: true, + success: specimen.callbackStatus === 'success', + }, + sendCallbackSentAt: now, + rawPayloadJson: { + source: 'dev_mock', + mock: true, + productName: specimen.productName, + }, + createdAt: now, + updatedAt: now, + }) + + if (!voucher) { + throw createHttpError('电子凭证 Mock 创建失败', { + statusCode: 500, + errorCode: 'dev_mock_kuaishou_industry_voucher_failed', + }) + } + + vouchers.push({ + voucherCode: voucher.voucher_code, + oid: voucher.oid, + status: voucher.status, + }) + } + + return { + createdCount: vouchers.length, + sellerId, + vouchers, + } +} + /** * 生成 91 查询请求体(带签名),可选对本机发起查询。 */ @@ -951,6 +1062,18 @@ function resolveMockUid(value: unknown, fallback: string) { return assertValidClaimUid(raw) } +function resolveMockIndustrySellerId(value: unknown): string { + const sellerId = String(value || '').trim() + if (sellerId) { + return sellerId + } + + const shop = listKuaishouIndustryShopConfigs(getKuaishouIndustrySourceConfig()).find( + (item) => item.enabled !== false && (item.sellerId || item.shopId), + ) + return String(shop?.sellerId || shop?.shopId || '141242642').trim() +} + function parseDeliveryItems(value: unknown): DeliveryItem[] { if (!value) { return [ diff --git a/apps/frontend/src/pages/admin/AdminDevMockPage.tsx b/apps/frontend/src/pages/admin/AdminDevMockPage.tsx index 3c4bfa37..9fd170ee 100644 --- a/apps/frontend/src/pages/admin/AdminDevMockPage.tsx +++ b/apps/frontend/src/pages/admin/AdminDevMockPage.tsx @@ -23,15 +23,19 @@ import { buildOpen91QueryDevMock, createAffiliateDashDevMock, createFeifeiDevMock, + createKuaishouIndustryVoucherDevMock, createLewanDevMock, fetchDevMockStatus, type DevMockCreateResult, + type DevMockKuaishouIndustryVoucherResult, type DevMockOpen91QueryResult, } from '@/services/admin' export default function AdminDevMockPage() { const { message } = App.useApp() const [lastCreate, setLastCreate] = useState(null) + const [lastVoucherCreate, setLastVoucherCreate] = + useState(null) const [lastQuery, setLastQuery] = useState(null) const statusQuery = useQuery({ @@ -76,6 +80,17 @@ export default function AdminDevMockPage() { }, }) + const voucherMutation = useMutation({ + mutationFn: createKuaishouIndustryVoucherDevMock, + onSuccess: (response) => { + setLastVoucherCreate(response.data) + message.success(`已生成 ${response.data.createdCount} 条电子凭证 Mock`) + }, + onError: (error) => { + message.error(error instanceof Error ? error.message : '生成失败') + }, + }) + const open91Mutation = useMutation({ mutationFn: buildOpen91QueryDevMock, onSuccess: (response) => { @@ -166,8 +181,17 @@ export default function AdminDevMockPage() { + + + voucherMutation.mutate(values)} + /> + + + - +
) : null} + {lastVoucherCreate ? ( + 查看电子凭证} + > + + 已为店铺 {lastVoucherCreate.sellerId} 写入 {lastVoucherCreate.createdCount} 条测试券码。 + + + {lastVoucherCreate.vouchers.map((voucher) => ( + + {voucher.status} · {voucher.voucherCode} · {voucher.oid} + + ))} + + + ) : null} + {lastQuery ? ( @@ -516,3 +558,26 @@ function AffiliateDashMockForm({ ) } + +function KuaishouIndustryVoucherMockForm({ + loading, + onSubmit, +}: { + loading: boolean + onSubmit: (values: { sellerId?: string }) => void +}) { + return ( +
+ + + + +
+ ) +} diff --git a/apps/frontend/src/pages/admin/AdminKuaishouIndustryPage.tsx b/apps/frontend/src/pages/admin/AdminKuaishouIndustryPage.tsx index e77f2d53..f5a34127 100644 --- a/apps/frontend/src/pages/admin/AdminKuaishouIndustryPage.tsx +++ b/apps/frontend/src/pages/admin/AdminKuaishouIndustryPage.tsx @@ -24,7 +24,7 @@ import { Typography, } from 'antd' import type { TableColumnsType, TabsProps } from 'antd' -import { useEffect, useMemo, useState } from 'react' +import { useEffect, useMemo, useRef, useState } from 'react' import { AdminRangePicker } from '@/components/admin/AdminDatePicker' import PageHeader from '@/components/admin/PageHeader' @@ -132,6 +132,8 @@ export default function AdminKuaishouIndustryPage() { page: 1, pageSize: 50, }) + // 异步接口返回后刷新列表时,仍使用用户最后一次输入的筛选条件。 + const voucherFiltersRef = useRef(voucherFilters) const [toolForm, setToolForm] = useState({ eticketType: '', consumeType: 'consume', @@ -419,8 +421,7 @@ export default function AdminKuaishouIndustryPage() { } async function loadVouchers(nextFilters: Partial = {}) { - const filters = { ...voucherFilters, ...nextFilters } - setVoucherFilters(filters) + const filters = updateVoucherFilters(nextFilters) setVoucherLoading(true) try { @@ -449,6 +450,24 @@ export default function AdminKuaishouIndustryPage() { } } + function updateVoucherFilters(patch: Partial): VoucherFilterState { + const nextFilters = { ...voucherFiltersRef.current, ...patch } + voucherFiltersRef.current = nextFilters + setVoucherFilters(nextFilters) + return nextFilters + } + + function clearVoucherFilters() { + void loadVouchers({ + oid: '', + voucherCode: '', + taskId: '', + sellerId: '', + status: '', + page: 1, + }) + } + function selectVoucher(row: AdminKuaishouIndustryVoucher) { setSelectedVoucher(row) showSuccess(`已选择券码 ${row.voucherCode}`) @@ -804,8 +823,7 @@ export default function AdminKuaishouIndustryPage() { placeholder="订单号 oid" value={voucherFilters.oid} onChange={(event) => - setVoucherFilters({ - ...voucherFilters, + updateVoucherFilters({ oid: event.target.value, }) } @@ -815,8 +833,7 @@ export default function AdminKuaishouIndustryPage() { placeholder="券码" value={voucherFilters.voucherCode} onChange={(event) => - setVoucherFilters({ - ...voucherFilters, + updateVoucherFilters({ voucherCode: event.target.value, }) } @@ -826,8 +843,7 @@ export default function AdminKuaishouIndustryPage() { placeholder="任务 ID" value={voucherFilters.taskId} onChange={(event) => - setVoucherFilters({ - ...voucherFilters, + updateVoucherFilters({ taskId: event.target.value, }) } @@ -854,8 +870,7 @@ export default function AdminKuaishouIndustryPage() { placeholder="店铺 ID" value={voucherFilters.sellerId} onChange={(event) => - setVoucherFilters({ - ...voucherFilters, + updateVoucherFilters({ sellerId: event.target.value, }) } @@ -871,7 +886,7 @@ export default function AdminKuaishouIndustryPage() { { label: '已核销', value: 'CONSUMED' }, { label: '已销毁', value: 'DESTROYED' }, ]} - onChange={(value) => setVoucherFilters({ ...voucherFilters, status: value || '' })} + onChange={(value) => updateVoucherFilters({ status: value || '' })} /> +
) diff --git a/apps/frontend/src/services/admin/dev-mock.ts b/apps/frontend/src/services/admin/dev-mock.ts index 826b0dc7..cd2376c3 100644 --- a/apps/frontend/src/services/admin/dev-mock.ts +++ b/apps/frontend/src/services/admin/dev-mock.ts @@ -35,6 +35,16 @@ export type DevMockOpen91QueryResult = { response: unknown } +export type DevMockKuaishouIndustryVoucherResult = { + createdCount: number + sellerId: string + vouchers: Array<{ + voucherCode: string + oid: string + status: string + }> +} + export function fetchDevMockStatus() { return apiGet('/api/v1/admin/dev-mock/status') } @@ -69,6 +79,13 @@ export function createAffiliateDashDevMock(payload: { return apiPost('/api/v1/admin/dev-mock/affiliate-dash', payload) } +export function createKuaishouIndustryVoucherDevMock(payload: { sellerId?: string } = {}) { + return apiPost( + '/api/v1/admin/dev-mock/kuaishou-industry/vouchers', + payload, + ) +} + export function buildOpen91QueryDevMock(payload: { orderNo: string execute?: boolean diff --git a/apps/frontend/src/styles/main.css b/apps/frontend/src/styles/main.css index 94dc1257..4d4731da 100644 --- a/apps/frontend/src/styles/main.css +++ b/apps/frontend/src/styles/main.css @@ -645,6 +645,13 @@ select { padding: 16px; } +.kuaishou-industry-tool-card { + /* 右侧操作区跟随管理内容滚动容器,始终保持在可视区域内。 */ + position: sticky; + top: 0; + align-self: start; +} + .kuaishou-industry-tool-section + .kuaishou-industry-tool-section { margin-top: 16px; padding-top: 16px; @@ -1725,6 +1732,11 @@ select { grid-template-columns: 1fr; } + .kuaishou-industry-tool-card { + /* 窄屏单列展示时不固定操作区,避免遮挡下方内容。 */ + position: static; + } + .manual-dispatch-grid, .fulfillment-overview-grid, .task-flow-grid {