Files
order_site/apps/frontend/src/pages/admin/AdminKuaishouIndustryPage.tsx
T

1978 lines
64 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import {
CheckCircleOutlined,
DeleteOutlined,
ReloadOutlined,
RollbackOutlined,
SearchOutlined,
SendOutlined,
SyncOutlined,
} from '@ant-design/icons'
import {
Alert,
Button,
Card,
Collapse,
Descriptions,
Input,
InputNumber,
Popconfirm,
Select,
Space,
Tabs,
Tag,
Typography,
} from 'antd'
import PercentTable from '@/components/common/PercentTable'
import type { TableColumnsType, TabsProps } from 'antd'
import { useEffect, useMemo, useRef, useState } from 'react'
import { AdminRangePicker } from '@/components/admin/AdminDatePicker'
import PageHeader from '@/components/admin/PageHeader'
import dayjs, { type Dayjs } from '@/lib/dayjs'
import { showError, showSuccess } from '@/lib/feedback'
import {
approveAdminKuaishouIndustryRefund,
checkAdminKuaishouIndustryVoucherAvailable,
consumeAdminKuaishouIndustryVoucher,
destroyAdminKuaishouIndustryVoucher,
disagreeAdminKuaishouIndustryRefund,
fetchAdminKuaishouIndustryShops,
fetchAdminKuaishouIndustryVouchers,
listAdminKuaishouIndustryRefunds,
resendAdminKuaishouIndustryVoucherCode,
reverseAdminKuaishouIndustryVoucher,
} from '@/services/admin'
import type {
AdminKuaishouIndustryOpenApiResult,
AdminKuaishouIndustryRefundApprovePayload,
AdminKuaishouIndustryRefundDisagreePayload,
AdminKuaishouIndustryRefundListPayload,
AdminKuaishouIndustryShopOption,
AdminKuaishouIndustryVoucher,
AdminKuaishouIndustryVoucherToolPayload,
} from '@/types/admin'
import { getAdminRole, hasAdminRole } from '@/utils/admin-auth'
import {
ADMIN_DEFAULT_PAGE_SIZE,
buildAdminLocalTablePagination,
buildAdminTablePagination,
} from '@/utils/admin-pagination'
import { formatAdminDateTime } from '@/utils/admin-time'
import { stringifyDisplayJson } from '@/utils/date-time'
type DateRangeValue = [Dayjs | null, Dayjs | null] | null
type IndustryTab = 'support-consume' | 'vouchers' | 'refunds' | 'result'
type IndustryActionScope = 'vouchers' | 'refunds'
type IndustryActionKind =
| 'check'
| 'consume'
| 'destroy'
| '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
voucherCode: string
taskId: string
sellerId: string
status: string
page: number
pageSize: number
}
type RefundListState = {
sellerId: string
orderId: string
type: string
pageSize: string
currentPage: string
pcursor: string
status: string
negotiateStatus: string
}
export default function AdminKuaishouIndustryPage() {
const isSupportOnly = getAdminRole() === 'support'
const canManageRefund = hasAdminRole('operator')
const [activeTab, setActiveTab] = useState<IndustryTab>(
isSupportOnly ? 'support-consume' : 'vouchers',
)
const [voucherLoading, setVoucherLoading] = useState(false)
const [actionLoading, setActionLoading] = useState('')
const [consumingVoucherId, setConsumingVoucherId] = useState<number | null>(null)
const [reversingVoucherId, setReversingVoucherId] = useState<number | null>(null)
const [vouchers, setVouchers] = useState<AdminKuaishouIndustryVoucher[]>([])
const [selectedVoucher, setSelectedVoucher] = useState<AdminKuaishouIndustryVoucher | null>(null)
const [voucherTotal, setVoucherTotal] = useState(0)
const [voucherFilters, setVoucherFilters] = useState<VoucherFilterState>({
oid: '',
voucherCode: '',
taskId: '',
sellerId: '',
status: '',
page: 1,
pageSize: 50,
})
// 异步接口返回后刷新列表时,仍使用用户最后一次输入的筛选条件。
const voucherFiltersRef = useRef(voucherFilters)
const [toolForm, setToolForm] = useState<AdminKuaishouIndustryVoucherToolPayload>({
eticketType: '',
consumeType: 'consume',
reason: 'USER_APPLY_REFUND',
})
const [refundDateRange, setRefundDateRange] = useState<DateRangeValue>(() => [
dayjs().subtract(1, 'day'),
dayjs(),
])
const [refundListForm, setRefundListForm] = useState<RefundListState>({
sellerId: '',
orderId: '',
type: '8',
pageSize: '50',
currentPage: '1',
pcursor: '',
status: '10',
negotiateStatus: '',
})
const [approveForm, setApproveForm] = useState<AdminKuaishouIndustryRefundApprovePayload>({
refundId: '',
refundAmount: '',
status: '',
negotiateStatus: '',
refundHandingWay: '10',
})
const [disagreeForm, setDisagreeForm] = useState<AdminKuaishouIndustryRefundDisagreePayload>({
refundId: '',
sellerDisagreeReason: '100',
sellerDisagreeDesc: '',
status: '10',
negotiateStatus: '1',
})
const [lastActionResult, setLastActionResult] = useState<IndustryActionResultView | null>(null)
const [refundRows, setRefundRows] = useState<Array<Record<string, unknown>>>([])
const [industryShops, setIndustryShops] = useState<AdminKuaishouIndustryShopOption[]>([])
const shopFilterOptions = useMemo(
() =>
industryShops
.filter((shop) => shop.sellerId)
.map((shop) => ({
label: formatShopOptionLabel(shop),
value: shop.sellerId,
})),
[industryShops],
)
const refundShopOptions = useMemo(
() =>
industryShops
.filter((shop) => shop.enabled !== false && shop.sellerId)
.map((shop) => ({
label: formatShopOptionLabel(shop),
value: shop.sellerId,
})),
[industryShops],
)
const shopNameBySellerId = useMemo(() => {
const map = new Map<string, string>()
for (const shop of industryShops) {
const name = String(shop.shopName || '').trim()
if (!name) {
continue
}
const sellerId = String(shop.sellerId || '').trim()
const shopId = String(shop.shopId || '').trim()
if (sellerId) {
map.set(sellerId, name)
}
if (shopId) {
map.set(shopId, name)
}
}
return map
}, [industryShops])
const columns = useMemo<TableColumnsType<AdminKuaishouIndustryVoucher>>(
() => [
{
title: '卡券号码',
width: 236,
render: (_, row) => (
<div className="cell-stack">
<Typography.Text
strong
copyable={Boolean(row.voucherCode)}
className="kuaishou-voucher-code-text"
title={row.voucherCode || undefined}
>
{row.voucherCode || '-'}
</Typography.Text>
<span className="muted kuaishou-voucher-meta">
发码:{formatAdminDateTime(row.sendCallbackSentAt)}
</span>
</div>
),
},
{
title: '订单商品',
width: 348,
// 主列自适应:订单号 + 商品名,避免额外「商品」列占宽
ellipsis: true,
render: (_, row) => {
const skuName = String(row.skuName || '').trim()
const skuCode = String(row.skuCode || '').trim()
const productLabel = skuName || (skuCode ? `编码 ${skuCode}` : '')
const taskLabel = row.taskNo
? `${row.taskNo}${row.taskId ? ` (#${row.taskId})` : ''}`
: row.taskId
? `#${row.taskId}`
: '-'
return (
<div className="cell-stack">
<Typography.Text
copyable={Boolean(row.oid)}
ellipsis={{ tooltip: row.oid || undefined }}
>
{row.oid || '-'}
</Typography.Text>
{productLabel ? (
<Typography.Text
ellipsis={{ tooltip: skuCode ? `${skuName || '-'}(${skuCode})` : productLabel }}
>
{productLabel}
</Typography.Text>
) : (
<span className="muted">未关联商品</span>
)}
<span className="muted">
任务 {taskLabel}
{row.orderId ? ` · 订单 #${row.orderId}` : ''}
</span>
</div>
)
},
},
{
title: '状态',
width: 148,
render: (_, row) => (
<div className="cell-stack">
<Tag color={getVoucherStatusColor(row.status)}>{getVoucherStatusLabel(row.status)}</Tag>
<Tag color={row.sendCallbackStatus === 'success' ? 'green' : 'orange'}>
发码:{row.sendCallbackStatus || '-'}
</Tag>
{row.eticketType ? (
<Typography.Text
type="secondary"
ellipsis={{ tooltip: row.eticketType }}
className="muted"
>
{row.eticketType}
</Typography.Text>
) : (
<span className="muted">类型未知</span>
)}
</div>
),
},
{
title: '店铺',
width: 128,
ellipsis: true,
render: (_, row) =>
renderShopCell(
row.shopName || shopNameBySellerId.get(String(row.sellerId || '').trim()) || '',
row.sellerId,
isSupportOnly ? '' : row.tokenMasked,
{ compact: true },
),
},
{
title: '核销',
width: 132,
render: (_, row) => {
const serial = String(row.consumeSerialNum || '').trim()
const consumedAt = formatAdminDateTime(row.consumedAt)
return (
<div className="cell-stack">
<span title={serial || undefined}>
{consumedAt !== '-' ? consumedAt : serial || '-'}
</span>
{serial && consumedAt !== '-' ? (
<Typography.Text type="secondary" ellipsis={{ tooltip: serial }} className="muted">
{serial}
</Typography.Text>
) : null}
</div>
)
},
},
{
title: '操作',
fixed: 'right' as const,
width: isSupportOnly ? 164 : 72,
render: (_: unknown, row: AdminKuaishouIndustryVoucher) => {
if (isSupportOnly) {
const canConsume = row.status === 'UNUSED'
const canReverse = row.status === 'CONSUMED'
return (
<Space size={4}>
<Popconfirm
title="确认核销该券码?"
description={row.voucherCode}
okText="核销"
cancelText="取消"
disabled={!canConsume}
onConfirm={() => consumeVoucherRow(row)}
>
<Button
size="small"
type="primary"
icon={<CheckCircleOutlined />}
disabled={!canConsume}
loading={consumingVoucherId === row.id}
>
核销
</Button>
</Popconfirm>
<Popconfirm
title="确认冲正该券码?"
description="冲正后券码将恢复为未使用状态。"
okText="冲正"
cancelText="取消"
disabled={!canReverse}
onConfirm={() => reverseVoucherRow(row)}
>
<Button
size="small"
danger
icon={<RollbackOutlined />}
disabled={!canReverse}
loading={reversingVoucherId === row.id}
>
冲正
</Button>
</Popconfirm>
</Space>
)
}
const isSelected = selectedVoucher?.id === row.id
return (
<Button
size="small"
type="primary"
ghost={!isSelected}
onClick={() => selectVoucher(row)}
>
{isSelected ? '已选' : '选择'}
</Button>
)
},
},
],
[
consumingVoucherId,
isSupportOnly,
reversingVoucherId,
selectedVoucher?.id,
shopNameBySellerId,
],
)
const refundColumns = useMemo<TableColumnsType<Record<string, unknown>>>(
() => [
{ title: '售后单', dataIndex: 'refundId', minWidth: 160 },
{ title: '订单', dataIndex: 'oid', minWidth: 160 },
{
title: '店铺',
dataIndex: 'sellerId',
minWidth: 180,
render: (_, row) => {
const sellerId = String(row.sellerId || '').trim()
return renderShopCell(shopNameBySellerId.get(sellerId) || '', sellerId)
},
},
{ title: '状态', dataIndex: 'status', width: 100 },
{ title: '协商', dataIndex: 'negotiateStatus', width: 100 },
{ title: '金额', dataIndex: 'refundFee', width: 100 },
{
title: '提交时间',
minWidth: 170,
render: (_, row) => formatTimestamp(row.submitTime || row.createTime),
},
{
title: '操作',
fixed: 'right',
width: 120,
render: (_, row) => (
<Button size="small" onClick={() => fillRefundAction(row)}>
填入
</Button>
),
},
],
[shopNameBySellerId],
)
useEffect(() => {
void loadIndustryShops()
void loadVouchers()
}, [])
async function loadIndustryShops() {
try {
const response = await fetchAdminKuaishouIndustryShops()
const shops = response.data.shops || []
setIndustryShops(shops)
const enabledSellerIds = shops
.filter((shop) => shop.enabled !== false && shop.sellerId)
.map((shop) => shop.sellerId)
if (enabledSellerIds.length === 1) {
applyDefaultSellerId(enabledSellerIds[0] as string)
}
} catch (error) {
// 店铺下拉失败不阻断列表,仅回退为手动输入店铺 ID
console.warn(error instanceof Error ? error.message : '读取店铺列表失败')
}
}
async function loadVouchers(nextFilters: Partial<VoucherFilterState> = {}) {
const filters = updateVoucherFilters(nextFilters)
setVoucherLoading(true)
try {
const response = await fetchAdminKuaishouIndustryVouchers({
oid: filters.oid.trim(),
voucherCode: filters.voucherCode.trim(),
taskId: filters.taskId.trim(),
sellerId: filters.sellerId.trim(),
status: filters.status,
page: filters.page,
pageSize: filters.pageSize,
})
const items = response.data.items
setVouchers(items)
setSelectedVoucher((current) => {
if (!current) {
return null
}
return items.find((item) => item.id === current.id) || current
})
setVoucherTotal(response.data.pagination.total)
} catch (error) {
showError(error instanceof Error ? error.message : '查询电子凭证失败')
} finally {
setVoucherLoading(false)
}
}
function updateVoucherFilters(patch: Partial<VoucherFilterState>): 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}`)
}
async function runCheckAvailable() {
await runOpenApiAction('check', '检查电子凭证有效性', 'check', () =>
checkAdminKuaishouIndustryVoucherAvailable(buildVoucherToolPayload()),
)
}
async function runReverse() {
await runOpenApiAction('reverse', '电子凭证冲正回调', 'reverse', () =>
reverseAdminKuaishouIndustryVoucher({
...buildVoucherToolPayload(),
reason: '后台手动冲正',
}),
)
await loadVouchers()
}
async function runConsume() {
setActionLoading('consume')
try {
const response = await consumeAdminKuaishouIndustryVoucher(buildVoucherToolPayload())
publishActionResult(buildIndustryActionResultView('consume', '手动核销', response.data))
showSuccess('手动核销已完成')
await loadVouchers()
} catch (error) {
const message = error instanceof Error ? error.message : '手动核销失败'
publishActionResult(
buildIndustryActionResultView('consume', '手动核销', {
success: false,
error: message,
}),
)
showError(message)
} finally {
setActionLoading('')
}
}
async function consumeVoucherRow(row: AdminKuaishouIndustryVoucher) {
if (row.status !== 'UNUSED') {
showError('仅未使用券码可核销')
return
}
setConsumingVoucherId(row.id)
setActionLoading('consume')
try {
const response = await consumeAdminKuaishouIndustryVoucher({
sellerId: row.sellerId,
oid: row.oid,
orderId: row.oid,
taskId: row.taskId || '',
voucherCode: row.voucherCode,
eticketType: row.eticketType || undefined,
consumeType: 'consume',
serialNum: row.consumeSerialNum || undefined,
etickets: [{ id: row.voucherCode, code: row.voucherCode, num: 1 }],
})
if (!isSupportOnly) {
publishActionResult(buildIndustryActionResultView('consume', '手动核销', response.data))
}
showSuccess(`券码 ${row.voucherCode} 已核销`)
await loadVouchers()
} catch (error) {
const message = error instanceof Error ? error.message : '核销失败'
if (!isSupportOnly) {
publishActionResult(
buildIndustryActionResultView('consume', '手动核销', {
success: false,
error: message,
}),
)
}
showError(message)
} finally {
setConsumingVoucherId(null)
setActionLoading('')
}
}
async function reverseVoucherRow(row: AdminKuaishouIndustryVoucher) {
if (row.status !== 'CONSUMED') {
showError('仅已核销券码可冲正')
return
}
setReversingVoucherId(row.id)
setActionLoading('reverse')
try {
const response = await reverseAdminKuaishouIndustryVoucher({
sellerId: row.sellerId,
oid: row.oid,
orderId: row.oid,
taskId: row.taskId || '',
voucherCode: row.voucherCode,
eticketType: row.eticketType || undefined,
reason: '客服手动冲正',
etickets: [{ id: row.voucherCode, code: row.voucherCode, num: 1 }],
})
if (response.data.success) {
showSuccess(`券码 ${row.voucherCode} 已冲正`)
} else {
showError(response.data.error || '冲正失败')
}
await loadVouchers()
} catch (error) {
showError(error instanceof Error ? error.message : '冲正失败')
} finally {
setReversingVoucherId(null)
setActionLoading('')
}
}
async function runResendCode() {
setActionLoading('resend')
try {
const response = await resendAdminKuaishouIndustryVoucherCode(buildVoucherToolPayload())
publishActionResult(buildIndustryActionResultView('resend', '重发发码回调', response.data))
if (response.data.success) {
showSuccess('发码回调已重发')
} else {
showError(response.data.error || '发码回调重发失败')
}
await loadVouchers()
} catch (error) {
const message = error instanceof Error ? error.message : '重发发码回调失败'
publishActionResult(
buildIndustryActionResultView('resend', '重发发码回调', {
success: false,
error: message,
}),
)
showError(message)
} finally {
setActionLoading('')
}
}
async function runDestroy() {
setActionLoading('destroy')
try {
const response = await destroyAdminKuaishouIndustryVoucher({
...buildVoucherToolPayload(),
reason: toolForm.reason || 'USER_APPLY_REFUND',
})
publishActionResult(buildIndustryActionResultView('destroy', '手动销毁', response.data))
showSuccess(
response.data.alreadyDestroyed
? `券码已是销毁状态,销毁回调已重试(${response.data.reason || 'USER_APPLY_REFUND'})`
: `券码已销毁(${response.data.reason || 'USER_APPLY_REFUND'})`,
)
await loadVouchers()
} catch (error) {
const message = error instanceof Error ? error.message : '手动销毁失败'
publishActionResult(
buildIndustryActionResultView('destroy', '手动销毁', {
success: false,
error: message,
}),
)
showError(message)
} finally {
setActionLoading('')
}
}
async function runRefundList() {
const [begin, end] = refundDateRange || []
const sellerId = resolveRefundSellerId(refundListForm.sellerId)
if (!sellerId && refundShopOptions.length > 1) {
showError('请先选择快手售后退款店铺')
return
}
await runOpenApiAction(
'refund-list',
'售后单列表',
'refund-list',
() =>
listAdminKuaishouIndustryRefunds({
...refundListForm,
sellerId,
beginTime: begin?.valueOf(),
endTime: end?.valueOf(),
} satisfies AdminKuaishouIndustryRefundListPayload),
(result) => {
const rows = extractRefundRows(result.response)
setRefundRows(rows)
return { refundCount: rows.length }
},
)
}
async function runApproveRefund() {
const sellerId = resolveRefundSellerId(approveForm.sellerId)
if (!sellerId && refundShopOptions.length > 1) {
showError('请先选择快手售后退款店铺')
return
}
await runOpenApiAction('refund-approve', '同意退款', 'refund-approve', () =>
approveAdminKuaishouIndustryRefund({
...approveForm,
sellerId,
orderId: approveForm.orderId || refundListForm.orderId,
}),
)
}
async function runDisagreeRefund() {
const sellerId = resolveRefundSellerId(disagreeForm.sellerId)
if (!sellerId && refundShopOptions.length > 1) {
showError('请先选择快手售后退款店铺')
return
}
await runOpenApiAction('refund-disagree', '不同意退款', 'refund-disagree', () =>
disagreeAdminKuaishouIndustryRefund({
...disagreeForm,
sellerId,
orderId: disagreeForm.orderId || refundListForm.orderId,
}),
)
}
async function runOpenApiAction(
kind: IndustryActionKind,
title: string,
loadingKey: string,
action: () => Promise<{ data: AdminKuaishouIndustryOpenApiResult }>,
afterSuccess?: (result: AdminKuaishouIndustryOpenApiResult) => { refundCount?: number } | void,
) {
setActionLoading(loadingKey)
try {
const response = await action()
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) {
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(
selectedVoucher ? selectedVoucher.voucherCode : toolForm.voucherCode || '',
).trim()
const oid = String(
selectedVoucher ? selectedVoucher.oid : toolForm.oid || toolForm.orderId || '',
).trim()
return {
...toolForm,
sellerId: selectedVoucher ? selectedVoucher.sellerId : toolForm.sellerId,
oid,
orderId: oid,
taskId: selectedVoucher ? selectedVoucher.taskId || '' : toolForm.taskId,
voucherCode,
eticketType: selectedVoucher ? selectedVoucher.eticketType || '' : toolForm.eticketType,
serialNum: toolForm.serialNum || selectedVoucher?.consumeSerialNum,
etickets: voucherCode ? [{ id: voucherCode, code: voucherCode, num: 1 }] : undefined,
}
}
function fillRefundAction(row: Record<string, unknown>) {
const refundId = String(row.refundId || '').trim()
const sellerId = String(row.sellerId || refundListForm.sellerId || '').trim()
const orderId = String(row.oid || row.orderId || refundListForm.orderId || '').trim()
const status = String(row.status || '').trim()
const negotiateStatus = String(row.negotiateStatus || '').trim()
setApproveForm((current) => ({
...current,
sellerId: sellerId || current.sellerId,
orderId: orderId || current.orderId,
refundId,
status,
negotiateStatus,
refundAmount: String(row.refundFee || current.refundAmount || ''),
refundHandingWay: String(row.handlingWay || current.refundHandingWay || ''),
}))
setDisagreeForm((current) => ({
...current,
sellerId: sellerId || current.sellerId,
orderId: orderId || current.orderId,
refundId,
status,
negotiateStatus: negotiateStatus || current.negotiateStatus,
}))
setRefundListForm((current) => ({
...current,
sellerId: sellerId || current.sellerId,
orderId: orderId || current.orderId,
}))
showSuccess('已填入退款操作区')
}
const tabItems: TabsProps['items'] = isSupportOnly
? [
{
key: 'support-consume',
label: '券码核销',
children: renderSupportConsumeTab(),
},
]
: [
{
key: 'vouchers',
label: '券码操作',
children: renderVoucherTab(),
},
...(canManageRefund
? [
{
key: 'refunds',
label: '售后退款',
children: renderRefundTab(),
},
]
: []),
{
key: 'result',
label: lastActionResult ? `接口结果 · ${lastActionResult.title}` : '接口结果',
children: renderResultTab(),
},
]
return (
<div className="page-stack kuaishou-industry-page">
<PageHeader
title="快手电子凭证"
description={
isSupportOnly
? '客服核销:查询本地券码并执行核销'
: canManageRefund
? '行业电子凭证接口工具与售后处理'
: '行业电子凭证接口工具'
}
/>
<Tabs
activeKey={activeTab}
onChange={(key) => setActiveTab(key as IndustryTab)}
items={tabItems}
/>
</div>
)
function renderVoucherFilters() {
return (
<Card>
<Space wrap className="filter-form">
<Input
allowClear
placeholder="订单号 oid"
value={voucherFilters.oid}
onChange={(event) =>
updateVoucherFilters({
oid: event.target.value,
})
}
/>
<Input
allowClear
placeholder="券码"
value={voucherFilters.voucherCode}
onChange={(event) =>
updateVoucherFilters({
voucherCode: event.target.value,
})
}
/>
<Input
allowClear
placeholder="任务 ID"
value={voucherFilters.taskId}
onChange={(event) =>
updateVoucherFilters({
taskId: event.target.value,
})
}
/>
{shopFilterOptions.length > 0 ? (
<Select
allowClear
showSearch
optionFilterProp="label"
placeholder="按店铺筛选"
style={{ minWidth: 240 }}
value={voucherFilters.sellerId || undefined}
options={shopFilterOptions}
onChange={(value) => {
void loadVouchers({
page: 1,
sellerId: String(value || '').trim(),
})
}}
/>
) : (
<Input
allowClear
placeholder="店铺 ID"
value={voucherFilters.sellerId}
onChange={(event) =>
updateVoucherFilters({
sellerId: event.target.value,
})
}
/>
)}
<Select
allowClear
placeholder="券码状态"
style={{ width: 132 }}
value={voucherFilters.status || undefined}
options={[
{ label: '未使用', value: 'UNUSED' },
{ label: '已核销', value: 'CONSUMED' },
{ label: '已销毁', value: 'DESTROYED' },
]}
onChange={(value) => updateVoucherFilters({ status: value || '' })}
/>
<Button
type="primary"
icon={<SearchOutlined />}
loading={voucherLoading}
onClick={() => loadVouchers({ page: 1 })}
>
查询
</Button>
<Button onClick={clearVoucherFilters}>清空筛选</Button>
</Space>
</Card>
)
}
function renderVoucherTable(scrollX?: number) {
return (
<PercentTable
rowKey="id"
size="middle"
tableLayout="fixed"
columns={columns}
dataSource={vouchers}
loading={voucherLoading}
rowClassName={(row) =>
selectedVoucher?.id === row.id ? 'kuaishou-industry-row-selected' : ''
}
// 列已压缩;仅在极窄容器时横向滚动
scroll={{ x: scrollX || 1064 }}
pagination={buildAdminTablePagination({
current: voucherFilters.page,
pageSize: voucherFilters.pageSize,
total: voucherTotal,
onChange: (page, pageSize) => {
void loadVouchers({ page, pageSize })
},
})}
/>
)
}
function renderSupportConsumeTab() {
return (
<div className="page-stack">
{renderVoucherFilters()}
<Card title="本地券码">{renderVoucherTable()}</Card>
</div>
)
}
function renderVoucherTab() {
const hasVoucherTarget = Boolean(
selectedVoucher ||
String(toolForm.voucherCode || '').trim() ||
String(toolForm.taskId || '').trim() ||
String(toolForm.oid || toolForm.orderId || '').trim(),
)
const selectedShopName = selectedVoucher
? selectedVoucher.shopName ||
shopNameBySellerId.get(String(selectedVoucher.sellerId || '').trim()) ||
'未命名店铺'
: ''
return (
<div className="page-stack">
{renderVoucherFilters()}
<div className="kuaishou-industry-split">
<Card title="本地券码" className="kuaishou-industry-voucher-card">
{renderVoucherTable(1064)}
</Card>
<Card title="券码操作" className="kuaishou-industry-tool-card">
<div className="kuaishou-industry-tool-section">
<div className="kuaishou-industry-tool-section-title">当前卡券</div>
{selectedVoucher ? (
<div className="kuaishou-industry-selected-voucher">
<div className="kuaishou-industry-selected-header">
<Typography.Text
strong
copyable={{ text: selectedVoucher.voucherCode }}
className="kuaishou-industry-selected-code"
>
{selectedVoucher.voucherCode}
</Typography.Text>
<Space size={4}>
<Tag color={getVoucherStatusColor(selectedVoucher.status)}>
{getVoucherStatusLabel(selectedVoucher.status)}
</Tag>
<Button type="link" size="small" onClick={() => setSelectedVoucher(null)}>
清除
</Button>
</Space>
</div>
<div className="kuaishou-industry-selected-meta">
<div>
<span>订单号</span>
<Typography.Text copyable={{ text: selectedVoucher.oid }}>
{selectedVoucher.oid || '-'}
</Typography.Text>
</div>
<div>
<span>任务</span>
<strong>{selectedVoucher.taskNo || selectedVoucher.taskId || '-'}</strong>
</div>
<div>
<span>店铺</span>
<strong>{selectedShopName}</strong>
<small>{selectedVoucher.sellerId || '-'}</small>
</div>
<div>
<span>凭证类型</span>
<strong title={selectedVoucher.eticketType || undefined}>
{selectedVoucher.eticketType || '-'}
</strong>
</div>
</div>
</div>
) : (
<div className="kuaishou-industry-selected-empty">
<strong>尚未选择卡券</strong>
<span>请从左侧列表选择,或在下方手动指定。</span>
</div>
)}
</div>
<div className="kuaishou-industry-tool-section">
<div className="kuaishou-industry-tool-section-title">操作参数</div>
<div className="kuaishou-industry-tool-grid">
<label className="kuaishou-industry-tool-field">
<span>核销类型</span>
<Input
placeholder="consume"
value={toolForm.consumeType}
onChange={(event) => updateToolForm({ consumeType: event.target.value })}
/>
</label>
<label className="kuaishou-industry-tool-field">
<span>核销序列号(可选)</span>
<Input
placeholder={selectedVoucher?.consumeSerialNum || '默认使用卡券记录'}
value={toolForm.serialNum}
onChange={(event) => updateToolForm({ serialNum: event.target.value })}
/>
</label>
<label className="kuaishou-industry-tool-field kuaishou-industry-tool-field-wide">
<span>销毁原因</span>
<Select
value={toolForm.reason || undefined}
onChange={(value) => updateToolForm({ reason: value || '' })}
options={[
{ label: '用户申请退款', value: 'USER_APPLY_REFUND' },
{ label: '后台作废', value: 'SYS_ADMIN_DESTROY' },
{ label: '供应商作废', value: 'SUPPLY_DESTROY' },
{ label: '凭证过期', value: 'ETICKET_EXPIRED' },
]}
/>
</label>
<label className="kuaishou-industry-tool-field">
<span>门店名称(可选)</span>
<Input
placeholder="用于核销明细"
value={toolForm.storeName}
onChange={(event) => updateToolForm({ storeName: event.target.value })}
/>
</label>
<label className="kuaishou-industry-tool-field">
<span>门店地址(可选)</span>
<Input
placeholder="用于核销明细"
value={toolForm.storeAddress}
onChange={(event) => updateToolForm({ storeAddress: event.target.value })}
/>
</label>
</div>
</div>
<Collapse
ghost
size="small"
className="kuaishou-industry-manual-target"
items={[
{
key: 'manual-target',
label: selectedVoucher ? '手动指定卡券(清除当前选择后可用)' : '手动指定卡券',
children: (
<div className="kuaishou-industry-tool-grid">
<label className="kuaishou-industry-tool-field">
<span>店铺 ID</span>
<Input
disabled={Boolean(selectedVoucher)}
value={toolForm.sellerId}
onChange={(event) => updateToolForm({ sellerId: event.target.value })}
/>
</label>
<label className="kuaishou-industry-tool-field">
<span>订单号 oid</span>
<Input
disabled={Boolean(selectedVoucher)}
value={toolForm.oid}
onChange={(event) =>
updateToolForm({
oid: event.target.value,
orderId: event.target.value,
})
}
/>
</label>
<label className="kuaishou-industry-tool-field">
<span>任务 ID</span>
<Input
disabled={Boolean(selectedVoucher)}
value={String(toolForm.taskId || '')}
onChange={(event) => updateToolForm({ taskId: event.target.value })}
/>
</label>
<label className="kuaishou-industry-tool-field">
<span>券码</span>
<Input
disabled={Boolean(selectedVoucher)}
value={toolForm.voucherCode}
onChange={(event) => updateToolForm({ voucherCode: event.target.value })}
/>
</label>
<label className="kuaishou-industry-tool-field kuaishou-industry-tool-field-wide">
<span>电子凭证类型</span>
<Input
disabled={Boolean(selectedVoucher)}
value={toolForm.eticketType}
onChange={(event) => updateToolForm({ eticketType: event.target.value })}
/>
</label>
</div>
),
},
]}
/>
<div className="kuaishou-industry-actions">
<Button
icon={<SyncOutlined />}
loading={actionLoading === 'check'}
disabled={!hasVoucherTarget}
onClick={runCheckAvailable}
>
检查有效性
</Button>
<Button
icon={<SendOutlined />}
loading={actionLoading === 'resend'}
disabled={!hasVoucherTarget}
onClick={runResendCode}
>
重发发码
</Button>
<Button
type="primary"
icon={<CheckCircleOutlined />}
loading={actionLoading === 'consume'}
disabled={!hasVoucherTarget}
onClick={runConsume}
>
手动核销
</Button>
<Button
danger
icon={<RollbackOutlined />}
loading={actionLoading === 'reverse'}
disabled={!hasVoucherTarget}
onClick={runReverse}
>
冲正
</Button>
<Popconfirm
title="确认销毁该券码?"
description="将先检查快手卡券状态,仅 frozen 状态会发起销毁并关闭关联任务。"
okText="销毁"
cancelText="取消"
disabled={!canManageRefund || !hasVoucherTarget}
onConfirm={() => runDestroy()}
>
<Button
danger
type="primary"
icon={<DeleteOutlined />}
loading={actionLoading === 'destroy'}
disabled={!canManageRefund || !hasVoucherTarget}
>
手动销毁
</Button>
</Popconfirm>
</div>
{renderInlineActionResult('vouchers')}
</Card>
</div>
</div>
)
}
function renderRefundTab() {
return (
<div className="page-stack">
<Card title="售后单列表">
<Space wrap className="filter-form">
{renderSellerIdControl(refundListForm.sellerId, (sellerId) =>
updateRefundListForm({ sellerId }),
)}
<Input
placeholder="订单号"
value={refundListForm.orderId}
onChange={(event) => updateRefundListForm({ orderId: event.target.value })}
/>
<AdminRangePicker
showTime
value={refundDateRange}
onChange={(value) => setRefundDateRange(value)}
/>
<Select
style={{ width: 140 }}
value={refundListForm.type}
options={[
{ label: '等待退款', value: '8' },
{ label: '全部退款', value: '9' },
]}
onChange={(value) => updateRefundListForm({ type: value })}
/>
<Input
placeholder="状态"
value={refundListForm.status}
onChange={(event) => updateRefundListForm({ status: event.target.value })}
/>
<Input
placeholder="游标"
value={refundListForm.pcursor}
onChange={(event) => updateRefundListForm({ pcursor: event.target.value })}
/>
<InputNumber
min={1}
max={100}
value={Number(refundListForm.pageSize)}
onChange={(value) => updateRefundListForm({ pageSize: String(value || 50) })}
/>
<Button
type="primary"
icon={<SearchOutlined />}
loading={actionLoading === 'refund-list'}
onClick={runRefundList}
>
查询售后
</Button>
</Space>
<PercentTable
rowKey={(row) => String(row.refundId || row.oid || JSON.stringify(row))}
columns={refundColumns}
dataSource={refundRows}
scroll={{ x: 1120 }}
pagination={buildAdminLocalTablePagination({ pageSize: ADMIN_DEFAULT_PAGE_SIZE })}
style={{ marginTop: 16 }}
/>
</Card>
<div className="kuaishou-industry-split">
<Card title="同意退款">
<div className="kuaishou-industry-tool-grid">
{renderSellerIdControl(approveForm.sellerId || refundListForm.sellerId, (sellerId) =>
setApproveForm({ ...approveForm, sellerId }),
)}
<Input
placeholder="退款单编号"
value={String(approveForm.refundId || '')}
onChange={(event) =>
setApproveForm({
...approveForm,
refundId: event.target.value,
})
}
/>
<Input
placeholder="退款金额(分)"
value={String(approveForm.refundAmount || '')}
onChange={(event) =>
setApproveForm({
...approveForm,
refundAmount: event.target.value,
})
}
/>
<Input
placeholder="退款单状态"
value={String(approveForm.status || '')}
onChange={(event) => setApproveForm({ ...approveForm, status: event.target.value })}
/>
<Input
placeholder="协商状态"
value={String(approveForm.negotiateStatus || '')}
onChange={(event) =>
setApproveForm({
...approveForm,
negotiateStatus: event.target.value,
})
}
/>
<Input
placeholder="退款方式"
value={String(approveForm.refundHandingWay || '')}
onChange={(event) =>
setApproveForm({
...approveForm,
refundHandingWay: event.target.value,
})
}
/>
<Input
placeholder="说明"
value={approveForm.desc}
onChange={(event) => setApproveForm({ ...approveForm, desc: event.target.value })}
/>
</div>
<Button
type="primary"
icon={<CheckCircleOutlined />}
loading={actionLoading === 'refund-approve'}
onClick={runApproveRefund}
>
同意退款
</Button>
</Card>
<Card title="不同意退款">
<div className="kuaishou-industry-tool-grid">
{renderSellerIdControl(disagreeForm.sellerId || refundListForm.sellerId, (sellerId) =>
setDisagreeForm({ ...disagreeForm, sellerId }),
)}
<Input
placeholder="退款单编号"
value={String(disagreeForm.refundId || '')}
onChange={(event) =>
setDisagreeForm({
...disagreeForm,
refundId: event.target.value,
})
}
/>
<Input
placeholder="拒绝原因枚举"
value={String(disagreeForm.sellerDisagreeReason || '')}
onChange={(event) =>
setDisagreeForm({
...disagreeForm,
sellerDisagreeReason: event.target.value,
})
}
/>
<Input
placeholder="拒绝说明"
value={disagreeForm.sellerDisagreeDesc}
onChange={(event) =>
setDisagreeForm({
...disagreeForm,
sellerDisagreeDesc: event.target.value,
})
}
/>
<Input
placeholder="退款单状态"
value={String(disagreeForm.status || '')}
onChange={(event) =>
setDisagreeForm({
...disagreeForm,
status: event.target.value,
})
}
/>
<Input
placeholder="协商状态"
value={String(disagreeForm.negotiateStatus || '')}
onChange={(event) =>
setDisagreeForm({
...disagreeForm,
negotiateStatus: event.target.value,
})
}
/>
</div>
<Button
danger
icon={<RollbackOutlined />}
loading={actionLoading === 'refund-disagree'}
onClick={runDisagreeRefund}
>
不同意退款
</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() {
return (
<Card
title={lastActionResult?.title || '接口结果'}
extra={
lastActionResult ? (
<Button icon={<ReloadOutlined />} onClick={() => setLastActionResult(null)}>
清空
</Button>
) : null
}
>
{lastActionResult ? (
renderActionResultBody(lastActionResult, { compact: false })
) : (
<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 }))
}
function updateRefundListForm(patch: Partial<RefundListState>) {
setRefundListForm((current) => ({ ...current, ...patch }))
}
function applyDefaultSellerId(sellerId: string) {
setRefundListForm((current) => (current.sellerId ? current : { ...current, sellerId }))
setApproveForm((current) => (current.sellerId ? current : { ...current, sellerId }))
setDisagreeForm((current) => (current.sellerId ? current : { ...current, sellerId }))
setToolForm((current) => (current.sellerId ? current : { ...current, sellerId }))
}
function resolveRefundSellerId(value?: unknown): string {
return (
String(value || refundListForm.sellerId || '').trim() ||
(refundShopOptions.length === 1 ? refundShopOptions[0]?.value || '' : '')
)
}
function renderSellerIdControl(value: string | undefined, onChange: (sellerId: string) => void) {
const normalizedValue = String(value || '').trim()
if (refundShopOptions.length > 0) {
return (
<Select
allowClear
showSearch
optionFilterProp="label"
placeholder="授权店铺"
style={{ minWidth: 220 }}
value={normalizedValue || undefined}
options={refundShopOptions}
onChange={(nextValue) => onChange(String(nextValue || '').trim())}
/>
)
}
return (
<Input
allowClear
placeholder="店铺 ID"
value={normalizedValue}
onChange={(event) => onChange(event.target.value)}
/>
)
}
}
function renderShopCell(
shopName?: string | null,
sellerId?: string | null,
tokenMasked?: string | null,
options: { compact?: boolean } = {},
) {
const name = String(shopName || '').trim()
const id = String(sellerId || '').trim()
const token = String(tokenMasked || '').trim()
const compact = options.compact === true
if (!name && !id) {
return <span>-</span>
}
const tipParts = [name || null, id ? `ID:${id}` : null, token ? `Token:${token}` : null].filter(
Boolean,
)
const tip = tipParts.join('\n')
if (compact) {
return (
<div className="cell-stack" title={tip}>
<Typography.Text ellipsis={{ tooltip: tip }}>{name || id}</Typography.Text>
{name && id ? <span className="muted">ID:{id}</span> : null}
</div>
)
}
return (
<div className="cell-stack">
<span>{name || id}</span>
{name && id ? <span className="muted">ID:{id}</span> : null}
{token ? <span className="muted">Token:{token}</span> : null}
</div>
)
}
function extractRefundRows(
response: Record<string, unknown> | null,
): Array<Record<string, unknown>> {
const data = response?.data
if (!data || typeof data !== 'object' || Array.isArray(data)) {
return []
}
const rows = (data as { refundOrderInfoList?: unknown }).refundOrderInfoList
return Array.isArray(rows)
? rows.filter((item): item is Record<string, unknown> =>
Boolean(item && typeof item === 'object' && !Array.isArray(item)),
)
: []
}
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(response)
pushHighlight(highlights, '返回券数', String(etickets.length || 0))
const resolvedType = pickFirstString([
record.eticketType,
voucher?.eticketType,
asRecord(openApi.request)?.eticketType,
asRecord(asRecord(openApi.request)?.param)?.eticketType,
])
if (resolvedType) {
pushHighlight(highlights, '请求类型', resolvedType)
}
if (etickets[0]) {
pushHighlight(
highlights,
'首张券码',
pickFirstString([etickets[0].code, etickets[0].id]) || '-',
)
pushHighlight(
highlights,
'展示状态',
pickFirstString([etickets[0].displayStatus, etickets[0].status]) || '-',
)
pushHighlight(highlights, '平台类型', pickFirstString([etickets[0].eticketType]) || '-')
pushHighlight(highlights, '剩余数量', String(etickets[0].leftNum ?? etickets[0].num ?? 1))
}
}
if (
kind === 'consume' ||
kind === 'destroy' ||
kind === 'reverse' ||
kind === 'resend' ||
voucher
) {
if (voucher) {
pushHighlight(highlights, '券码', String(voucher.voucherCode || '-'))
pushHighlight(highlights, '券状态', getVoucherStatusLabel(String(voucher.status || '')))
if (voucher.eticketType) {
pushHighlight(highlights, '凭证类型', String(voucher.eticketType))
}
if (kind === 'destroy' && record.reason) {
pushHighlight(highlights, '销毁原因', String(record.reason))
}
if (kind === 'destroy' && typeof record.alreadyDestroyed === 'boolean') {
pushHighlight(highlights, '已是销毁态', record.alreadyDestroyed ? '是(重试回调)' : '否')
}
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,
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 || '检查成功,请结合展示状态(如 frozen)判断是否可核销。'
: context.platformMessage || '检查失败,请核对卖家、券码与电子凭证类型。'
}
if (kind === 'consume') {
return success
? `核销成功${context.voucherStatus ? `,券状态:${context.voucherStatus}` : ''}。`
: context.platformMessage || '核销失败。'
}
if (kind === 'destroy') {
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' || kind === 'destroy') {
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'
if (normalized === 'DESTROYED') return 'red'
return 'blue'
}
function getVoucherStatusLabel(status: string) {
const normalized = String(status || '').toUpperCase()
if (normalized === 'CONSUMED') return '已核销'
if (normalized === 'DESTROYED') return '已销毁'
if (normalized === 'UNUSED') return '未使用'
return status || '-'
}
function formatShopOptionLabel(shop: AdminKuaishouIndustryShopOption) {
const name = String(shop.shopName || '').trim() || '未命名店铺'
const id = shop.sellerId || shop.shopId || ''
const disabledSuffix = shop.enabled === false ? ' · 已停用' : ''
return id ? `${name}(${id})${disabledSuffix}` : `${name}${disabledSuffix}`
}
function formatTimestamp(value: unknown) {
const timestamp = Number(value || 0)
if (!Number.isFinite(timestamp) || timestamp <= 0) {
return '-'
}
return formatAdminDateTime(new Date(timestamp).toISOString())
}