新增快手电子凭证后台工具

This commit is contained in:
yml2213
2026-07-09 18:39:34 +08:00
parent 1bfb32dcd0
commit c61078d3f6
29 changed files with 3034 additions and 474 deletions
@@ -0,0 +1,834 @@
import {
CheckCircleOutlined,
ReloadOutlined,
RollbackOutlined,
SearchOutlined,
SendOutlined,
SyncOutlined,
} from '@ant-design/icons'
import {
Alert,
Button,
Card,
DatePicker,
Input,
InputNumber,
Select,
Space,
Table,
Tabs,
Tag,
Typography,
} from 'antd'
import type { TableColumnsType } from 'antd'
import dayjs from 'dayjs'
import type { Dayjs } from 'dayjs'
import { useEffect, useMemo, useState } from 'react'
import PageHeader from '@/components/admin/PageHeader'
import { showError, showSuccess } from '@/lib/feedback'
import {
approveAdminKuaishouIndustryRefund,
checkAdminKuaishouIndustryVoucherAvailable,
consumeAdminKuaishouIndustryVoucher,
disagreeAdminKuaishouIndustryRefund,
fetchAdminKuaishouIndustryVouchers,
listAdminKuaishouIndustryRefunds,
resendAdminKuaishouIndustryVoucherCode,
reverseAdminKuaishouIndustryVoucher,
} from '@/services/admin'
import type {
AdminKuaishouIndustryOpenApiResult,
AdminKuaishouIndustryRefundApprovePayload,
AdminKuaishouIndustryRefundDisagreePayload,
AdminKuaishouIndustryRefundListPayload,
AdminKuaishouIndustryVoucher,
AdminKuaishouIndustryVoucherToolPayload,
} from '@/types/admin'
import { formatAdminDateTime } from '@/utils/admin-time'
import { stringifyDisplayJson } from '@/utils/date-time'
type DateRangeValue = [Dayjs | null, Dayjs | null] | null
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
}
const DEFAULT_ETICKET_TYPE = 'DINING_OPEN_TICKET'
export default function AdminKuaishouIndustryPage() {
const [voucherLoading, setVoucherLoading] = useState(false)
const [actionLoading, setActionLoading] = useState('')
const [vouchers, setVouchers] = useState<AdminKuaishouIndustryVoucher[]>([])
const [voucherTotal, setVoucherTotal] = useState(0)
const [voucherFilters, setVoucherFilters] = useState<VoucherFilterState>({
oid: '',
voucherCode: '',
taskId: '',
sellerId: '',
status: '',
page: 1,
pageSize: 50,
})
const [toolForm, setToolForm] = useState<AdminKuaishouIndustryVoucherToolPayload>({
eticketType: DEFAULT_ETICKET_TYPE,
consumeType: 'delivery',
})
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 [lastResultTitle, setLastResultTitle] = useState('接口结果')
const [lastResult, setLastResult] = useState<unknown>(null)
const [refundRows, setRefundRows] = useState<Array<Record<string, unknown>>>([])
const columns = useMemo<TableColumnsType<AdminKuaishouIndustryVoucher>>(
() => [
{
title: '券码',
minWidth: 220,
render: (_, row) => (
<div className="cell-stack">
<Typography.Text strong copyable>
{row.voucherCode}
</Typography.Text>
<span className="muted">{row.unitIndex}</span>
</div>
),
},
{
title: '订单 / 任务',
minWidth: 220,
render: (_, row) => (
<div className="cell-stack">
<Typography.Text copyable>{row.oid || '-'}</Typography.Text>
<span className="muted">
{row.taskId || '-'} / {row.orderId || '-'}
</span>
</div>
),
},
{
title: '状态',
width: 150,
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>
</div>
),
},
{
title: '卖家',
minWidth: 160,
render: (_, row) => (
<div className="cell-stack">
<span>{row.sellerId || '-'}</span>
<span className="muted">Token{row.tokenMasked || '-'}</span>
</div>
),
},
{
title: '核销',
minWidth: 180,
render: (_, row) => (
<div className="cell-stack">
<span>{row.consumeSerialNum || '-'}</span>
<span className="muted">{formatAdminDateTime(row.consumedAt)}</span>
</div>
),
},
{
title: '更新时间',
width: 170,
render: (_, row) => formatAdminDateTime(row.updatedAt),
},
{
title: '操作',
fixed: 'right',
width: 120,
render: (_, row) => (
<Button size="small" type="primary" ghost onClick={() => selectVoucher(row)}>
</Button>
),
},
],
[],
)
const refundColumns = useMemo<TableColumnsType<Record<string, unknown>>>(
() => [
{ title: '售后单', dataIndex: 'refundId', minWidth: 160 },
{ title: '订单', dataIndex: 'oid', minWidth: 160 },
{ 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>
),
},
],
[],
)
useEffect(() => {
void loadVouchers()
}, [])
async function loadVouchers(nextFilters: Partial<VoucherFilterState> = {}) {
const filters = { ...voucherFilters, ...nextFilters }
setVoucherFilters(filters)
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,
})
setVouchers(response.data.items)
setVoucherTotal(response.data.pagination.total)
} catch (error) {
showError(error instanceof Error ? error.message : '查询电子凭证失败')
} finally {
setVoucherLoading(false)
}
}
function selectVoucher(row: AdminKuaishouIndustryVoucher) {
setToolForm((current) => ({
...current,
sellerId: row.sellerId,
oid: row.oid,
orderId: row.oid,
taskId: row.taskId || '',
voucherCode: row.voucherCode,
serialNum: row.consumeSerialNum || current.serialNum,
}))
showSuccess('已填入券码操作区')
}
async function runCheckAvailable() {
await runOpenApiAction('检查电子凭证有效性', 'check', () =>
checkAdminKuaishouIndustryVoucherAvailable(buildVoucherToolPayload()),
)
}
async function runReverse() {
await runOpenApiAction('电子凭证冲正回调', 'reverse', () =>
reverseAdminKuaishouIndustryVoucher({
...buildVoucherToolPayload(),
reason: toolForm.reason || '后台手动冲正',
}),
)
await loadVouchers()
}
async function runConsume() {
setActionLoading('consume')
try {
const response = await consumeAdminKuaishouIndustryVoucher(buildVoucherToolPayload())
setLastResultTitle('手动核销')
setLastResult(response.data)
showSuccess('手动核销已完成')
await loadVouchers()
} catch (error) {
showError(error instanceof Error ? error.message : '手动核销失败')
} finally {
setActionLoading('')
}
}
async function runResendCode() {
setActionLoading('resend')
try {
const response = await resendAdminKuaishouIndustryVoucherCode(buildVoucherToolPayload())
setLastResultTitle('重发发码回调')
setLastResult(response.data)
if (response.data.success) {
showSuccess('发码回调已重发')
} else {
showError(response.data.error || '发码回调重发失败')
}
await loadVouchers()
} catch (error) {
showError(error instanceof Error ? error.message : '重发发码回调失败')
} finally {
setActionLoading('')
}
}
async function runRefundList() {
const [begin, end] = refundDateRange || []
await runOpenApiAction('售后单列表', 'refund-list', () =>
listAdminKuaishouIndustryRefunds({
...refundListForm,
beginTime: begin?.valueOf(),
endTime: end?.valueOf(),
} satisfies AdminKuaishouIndustryRefundListPayload),
(result) => {
const rows = extractRefundRows(result.response)
setRefundRows(rows)
},
)
}
async function runApproveRefund() {
await runOpenApiAction('同意退款', 'refund-approve', () =>
approveAdminKuaishouIndustryRefund({
...approveForm,
sellerId: approveForm.sellerId || refundListForm.sellerId,
}),
)
}
async function runDisagreeRefund() {
await runOpenApiAction('不同意退款', 'refund-disagree', () =>
disagreeAdminKuaishouIndustryRefund({
...disagreeForm,
sellerId: disagreeForm.sellerId || refundListForm.sellerId,
}),
)
}
async function runOpenApiAction(
title: string,
loadingKey: string,
action: () => Promise<{ data: AdminKuaishouIndustryOpenApiResult }>,
afterSuccess?: (result: AdminKuaishouIndustryOpenApiResult) => void,
) {
setActionLoading(loadingKey)
try {
const response = await action()
setLastResultTitle(title)
setLastResult(response.data)
afterSuccess?.(response.data)
if (response.data.success) {
showSuccess(`${title}已执行`)
} else {
showError(response.data.error || `${title}返回失败`)
}
} catch (error) {
showError(error instanceof Error ? error.message : `${title}失败`)
} finally {
setActionLoading('')
}
}
function buildVoucherToolPayload(): AdminKuaishouIndustryVoucherToolPayload {
const voucherCode = String(toolForm.voucherCode || '').trim()
return {
...toolForm,
etickets: voucherCode ? [{ id: voucherCode, code: voucherCode, num: 1 }] : undefined,
}
}
function fillRefundAction(row: Record<string, unknown>) {
const refundId = String(row.refundId || '').trim()
const status = String(row.status || '').trim()
const negotiateStatus = String(row.negotiateStatus || '').trim()
setApproveForm((current) => ({
...current,
refundId,
status,
negotiateStatus,
refundAmount: String(row.refundFee || current.refundAmount || ''),
}))
setDisagreeForm((current) => ({
...current,
refundId,
status,
negotiateStatus: negotiateStatus || current.negotiateStatus,
}))
showSuccess('已填入退款操作区')
}
return (
<div className="page-stack kuaishou-industry-page">
<PageHeader title="快手电子凭证" description="行业电子凭证接口工具与售后处理" />
<Tabs
items={[
{
key: 'vouchers',
label: '券码操作',
children: renderVoucherTab(),
},
{
key: 'refunds',
label: '售后退款',
children: renderRefundTab(),
},
{
key: 'result',
label: '接口结果',
children: renderResultTab(),
},
]}
/>
</div>
)
function renderVoucherTab() {
return (
<div className="page-stack">
<Card>
<Space wrap className="filter-form">
<Input
allowClear
placeholder="订单号 oid"
value={voucherFilters.oid}
onChange={(event) => setVoucherFilters({ ...voucherFilters, oid: event.target.value })}
/>
<Input
allowClear
placeholder="券码"
value={voucherFilters.voucherCode}
onChange={(event) =>
setVoucherFilters({ ...voucherFilters, voucherCode: event.target.value })
}
/>
<Input
allowClear
placeholder="任务 ID"
value={voucherFilters.taskId}
onChange={(event) =>
setVoucherFilters({ ...voucherFilters, taskId: event.target.value })
}
/>
<Input
allowClear
placeholder="卖家 ID"
value={voucherFilters.sellerId}
onChange={(event) =>
setVoucherFilters({ ...voucherFilters, 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) => setVoucherFilters({ ...voucherFilters, status: value || '' })}
/>
<Button
type="primary"
icon={<SearchOutlined />}
loading={voucherLoading}
onClick={() => loadVouchers({ page: 1 })}
>
</Button>
</Space>
</Card>
<div className="kuaishou-industry-split">
<Card title="本地券码">
<Table
rowKey="id"
columns={columns}
dataSource={vouchers}
loading={voucherLoading}
scroll={{ x: 1180 }}
pagination={{
current: voucherFilters.page,
pageSize: voucherFilters.pageSize,
total: voucherTotal,
showSizeChanger: true,
onChange: (page, pageSize) => loadVouchers({ page, pageSize }),
}}
/>
</Card>
<Card title="接口操作">
<div className="kuaishou-industry-tool-grid">
<Input
placeholder="卖家 ID"
value={toolForm.sellerId}
onChange={(event) => updateToolForm({ sellerId: event.target.value })}
/>
<Input
placeholder="订单号 oid"
value={toolForm.oid}
onChange={(event) =>
updateToolForm({ oid: event.target.value, orderId: event.target.value })
}
/>
<Input
placeholder="任务 ID"
value={String(toolForm.taskId || '')}
onChange={(event) => updateToolForm({ taskId: event.target.value })}
/>
<Input
placeholder="券码"
value={toolForm.voucherCode}
onChange={(event) => updateToolForm({ voucherCode: event.target.value })}
/>
<Input
placeholder="电子凭证类型"
value={toolForm.eticketType}
onChange={(event) => updateToolForm({ eticketType: event.target.value })}
/>
<Input
placeholder="核销序列号"
value={toolForm.serialNum}
onChange={(event) => updateToolForm({ serialNum: event.target.value })}
/>
<Input
placeholder="核销类型"
value={toolForm.consumeType}
onChange={(event) => updateToolForm({ consumeType: event.target.value })}
/>
<Input
placeholder="冲正原因"
value={toolForm.reason}
onChange={(event) => updateToolForm({ reason: event.target.value })}
/>
<Input
placeholder="门店名称"
value={toolForm.storeName}
onChange={(event) => updateToolForm({ storeName: event.target.value })}
/>
<Input
placeholder="门店地址"
value={toolForm.storeAddress}
onChange={(event) => updateToolForm({ storeAddress: event.target.value })}
/>
</div>
<Space wrap className="kuaishou-industry-actions">
<Button
icon={<SyncOutlined />}
loading={actionLoading === 'check'}
onClick={runCheckAvailable}
>
</Button>
<Button
type="primary"
icon={<CheckCircleOutlined />}
loading={actionLoading === 'consume'}
onClick={runConsume}
>
</Button>
<Button
danger
icon={<RollbackOutlined />}
loading={actionLoading === 'reverse'}
onClick={runReverse}
>
</Button>
<Button
icon={<SendOutlined />}
loading={actionLoading === 'resend'}
onClick={runResendCode}
>
</Button>
</Space>
</Card>
</div>
</div>
)
}
function renderRefundTab() {
return (
<div className="page-stack">
<Card title="售后单列表">
<Space wrap className="filter-form">
<Input
placeholder="卖家 ID"
value={refundListForm.sellerId}
onChange={(event) => updateRefundListForm({ sellerId: event.target.value })}
/>
<Input
placeholder="订单号"
value={refundListForm.orderId}
onChange={(event) => updateRefundListForm({ orderId: event.target.value })}
/>
<DatePicker.RangePicker
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>
<Table
rowKey={(row) => String(row.refundId || row.oid || JSON.stringify(row))}
columns={refundColumns}
dataSource={refundRows}
scroll={{ x: 980 }}
pagination={{ pageSize: 10 }}
style={{ marginTop: 16 }}
/>
</Card>
<div className="kuaishou-industry-split">
<Card title="同意退款">
<div className="kuaishou-industry-tool-grid">
<Input
placeholder="卖家 ID"
value={approveForm.sellerId || refundListForm.sellerId}
onChange={(event) => setApproveForm({ ...approveForm, sellerId: event.target.value })}
/>
<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">
<Input
placeholder="卖家 ID"
value={disagreeForm.sellerId || refundListForm.sellerId}
onChange={(event) =>
setDisagreeForm({ ...disagreeForm, sellerId: event.target.value })
}
/>
<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>
</div>
)
}
function renderResultTab() {
return (
<Card
title={lastResultTitle}
extra={
<Button icon={<ReloadOutlined />} onClick={() => setLastResult(null)}>
</Button>
}
>
{lastResult ? (
<pre className="json-preview">{stringifyDisplayJson(lastResult)}</pre>
) : (
<Alert type="info" showIcon message="暂无接口结果" />
)}
</Card>
)
}
function updateToolForm(patch: Partial<AdminKuaishouIndustryVoucherToolPayload>) {
setToolForm((current) => ({ ...current, ...patch }))
}
function updateRefundListForm(patch: Partial<RefundListState>) {
setRefundListForm((current) => ({ ...current, ...patch }))
}
}
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 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 formatTimestamp(value: unknown) {
const timestamp = Number(value || 0)
if (!Number.isFinite(timestamp) || timestamp <= 0) {
return '-'
}
return formatAdminDateTime(new Date(timestamp).toISOString())
}