优化快手电子凭证:接口结果按动作内联结构化展示

- 执行后不再强制跳转到接口结果 Tab
- 券码/售后操作区直接展示成功失败摘要与关键字段
- 按检查/核销/冲正/发码/售后等接口类型解析展示
- 原始 JSON 折叠保留,可按需打开接口结果页
This commit is contained in:
yml2213
2026-07-10 16:19:03 +08:00
parent 9589561988
commit 80b8e383c7
2 changed files with 439 additions and 69 deletions
@@ -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<unknown>(null)
const [lastActionResult, setLastActionResult] = useState<IndustryActionResultView | null>(null)
const [refundRows, setRefundRows] = useState<Array<Record<string, unknown>>>([])
const [industryShops, setIndustryShops] = useState<AdminKuaishouIndustryShopConfig[]>([])
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() {
</Button>
</Space>
{renderInlineActionResult('vouchers')}
</Card>
</div>
</div>
@@ -898,39 +955,124 @@ export default function AdminKuaishouIndustryPage() {
</Button>
</Card>
</div>
{renderInlineActionResult('refunds')}
</div>
)
}
function renderInlineActionResult(scope: IndustryActionScope) {
if (!lastActionResult || lastActionResult.scope !== scope) {
return null
}
return (
<div className="kuaishou-industry-action-result">
{renderActionResultBody(lastActionResult, {
compact: true,
onOpenRaw: () => setActiveTab('result'),
onClear: () => setLastActionResult(null),
})}
</div>
)
}
function renderResultTab() {
const summary = resolveOpenApiResultSummary(lastResult)
return (
<Card
title={lastResultTitle}
title={lastActionResult?.title || '接口结果'}
extra={
<Button icon={<ReloadOutlined />} onClick={() => setLastResult(null)}>
</Button>
lastActionResult ? (
<Button icon={<ReloadOutlined />} onClick={() => setLastActionResult(null)}>
</Button>
) : null
}
>
{lastResult ? (
<Space direction="vertical" size={12} className="full-width">
<Alert
type={summary.success ? 'success' : 'error'}
showIcon
message={summary.title}
description={summary.description}
/>
<pre className="json-preview">{stringifyDisplayJson(lastResult)}</pre>
</Space>
{lastActionResult ? (
renderActionResultBody(lastActionResult, { compact: false })
) : (
<Alert type="info" showIcon message="暂无接口结果" />
<Alert
type="info"
showIcon
message="暂无接口结果"
description="在「券码操作」或「售后退款」执行接口后,结果会展示在对应操作区;这里保留完整原始响应。"
/>
)}
</Card>
)
}
function renderActionResultBody(
result: IndustryActionResultView,
options: {
compact?: boolean
onOpenRaw?: () => void
onClear?: () => void
} = {},
) {
return (
<Space direction="vertical" size={12} className="full-width">
<Alert
type={result.success ? 'success' : 'error'}
showIcon
message={
<Space wrap size={[8, 4]}>
<span>{result.title}</span>
<Tag color={result.success ? 'success' : 'error'}>
{result.success ? '成功' : '失败'}
</Tag>
<Typography.Text type="secondary" style={{ fontSize: 12 }}>
{formatAdminDateTime(result.executedAt)}
</Typography.Text>
</Space>
}
description={result.summary}
action={
options.compact ? (
<Space>
{options.onOpenRaw ? (
<Button size="small" type="link" onClick={options.onOpenRaw}>
</Button>
) : null}
{options.onClear ? (
<Button size="small" type="link" onClick={options.onClear}>
</Button>
) : null}
</Space>
) : undefined
}
/>
{result.highlights.length > 0 ? (
<Descriptions
size="small"
bordered
column={options.compact ? 1 : 2}
items={result.highlights.map((item) => ({
key: item.label,
label: item.label,
children: item.value || '-',
}))}
/>
) : null}
<Collapse
size="small"
items={[
{
key: 'raw',
label: '原始响应 JSON',
children: <pre className="json-preview">{stringifyDisplayJson(result.raw)}</pre>,
},
]}
defaultActiveKey={options.compact ? [] : ['raw']}
/>
</Space>
)
}
function updateToolForm(patch: Partial<AdminKuaishouIndustryVoucherToolPayload>) {
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<string, unknown>)
: {}
const response =
result.response && typeof result.response === 'object' && !Array.isArray(result.response)
? (result.response as Record<string, unknown>)
: 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<string, unknown>,
openApi: ReturnType<typeof asOpenApiShape>,
) {
if (typeof record.success === 'boolean') {
return record.success
}
if (kind === 'consume') {
return Boolean(record.voucher)
}
return openApi.success
}
function asOpenApiShape(record: Record<string, unknown>) {
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<string, unknown> | 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<string, unknown> => Boolean(item))
}
return [] as Array<Record<string, unknown>>
}
function asRecord(value: unknown): Record<string, unknown> | null {
if (!value || typeof value !== 'object' || Array.isArray(value)) {
return null
}
return value as Record<string, unknown>
}
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'
+10
View File
@@ -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;