优化快手电子凭证:客服核销页、店铺展示与筛选

- 客服角色仅展示「券码核销」Tab,本地列表直接核销
- 卖家列改为店铺名+ID 展示,并返回 shopName
- 新增轻量店铺列表接口,支持按店铺下拉筛选
- 修复 asRecord 空值导致的前端构建类型错误
This commit is contained in:
yml2213
2026-07-10 17:46:40 +08:00
parent 7aeaafbe75
commit 36fb994acd
6 changed files with 414 additions and 139 deletions
@@ -14,6 +14,7 @@ import {
Descriptions,
Input,
InputNumber,
Popconfirm,
Select,
Space,
Table,
@@ -33,7 +34,7 @@ import {
checkAdminKuaishouIndustryVoucherAvailable,
consumeAdminKuaishouIndustryVoucher,
disagreeAdminKuaishouIndustryRefund,
fetchAdminKuaishouIndustrySourceConfig,
fetchAdminKuaishouIndustryShops,
fetchAdminKuaishouIndustryVouchers,
listAdminKuaishouIndustryRefunds,
resendAdminKuaishouIndustryVoucherCode,
@@ -44,11 +45,11 @@ import type {
AdminKuaishouIndustryRefundApprovePayload,
AdminKuaishouIndustryRefundDisagreePayload,
AdminKuaishouIndustryRefundListPayload,
AdminKuaishouIndustryShopConfig,
AdminKuaishouIndustryShopOption,
AdminKuaishouIndustryVoucher,
AdminKuaishouIndustryVoucherToolPayload,
} from '@/types/admin'
import { hasAdminRole } from '@/utils/admin-auth'
import { getAdminRole, hasAdminRole } from '@/utils/admin-auth'
import {
ADMIN_DEFAULT_PAGE_SIZE,
buildAdminLocalTablePagination,
@@ -58,7 +59,7 @@ import { formatAdminDateTime } from '@/utils/admin-time'
import { stringifyDisplayJson } from '@/utils/date-time'
type DateRangeValue = [Dayjs | null, Dayjs | null] | null
type IndustryTab = 'vouchers' | 'refunds' | 'result'
type IndustryTab = 'support-consume' | 'vouchers' | 'refunds' | 'result'
type IndustryActionScope = 'vouchers' | 'refunds'
type IndustryActionKind =
| 'check'
@@ -109,10 +110,14 @@ type RefundListState = {
const DEFAULT_ETICKET_TYPE = 'DINING_OPEN_TICKET'
export default function AdminKuaishouIndustryPage() {
const [activeTab, setActiveTab] = useState<IndustryTab>('vouchers')
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 [vouchers, setVouchers] = useState<AdminKuaishouIndustryVoucher[]>([])
const [voucherTotal, setVoucherTotal] = useState(0)
const [voucherFilters, setVoucherFilters] = useState<VoucherFilterState>({
@@ -158,7 +163,17 @@ export default function AdminKuaishouIndustryPage() {
})
const [lastActionResult, setLastActionResult] = useState<IndustryActionResultView | null>(null)
const [refundRows, setRefundRows] = useState<Array<Record<string, unknown>>>([])
const [industryShops, setIndustryShops] = useState<AdminKuaishouIndustryShopConfig[]>([])
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
@@ -169,6 +184,24 @@ export default function AdminKuaishouIndustryPage() {
})),
[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>>(
() => [
@@ -209,14 +242,14 @@ export default function AdminKuaishouIndustryPage() {
),
},
{
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) =>
renderShopCell(
row.shopName || shopNameBySellerId.get(String(row.sellerId || '').trim()) || '',
row.sellerId,
isSupportOnly ? '' : row.tokenMasked,
),
},
{
title: '核销',
@@ -228,30 +261,69 @@ export default function AdminKuaishouIndustryPage() {
</div>
),
},
{
title: '更新时间',
width: 170,
render: (_, row) => formatAdminDateTime(row.updatedAt),
},
...(isSupportOnly
? []
: [
{
title: '更新时间',
width: 170,
render: (_: unknown, row: AdminKuaishouIndustryVoucher) =>
formatAdminDateTime(row.updatedAt),
},
]),
{
title: '操作',
fixed: 'right',
width: 120,
render: (_, row) => (
<Button size="small" type="primary" ghost onClick={() => selectVoucher(row)}>
</Button>
),
fixed: 'right' as const,
width: isSupportOnly ? 100 : 120,
render: (_: unknown, row: AdminKuaishouIndustryVoucher) => {
if (isSupportOnly) {
const canConsume = row.status === 'UNUSED'
return (
<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>
)
}
return (
<Button size="small" type="primary" ghost onClick={() => selectVoucher(row)}>
</Button>
)
},
},
],
[],
[consumingVoucherId, isSupportOnly, shopNameBySellerId],
)
const refundColumns = useMemo<TableColumnsType<Record<string, unknown>>>(
() => [
{ title: '售后单', dataIndex: 'refundId', minWidth: 160 },
{ title: '订单', dataIndex: 'oid', minWidth: 160 },
{ title: '卖家', dataIndex: 'sellerId', minWidth: 140 },
{
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 },
@@ -271,18 +343,18 @@ export default function AdminKuaishouIndustryPage() {
),
},
],
[],
[shopNameBySellerId],
)
useEffect(() => {
void loadVouchers()
void loadIndustryShops()
void loadVouchers()
}, [])
async function loadIndustryShops() {
try {
const response = await fetchAdminKuaishouIndustrySourceConfig()
const shops = response.data.source.shops || []
const response = await fetchAdminKuaishouIndustryShops()
const shops = response.data.shops || []
setIndustryShops(shops)
const enabledSellerIds = shops
@@ -292,7 +364,8 @@ export default function AdminKuaishouIndustryPage() {
applyDefaultSellerId(enabledSellerIds[0] as string)
}
} catch (error) {
showError(error instanceof Error ? error.message : '读取快手授权店铺失败')
// 店铺下拉失败不阻断列表,仅回退为手动输入店铺 ID
console.warn(error instanceof Error ? error.message : '读取店铺列表失败')
}
}
@@ -372,6 +445,50 @@ export default function AdminKuaishouIndustryPage() {
}
}
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: DEFAULT_ETICKET_TYPE,
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 runResendCode() {
setActionLoading('resend')
try {
@@ -403,7 +520,7 @@ export default function AdminKuaishouIndustryPage() {
const [begin, end] = refundDateRange || []
const sellerId = resolveRefundSellerId(refundListForm.sellerId)
if (!sellerId && refundShopOptions.length > 1) {
showError('请先选择快手售后退款店铺 sellerId')
showError('请先选择快手售后退款店铺')
return
}
@@ -429,7 +546,7 @@ export default function AdminKuaishouIndustryPage() {
async function runApproveRefund() {
const sellerId = resolveRefundSellerId(approveForm.sellerId)
if (!sellerId && refundShopOptions.length > 1) {
showError('请先选择快手售后退款店铺 sellerId')
showError('请先选择快手售后退款店铺')
return
}
@@ -445,7 +562,7 @@ export default function AdminKuaishouIndustryPage() {
async function runDisagreeRefund() {
const sellerId = resolveRefundSellerId(disagreeForm.sellerId)
if (!sellerId && refundShopOptions.length > 1) {
showError('请先选择快手售后退款店铺 sellerId')
showError('请先选择快手售后退款店铺')
return
}
@@ -537,33 +654,47 @@ export default function AdminKuaishouIndustryPage() {
showSuccess('已填入退款操作区')
}
const tabItems: TabsProps['items'] = [
{
key: 'vouchers',
label: '券码操作',
children: renderVoucherTab(),
},
...(canManageRefund
? [
{
key: 'refunds',
label: '售后退款',
children: renderRefundTab(),
},
]
: []),
{
key: 'result',
label: lastActionResult ? `接口结果 · ${lastActionResult.title}` : '接口结果',
children: renderResultTab(),
},
]
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={canManageRefund ? '行业电子凭证接口工具与售后处理' : '行业电子凭证接口工具'}
description={
isSupportOnly
? '客服核销:查询本地券码并执行核销'
: canManageRefund
? '行业电子凭证接口工具与售后处理'
: '行业电子凭证接口工具'
}
/>
<Tabs
@@ -574,47 +705,63 @@ export default function AdminKuaishouIndustryPage() {
</div>
)
function renderVoucherTab() {
function renderVoucherFilters() {
return (
<div className="page-stack">
<Card>
<Space wrap className="filter-form">
<Input
<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,
})
}
/>
{shopFilterOptions.length > 0 ? (
<Select
allowClear
placeholder="订单号 oid"
value={voucherFilters.oid}
onChange={(event) =>
setVoucherFilters({
...voucherFilters,
oid: event.target.value,
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="券码"
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"
placeholder="店铺 ID"
value={voucherFilters.sellerId}
onChange={(event) =>
setVoucherFilters({
@@ -623,50 +770,71 @@ export default function AdminKuaishouIndustryPage() {
})
}
/>
<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>
)}
<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>
)
}
function renderVoucherTable(scrollX = 1180) {
return (
<Table
rowKey="id"
columns={columns}
dataSource={vouchers}
loading={voucherLoading}
scroll={{ x: scrollX }}
pagination={buildAdminTablePagination({
current: voucherFilters.page,
pageSize: voucherFilters.pageSize,
total: voucherTotal,
onChange: (page, pageSize) => loadVouchers({ page, pageSize }),
})}
/>
)
}
function renderSupportConsumeTab() {
return (
<div className="page-stack">
{renderVoucherFilters()}
<Card title="本地券码">{renderVoucherTable(1080)}</Card>
</div>
)
}
function renderVoucherTab() {
return (
<div className="page-stack">
{renderVoucherFilters()}
<div className="kuaishou-industry-split">
<Card title="本地券码">
<Table
rowKey="id"
columns={columns}
dataSource={vouchers}
loading={voucherLoading}
scroll={{ x: 1180 }}
pagination={buildAdminTablePagination({
current: voucherFilters.page,
pageSize: voucherFilters.pageSize,
total: voucherTotal,
onChange: (page, pageSize) => loadVouchers({ page, pageSize }),
})}
/>
</Card>
<Card title="本地券码">{renderVoucherTable()}</Card>
<Card title="接口操作">
<div className="kuaishou-industry-tool-grid">
<Input
placeholder="卖家 ID"
placeholder="店铺 ID"
value={toolForm.sellerId}
onChange={(event) => updateToolForm({ sellerId: event.target.value })}
/>
@@ -1115,7 +1283,7 @@ export default function AdminKuaishouIndustryPage() {
return (
<Input
allowClear
placeholder="卖家 ID"
placeholder="店铺 ID"
value={normalizedValue}
onChange={(event) => onChange(event.target.value)}
/>
@@ -1123,6 +1291,28 @@ export default function AdminKuaishouIndustryPage() {
}
}
function renderShopCell(
shopName?: string | null,
sellerId?: string | null,
tokenMasked?: string | null,
) {
const name = String(shopName || '').trim()
const id = String(sellerId || '').trim()
const token = String(tokenMasked || '').trim()
if (!name && !id) {
return <span>-</span>
}
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>> {
@@ -1409,11 +1599,12 @@ function getVoucherStatusLabel(status: string) {
return status || '-'
}
function formatShopOptionLabel(shop: AdminKuaishouIndustryShopConfig) {
const name = shop.customShopName || shop.shopName || '未命名店铺'
const tokenStatus = shop.hasAccessToken ? '' : ' / 未授权'
function formatShopOptionLabel(shop: AdminKuaishouIndustryShopOption) {
const name = String(shop.shopName || '').trim() || '未命名店铺'
const id = shop.sellerId || shop.shopId || ''
const disabledSuffix = shop.enabled === false ? ' · 已停用' : ''
return `${name} / ${shop.sellerId}${tokenStatus}`
return id ? `${name}${id}${disabledSuffix}` : `${name}${disabledSuffix}`
}
function formatTimestamp(value: unknown) {
@@ -4,6 +4,7 @@ import type {
AdminKuaishouIndustryRefundApprovePayload,
AdminKuaishouIndustryRefundDisagreePayload,
AdminKuaishouIndustryRefundListPayload,
AdminKuaishouIndustryShopListResult,
AdminKuaishouIndustryVoucherConsumeResult,
AdminKuaishouIndustryVoucherListResult,
AdminKuaishouIndustryVoucherResendResult,
@@ -17,6 +18,10 @@ export function fetchAdminKuaishouIndustryVouchers(params?: Record<string, unkno
)
}
export function fetchAdminKuaishouIndustryShops() {
return apiGet<AdminKuaishouIndustryShopListResult>('/api/v1/admin/kuaishou-industry/shops')
}
export function listAdminKuaishouIndustryRefunds(
payload: AdminKuaishouIndustryRefundListPayload,
) {
+2
View File
@@ -32,6 +32,8 @@ export type {
AdminKuaishouIndustryOpenApiResult,
AdminKuaishouIndustryVoucher,
AdminKuaishouIndustryVoucherListResult,
AdminKuaishouIndustryShopOption,
AdminKuaishouIndustryShopListResult,
AdminKuaishouIndustryVoucherConsumeResult,
AdminKuaishouIndustryVoucherResendResult,
AdminKuaishouIndustryRefundListPayload,
@@ -19,6 +19,8 @@ export interface AdminKuaishouIndustryVoucher {
taskId: number | null
unitIndex: number
sellerId: string
/** 来自行业店铺配置的展示名(customShopName 优先) */
shopName: string
tokenMasked: string
status: string
validStartTime: number
@@ -41,6 +43,18 @@ export interface AdminKuaishouIndustryVoucherListResult {
pagination: AdminPagination
}
/** 电子凭证页筛选用的轻量店铺选项(无 token 等敏感字段) */
export interface AdminKuaishouIndustryShopOption {
sellerId: string
shopId: string
shopName: string
enabled: boolean
}
export interface AdminKuaishouIndustryShopListResult {
shops: AdminKuaishouIndustryShopOption[]
}
export interface AdminKuaishouIndustryVoucherConsumeResult {
success: boolean
voucher: AdminKuaishouIndustryVoucher