diff --git a/apps/frontend/src/pages/admin/AdminKuaishouIndustryPage.tsx b/apps/frontend/src/pages/admin/AdminKuaishouIndustryPage.tsx index 191c2c1f..cb04d9c5 100644 --- a/apps/frontend/src/pages/admin/AdminKuaishouIndustryPage.tsx +++ b/apps/frontend/src/pages/admin/AdminKuaishouIndustryPage.tsx @@ -10,6 +10,8 @@ import { Alert, Button, Card, + Collapse, + Descriptions, Input, InputNumber, Select, @@ -57,6 +59,31 @@ import { stringifyDisplayJson } from '@/utils/date-time' type DateRangeValue = [Dayjs | null, Dayjs | null] | null type IndustryTab = 'vouchers' | 'refunds' | 'result' +type IndustryActionScope = 'vouchers' | 'refunds' +type IndustryActionKind = + | 'check' + | 'consume' + | 'reverse' + | 'resend' + | 'refund-list' + | 'refund-approve' + | 'refund-disagree' + +type IndustryActionHighlight = { + label: string + value: string +} + +type IndustryActionResultView = { + kind: IndustryActionKind + scope: IndustryActionScope + title: string + success: boolean + summary: string + highlights: IndustryActionHighlight[] + raw: unknown + executedAt: string +} type VoucherFilterState = { oid: string @@ -129,8 +156,7 @@ export default function AdminKuaishouIndustryPage() { status: '10', negotiateStatus: '1', }) - const [lastResultTitle, setLastResultTitle] = useState('接口结果') - const [lastResult, setLastResult] = useState(null) + const [lastActionResult, setLastActionResult] = useState(null) const [refundRows, setRefundRows] = useState>>([]) const [industryShops, setIndustryShops] = useState([]) const refundShopOptions = useMemo( @@ -308,13 +334,13 @@ export default function AdminKuaishouIndustryPage() { } async function runCheckAvailable() { - await runOpenApiAction('检查电子凭证有效性', 'check', () => + await runOpenApiAction('check', '检查电子凭证有效性', 'check', () => checkAdminKuaishouIndustryVoucherAvailable(buildVoucherToolPayload()), ) } async function runReverse() { - await runOpenApiAction('电子凭证冲正回调', 'reverse', () => + await runOpenApiAction('reverse', '电子凭证冲正回调', 'reverse', () => reverseAdminKuaishouIndustryVoucher({ ...buildVoucherToolPayload(), reason: toolForm.reason || '后台手动冲正', @@ -327,13 +353,20 @@ export default function AdminKuaishouIndustryPage() { setActionLoading('consume') try { const response = await consumeAdminKuaishouIndustryVoucher(buildVoucherToolPayload()) - setLastResultTitle('手动核销') - setLastResult(response.data) - setActiveTab('result') + publishActionResult( + buildIndustryActionResultView('consume', '手动核销', response.data), + ) showSuccess('手动核销已完成') await loadVouchers() } catch (error) { - showError(error instanceof Error ? error.message : '手动核销失败') + const message = error instanceof Error ? error.message : '手动核销失败' + publishActionResult( + buildIndustryActionResultView('consume', '手动核销', { + success: false, + error: message, + }), + ) + showError(message) } finally { setActionLoading('') } @@ -343,9 +376,9 @@ export default function AdminKuaishouIndustryPage() { setActionLoading('resend') try { const response = await resendAdminKuaishouIndustryVoucherCode(buildVoucherToolPayload()) - setLastResultTitle('重发发码回调') - setLastResult(response.data) - setActiveTab('result') + publishActionResult( + buildIndustryActionResultView('resend', '重发发码回调', response.data), + ) if (response.data.success) { showSuccess('发码回调已重发') } else { @@ -353,7 +386,14 @@ export default function AdminKuaishouIndustryPage() { } await loadVouchers() } catch (error) { - showError(error instanceof Error ? error.message : '重发发码回调失败') + const message = error instanceof Error ? error.message : '重发发码回调失败' + publishActionResult( + buildIndustryActionResultView('resend', '重发发码回调', { + success: false, + error: message, + }), + ) + showError(message) } finally { setActionLoading('') } @@ -368,6 +408,7 @@ export default function AdminKuaishouIndustryPage() { } await runOpenApiAction( + 'refund-list', '售后单列表', 'refund-list', () => @@ -380,6 +421,7 @@ export default function AdminKuaishouIndustryPage() { (result) => { const rows = extractRefundRows(result.response) setRefundRows(rows) + return { refundCount: rows.length } }, ) } @@ -391,7 +433,7 @@ export default function AdminKuaishouIndustryPage() { return } - await runOpenApiAction('同意退款', 'refund-approve', () => + await runOpenApiAction('refund-approve', '同意退款', 'refund-approve', () => approveAdminKuaishouIndustryRefund({ ...approveForm, sellerId, @@ -407,7 +449,7 @@ export default function AdminKuaishouIndustryPage() { return } - await runOpenApiAction('不同意退款', 'refund-disagree', () => + await runOpenApiAction('refund-disagree', '不同意退款', 'refund-disagree', () => disagreeAdminKuaishouIndustryRefund({ ...disagreeForm, sellerId, @@ -417,30 +459,44 @@ export default function AdminKuaishouIndustryPage() { } async function runOpenApiAction( + kind: IndustryActionKind, title: string, loadingKey: string, action: () => Promise<{ data: AdminKuaishouIndustryOpenApiResult }>, - afterSuccess?: (result: AdminKuaishouIndustryOpenApiResult) => void, + afterSuccess?: ( + result: AdminKuaishouIndustryOpenApiResult, + ) => { refundCount?: number } | void, ) { setActionLoading(loadingKey) try { const response = await action() - setLastResultTitle(title) - setLastResult(response.data) - setActiveTab('result') - afterSuccess?.(response.data) + const meta = afterSuccess?.(response.data) || undefined + publishActionResult( + buildIndustryActionResultView(kind, title, response.data, meta || undefined), + ) if (response.data.success) { showSuccess(`${title}已执行`) } else { showError(response.data.error || `${title}返回失败`) } } catch (error) { - showError(error instanceof Error ? error.message : `${title}失败`) + const message = error instanceof Error ? error.message : `${title}失败` + publishActionResult( + buildIndustryActionResultView(kind, title, { + success: false, + error: message, + }), + ) + showError(message) } finally { setActionLoading('') } } + function publishActionResult(view: IndustryActionResultView) { + setLastActionResult(view) + } + function buildVoucherToolPayload(): AdminKuaishouIndustryVoucherToolPayload { const voucherCode = String(toolForm.voucherCode || '').trim() return { @@ -498,7 +554,7 @@ export default function AdminKuaishouIndustryPage() { : []), { key: 'result', - label: '接口结果', + label: lastActionResult ? `接口结果 · ${lastActionResult.title}` : '接口结果', children: renderResultTab(), }, ] @@ -697,6 +753,7 @@ export default function AdminKuaishouIndustryPage() { 重发发码 + {renderInlineActionResult('vouchers')} @@ -898,39 +955,124 @@ export default function AdminKuaishouIndustryPage() { + {renderInlineActionResult('refunds')} + + ) + } + + function renderInlineActionResult(scope: IndustryActionScope) { + if (!lastActionResult || lastActionResult.scope !== scope) { + return null + } + + return ( +
+ {renderActionResultBody(lastActionResult, { + compact: true, + onOpenRaw: () => setActiveTab('result'), + onClear: () => setLastActionResult(null), + })}
) } function renderResultTab() { - const summary = resolveOpenApiResultSummary(lastResult) - return ( } onClick={() => setLastResult(null)}> - 清空 - + lastActionResult ? ( + + ) : null } > - {lastResult ? ( - - -
{stringifyDisplayJson(lastResult)}
-
+ {lastActionResult ? ( + renderActionResultBody(lastActionResult, { compact: false }) ) : ( - + )}
) } + function renderActionResultBody( + result: IndustryActionResultView, + options: { + compact?: boolean + onOpenRaw?: () => void + onClear?: () => void + } = {}, + ) { + return ( + + + {result.title} + + {result.success ? '成功' : '失败'} + + + {formatAdminDateTime(result.executedAt)} + + + } + description={result.summary} + action={ + options.compact ? ( + + {options.onOpenRaw ? ( + + ) : null} + {options.onClear ? ( + + ) : null} + + ) : undefined + } + /> + + {result.highlights.length > 0 ? ( + ({ + key: item.label, + label: item.label, + children: item.value || '-', + }))} + /> + ) : null} + + {stringifyDisplayJson(result.raw)}, + }, + ]} + defaultActiveKey={options.compact ? [] : ['raw']} + /> + + ) + } + function updateToolForm(patch: Partial) { setToolForm((current) => ({ ...current, ...patch })) } @@ -997,43 +1139,261 @@ function extractRefundRows( : [] } -function resolveOpenApiResultSummary(value: unknown) { - const result = - value && typeof value === 'object' && !Array.isArray(value) - ? (value as Record) - : {} - const response = - result.response && typeof result.response === 'object' && !Array.isArray(result.response) - ? (result.response as Record) - : null - const success = Boolean(result.success) - const code = String(response?.code || response?.sub_code || '').trim() - const resultCode = String(response?.result || '').trim() - const message = String( - response?.error_msg || - response?.msg || - response?.sub_msg || - response?.message || - result.error || - '', - ).trim() - const status = Number(result.httpStatus || 0) || 0 - const durationMs = Number(result.durationMs || 0) || 0 - const markers = [ - status ? `HTTP ${status}` : '', - resultCode ? `result=${resultCode}` : '', - code ? `code=${code}` : '', - durationMs ? `${durationMs}ms` : '', - ].filter(Boolean) +function buildIndustryActionResultView( + kind: IndustryActionKind, + title: string, + raw: unknown, + meta?: { refundCount?: number }, +): IndustryActionResultView { + const scope: IndustryActionScope = kind.startsWith('refund') ? 'refunds' : 'vouchers' + const record = asRecord(raw) + const openApi = asOpenApiShape(record) + const voucher = asRecord(record.voucher) + const task = asRecord(record.task) + const response = openApi.response + const responseData = asRecord(response?.data) + const success = resolveActionSuccess(kind, record, openApi) + const platformMessage = pickFirstString([ + response?.error_msg, + response?.msg, + response?.sub_msg, + response?.message, + openApi.error, + record.error, + ]) + const platformResult = pickFirstString([response?.result, response?.code, response?.sub_code]) + const highlights: IndustryActionHighlight[] = [] + + pushHighlight(highlights, '接口', title) + if (openApi.httpStatus) { + pushHighlight(highlights, 'HTTP', String(openApi.httpStatus)) + } + if (openApi.durationMs) { + pushHighlight(highlights, '耗时', `${openApi.durationMs}ms`) + } + if (platformResult) { + pushHighlight(highlights, '平台 result', platformResult) + } + if (openApi.skippedReason) { + pushHighlight(highlights, '跳过原因', openApi.skippedReason) + } + + if (kind === 'check') { + const etickets = extractAvailableEtickets(responseData) + pushHighlight(highlights, '可核销券数', String(etickets.length || 0)) + if (etickets[0]) { + pushHighlight( + highlights, + '首张券码', + pickFirstString([etickets[0].code, etickets[0].id]) || '-', + ) + pushHighlight(highlights, '首张数量', String(etickets[0].num || 1)) + } + } + + if (kind === 'consume' || kind === 'reverse' || kind === 'resend' || voucher) { + if (voucher) { + pushHighlight(highlights, '券码', String(voucher.voucherCode || '-')) + pushHighlight( + highlights, + '券状态', + getVoucherStatusLabel(String(voucher.status || '')), + ) + if (voucher.consumeSerialNum) { + pushHighlight(highlights, '核销序列号', String(voucher.consumeSerialNum)) + } + if (voucher.sendCallbackStatus) { + pushHighlight(highlights, '发码状态', String(voucher.sendCallbackStatus)) + } + if (voucher.sendCallbackLastError) { + pushHighlight(highlights, '发码错误', String(voucher.sendCallbackLastError)) + } + } + if (task) { + pushHighlight(highlights, '任务号', String(task.taskNo || task.taskId || '-')) + pushHighlight(highlights, '任务状态', String(task.status || '-')) + if (task.deliveryStatus) { + pushHighlight(highlights, '交付状态', String(task.deliveryStatus)) + } + } + } + + if (kind === 'refund-list') { + const count = + typeof meta?.refundCount === 'number' + ? meta.refundCount + : extractRefundRows(response).length + pushHighlight(highlights, '售后单数量', String(count)) + const pcursor = pickFirstString([ + responseData?.pcursor, + responseData?.cursor, + asRecord(response)?.pcursor, + ]) + if (pcursor) { + pushHighlight(highlights, '下一页游标', pcursor) + } + } + + if (kind === 'refund-approve' || kind === 'refund-disagree') { + const refundId = pickFirstString([ + responseData?.refundId, + asRecord(openApi.request)?.refundId, + ]) + if (refundId) { + pushHighlight(highlights, '退款单号', refundId) + } + } + + if (platformMessage) { + pushHighlight(highlights, '平台消息', platformMessage) + } return { + kind, + scope, + title, success, - title: success ? '接口返回成功' : '接口返回失败', - description: - [markers.join(' / '), message].filter(Boolean).join(',') || '请查看下方原始响应。', + summary: buildActionSummary(kind, success, { + platformMessage, + platformResult, + refundCount: meta?.refundCount, + voucherStatus: voucher ? getVoucherStatusLabel(String(voucher.status || '')) : '', + skippedReason: openApi.skippedReason, + }), + highlights, + raw, + executedAt: new Date().toISOString(), } } +function buildActionSummary( + kind: IndustryActionKind, + success: boolean, + context: { + platformMessage: string + platformResult: string + refundCount?: number + voucherStatus: string + skippedReason: string + }, +) { + if (context.skippedReason) { + return `请求被跳过:${context.skippedReason}${context.platformMessage ? `,${context.platformMessage}` : ''}` + } + + if (kind === 'check') { + return success + ? context.platformMessage || '平台返回券码有效,可继续核销。' + : context.platformMessage || '检查失败,请核对卖家、券码与电子凭证类型。' + } + + if (kind === 'consume') { + return success + ? `核销成功${context.voucherStatus ? `,券状态:${context.voucherStatus}` : ''}。` + : context.platformMessage || '核销失败。' + } + + if (kind === 'reverse') { + return success + ? `冲正成功${context.voucherStatus ? `,券状态已回写为 ${context.voucherStatus}` : ''}。` + : context.platformMessage || '冲正失败。' + } + + if (kind === 'resend') { + return success + ? '发码回调已重发,请确认发码状态是否更新为 success。' + : context.platformMessage || '重发发码失败。' + } + + if (kind === 'refund-list') { + return success + ? `查询成功,共 ${context.refundCount ?? 0} 条售后单,已填充列表。` + : context.platformMessage || '查询售后单失败。' + } + + if (kind === 'refund-approve') { + return success + ? context.platformMessage || '同意退款已提交。' + : context.platformMessage || '同意退款失败。' + } + + if (kind === 'refund-disagree') { + return success + ? context.platformMessage || '不同意退款已提交。' + : context.platformMessage || '不同意退款失败。' + } + + return ( + context.platformMessage || + (success ? '接口执行成功。' : '接口执行失败。') + + (context.platformResult ? `(result=${context.platformResult})` : '') + ) +} + +function resolveActionSuccess( + kind: IndustryActionKind, + record: Record, + openApi: ReturnType, +) { + if (typeof record.success === 'boolean') { + return record.success + } + if (kind === 'consume') { + return Boolean(record.voucher) + } + return openApi.success +} + +function asOpenApiShape(record: Record) { + const response = asRecord(record.response) + return { + success: Boolean(record.success), + error: String(record.error || '').trim(), + response, + request: asRecord(record.request), + durationMs: Number(record.durationMs || 0) || 0, + httpStatus: Number(record.httpStatus || 0) || 0, + skippedReason: String(record.skippedReason || '').trim(), + } +} + +function extractAvailableEtickets(data: Record | null) { + const candidates = [data?.etickets, data?.eTicketList, data?.eticketList, data?.list] + for (const candidate of candidates) { + if (!Array.isArray(candidate)) { + continue + } + return candidate + .map((item) => asRecord(item)) + .filter((item): item is Record => Boolean(item)) + } + return [] as Array> +} + +function asRecord(value: unknown): Record | null { + if (!value || typeof value !== 'object' || Array.isArray(value)) { + return null + } + return value as Record +} + +function pickFirstString(values: unknown[]) { + for (const value of values) { + const text = String(value ?? '').trim() + if (text) { + return text + } + } + return '' +} + +function pushHighlight(list: IndustryActionHighlight[], label: string, value: string) { + if (!value || value === '-') { + return + } + list.push({ label, value }) +} + function getVoucherStatusColor(status: string) { const normalized = String(status || '').toUpperCase() if (normalized === 'CONSUMED') return 'green' diff --git a/apps/frontend/src/styles/main.css b/apps/frontend/src/styles/main.css index 983ef714..b5a6fc84 100644 --- a/apps/frontend/src/styles/main.css +++ b/apps/frontend/src/styles/main.css @@ -324,6 +324,16 @@ select { margin-top: 2px; } +.kuaishou-industry-action-result { + margin-top: 14px; + padding-top: 14px; + border-top: 1px solid #edf0f5; +} + +.kuaishou-industry-action-result .json-preview { + max-height: 280px; +} + .task-action-hint { display: block; margin-top: 10px;