feat(douyin-mashida): 新增抖音券核销后台界面(列表/核销/冲正/同步/对账/作废)
抖音订单并非纯自动发货:内部 SKU 命中 AFFILIATE_DASH_SKU_MAPPING 才走自动通道, 未命中显式回落人工,且核销依赖买家自己到 /redeem 输订单号。此前核销侧在后台完全 不可见:自动核销是 void .catch(warn) 只落日志,cancelVerify 出网方法已就绪但没有入口。 照快手电子凭证的骨架做一套抖音版(两张表两套状态机,不复用): - 券码状态、平台侧状态、同步错误全部可视化,支持只看同步失败 - 核销/冲正/重新同步/平台对账/重发券码/作废,每个写操作都带审计 - 券码列显示「核销实际会用的券码」(platform_cert_no 优先),平台改过名时额外标注 - 冲正先打平台 cancelVerify 再回退本地,避免本地与平台状态漂移
This commit is contained in:
@@ -0,0 +1,681 @@
|
||||
import {
|
||||
CheckCircleOutlined,
|
||||
DeleteOutlined,
|
||||
ReloadOutlined,
|
||||
RollbackOutlined,
|
||||
SearchOutlined,
|
||||
SendOutlined,
|
||||
SyncOutlined,
|
||||
} from '@ant-design/icons'
|
||||
import {
|
||||
Alert,
|
||||
Button,
|
||||
Card,
|
||||
Descriptions,
|
||||
Input,
|
||||
Popconfirm,
|
||||
Select,
|
||||
Space,
|
||||
Table,
|
||||
Tag,
|
||||
Typography,
|
||||
} from 'antd'
|
||||
import type { TableColumnsType } from 'antd'
|
||||
import { useEffect, useMemo, useRef, useState } from 'react'
|
||||
|
||||
import PageHeader from '@/components/admin/PageHeader'
|
||||
import { showError, showSuccess } from '@/lib/feedback'
|
||||
import {
|
||||
cancelAdminDouyinMashidaCertificateVerify,
|
||||
checkAdminDouyinMashidaCertificate,
|
||||
fetchAdminDouyinMashidaCertificates,
|
||||
invalidateAdminDouyinMashidaCertificate,
|
||||
resendAdminDouyinMashidaCertificate,
|
||||
syncAdminDouyinMashidaCertificate,
|
||||
verifyAdminDouyinMashidaCertificate,
|
||||
} from '@/services/admin'
|
||||
import type {
|
||||
AdminDouyinMashidaCertificate,
|
||||
AdminDouyinMashidaCertificateActionPayload,
|
||||
AdminDouyinMashidaPlatformCoupon,
|
||||
} from '@/types/admin'
|
||||
import { getAdminRole } from '@/utils/admin-auth'
|
||||
import { ADMIN_DEFAULT_PAGE_SIZE, buildAdminTablePagination } from '@/utils/admin-pagination'
|
||||
import { formatAdminDateTime } from '@/utils/admin-time'
|
||||
|
||||
/**
|
||||
* 后台「抖音券核销」。
|
||||
*
|
||||
* 与「快手电子凭证」是两套独立的表和状态机:
|
||||
* 这里的券是我们自己生成的 DYC 券码,经 `coupons.syncV2` 交给抖音码速达展示给买家,
|
||||
* 核销打的是 `coupons.verifyV3`、冲正打 `coupons.cancelVerify`。
|
||||
*
|
||||
* 界面照 `AdminKuaishouIndustryPage` 的骨架做(筛选 + 左列表 + 右操作),运营不用重新学习;
|
||||
* 但数据口径换成抖音自己的:**平台侧券码优先**(syncV2 可能改写我们的券码,
|
||||
* 拿旧券码核销会得到 `10017 核销码与订单不匹配`)。
|
||||
*/
|
||||
type CertificateFilterState = {
|
||||
certNo: string
|
||||
orderId: string
|
||||
status: string
|
||||
syncStatus: string
|
||||
/** 只看需要人跟进的异常:同步失败 / 已核销但平台显示未使用 */
|
||||
abnormalOnly: boolean
|
||||
page: number
|
||||
pageSize: number
|
||||
}
|
||||
|
||||
type ActionKey = 'check' | 'verify' | 'cancel' | 'sync' | 'resend' | 'invalidate'
|
||||
|
||||
type ActionResultView = {
|
||||
key: ActionKey
|
||||
title: string
|
||||
success: boolean
|
||||
message: string
|
||||
executedAt: string
|
||||
raw: unknown
|
||||
}
|
||||
|
||||
const STATUS_OPTIONS = [
|
||||
{ label: '已生成 issued', value: 'issued' },
|
||||
{ label: '已同步 synced', value: 'synced' },
|
||||
{ label: '已核销 verified', value: 'verified' },
|
||||
{ label: '已作废 invalid', value: 'invalid' },
|
||||
]
|
||||
|
||||
const SYNC_STATUS_OPTIONS = [
|
||||
{ label: '待同步 pending', value: 'pending' },
|
||||
{ label: '同步成功 success', value: 'success' },
|
||||
{ label: '同步失败 failed', value: 'failed' },
|
||||
]
|
||||
|
||||
function createEmptyFilters(): CertificateFilterState {
|
||||
return {
|
||||
certNo: '',
|
||||
orderId: '',
|
||||
status: '',
|
||||
syncStatus: '',
|
||||
abnormalOnly: false,
|
||||
page: 1,
|
||||
pageSize: ADMIN_DEFAULT_PAGE_SIZE,
|
||||
}
|
||||
}
|
||||
|
||||
export default function AdminDouyinMashidaCertificatePage() {
|
||||
const role = getAdminRole()
|
||||
const canInvalidate = role === 'admin' || role === 'operator'
|
||||
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [actionLoading, setActionLoading] = useState('')
|
||||
const [certificates, setCertificates] = useState<AdminDouyinMashidaCertificate[]>([])
|
||||
const [total, setTotal] = useState(0)
|
||||
const [selected, setSelected] = useState<AdminDouyinMashidaCertificate | null>(null)
|
||||
const [filters, setFilters] = useState<CertificateFilterState>(createEmptyFilters)
|
||||
// 异步操作后刷新列表时,沿用用户最后一次的筛选条件
|
||||
const filtersRef = useRef(filters)
|
||||
const [result, setResult] = useState<ActionResultView | null>(null)
|
||||
const [manualCertNo, setManualCertNo] = useState('')
|
||||
|
||||
useEffect(() => {
|
||||
void loadCertificates()
|
||||
}, [])
|
||||
|
||||
async function loadCertificates(patch: Partial<CertificateFilterState> = {}) {
|
||||
const next = updateFilters(patch)
|
||||
setLoading(true)
|
||||
|
||||
try {
|
||||
const response = await fetchAdminDouyinMashidaCertificates({
|
||||
certNo: next.certNo.trim(),
|
||||
orderId: next.orderId.trim(),
|
||||
status: next.abnormalOnly ? '' : next.status,
|
||||
syncStatus: next.abnormalOnly ? 'failed' : next.syncStatus,
|
||||
page: next.page,
|
||||
pageSize: next.pageSize,
|
||||
})
|
||||
|
||||
setCertificates(response.data.items)
|
||||
setTotal(response.data.pagination.total)
|
||||
setSelected((current) => {
|
||||
if (!current) {
|
||||
return null
|
||||
}
|
||||
return response.data.items.find((item) => item.id === current.id) || current
|
||||
})
|
||||
} catch (error) {
|
||||
showError(error instanceof Error ? error.message : '查询抖音兑换券失败')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
function updateFilters(patch: Partial<CertificateFilterState>): CertificateFilterState {
|
||||
const next = { ...filtersRef.current, ...patch }
|
||||
filtersRef.current = next
|
||||
setFilters(next)
|
||||
return next
|
||||
}
|
||||
|
||||
const columns = useMemo<TableColumnsType<AdminDouyinMashidaCertificate>>(
|
||||
() => [
|
||||
{
|
||||
title: '券码',
|
||||
width: 240,
|
||||
render: (_, row) => (
|
||||
<div className="cell-stack">
|
||||
<Typography.Text
|
||||
strong
|
||||
copyable={Boolean(row.verifyCertNo)}
|
||||
title={row.verifyCertNo || undefined}
|
||||
>
|
||||
{row.verifyCertNo || '-'}
|
||||
</Typography.Text>
|
||||
{/* 平台改过券码时必须让它显眼:贴错券码核销会得到 10017 */}
|
||||
{row.platformCertNo && row.platformCertNo !== row.certNo ? (
|
||||
<span className="muted">平台已改名:{row.certNo}</span>
|
||||
) : (
|
||||
<span className="muted">发券:{formatAdminDateTime(row.grantTime)}</span>
|
||||
)}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '订单商品',
|
||||
width: 300,
|
||||
ellipsis: true,
|
||||
render: (_, row) => (
|
||||
<div className="cell-stack">
|
||||
<Typography.Text copyable={Boolean(row.orderId)} ellipsis={{ tooltip: row.orderId }}>
|
||||
{row.orderId || '-'}
|
||||
</Typography.Text>
|
||||
{row.skuCode || row.skuName ? (
|
||||
<Typography.Text
|
||||
ellipsis={{
|
||||
tooltip: row.skuName ? `${row.skuName}(${row.skuCode})` : row.skuCode,
|
||||
}}
|
||||
>
|
||||
{row.skuCode || row.skuName}
|
||||
</Typography.Text>
|
||||
) : (
|
||||
<span className="muted">未关联商品</span>
|
||||
)}
|
||||
<span className="muted">
|
||||
任务 {row.taskNo || '-'}
|
||||
{row.parentOrderId && row.parentOrderId !== row.orderId
|
||||
? ` · 父单 ${row.parentOrderId}`
|
||||
: ''}
|
||||
</span>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '本地状态',
|
||||
width: 150,
|
||||
render: (_, row) => (
|
||||
<div className="cell-stack">
|
||||
<Tag color={resolveStatusColor(row.status)}>{resolveStatusLabel(row.status)}</Tag>
|
||||
<span className="muted">同步 {resolveSyncLabel(row.syncStatus)}</span>
|
||||
{row.syncError ? (
|
||||
<Typography.Text
|
||||
type="danger"
|
||||
ellipsis={{ tooltip: row.syncError }}
|
||||
className="muted"
|
||||
>
|
||||
{row.syncError}
|
||||
</Typography.Text>
|
||||
) : null}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '平台侧',
|
||||
width: 160,
|
||||
render: (_, row) => (
|
||||
<div className="cell-stack">
|
||||
<Tag color={resolvePlatformColor(row.platformStatus)}>
|
||||
{row.platformStatusLabel || '未回查'}
|
||||
</Tag>
|
||||
<span className="muted">核销:{formatAdminDateTime(row.verifiedAt)}</span>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
fixed: 'right',
|
||||
width: 96,
|
||||
render: (_, row) => {
|
||||
const isSelected = selected?.id === row.id
|
||||
return (
|
||||
<Button size="small" type="primary" ghost={!isSelected} onClick={() => selectRow(row)}>
|
||||
{isSelected ? '已选' : '选择'}
|
||||
</Button>
|
||||
)
|
||||
},
|
||||
},
|
||||
],
|
||||
[selected?.id],
|
||||
)
|
||||
|
||||
function selectRow(row: AdminDouyinMashidaCertificate) {
|
||||
setSelected(row)
|
||||
setManualCertNo('')
|
||||
}
|
||||
|
||||
const targetCertNo = String(selected?.verifyCertNo || selected?.certNo || manualCertNo).trim()
|
||||
|
||||
function buildPayload(): AdminDouyinMashidaCertificateActionPayload {
|
||||
return {
|
||||
certNo: targetCertNo,
|
||||
orderId: selected?.orderId || undefined,
|
||||
localOrderId: selected?.localOrderId || undefined,
|
||||
orderItemId: selected?.orderItemId || undefined,
|
||||
}
|
||||
}
|
||||
|
||||
async function runAction(
|
||||
key: ActionKey,
|
||||
title: string,
|
||||
successMessage: string,
|
||||
action: () => Promise<{ data: unknown }>,
|
||||
) {
|
||||
if (!targetCertNo) {
|
||||
showError('请先选择券码,或在下方手动填写券码')
|
||||
return
|
||||
}
|
||||
|
||||
setActionLoading(key)
|
||||
try {
|
||||
const response = await action()
|
||||
publishResult(key, title, true, successMessage, response.data)
|
||||
showSuccess(successMessage)
|
||||
await loadCertificates()
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : `${title}失败`
|
||||
publishResult(key, title, false, message, null)
|
||||
showError(message)
|
||||
} finally {
|
||||
setActionLoading('')
|
||||
}
|
||||
}
|
||||
|
||||
function publishResult(
|
||||
key: ActionKey,
|
||||
title: string,
|
||||
success: boolean,
|
||||
message: string,
|
||||
raw: unknown,
|
||||
) {
|
||||
setResult({
|
||||
key,
|
||||
title,
|
||||
success,
|
||||
message,
|
||||
executedAt: new Date().toISOString(),
|
||||
raw,
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="page-stack douyin-mashida-certificate-page">
|
||||
<PageHeader
|
||||
title="抖音券核销"
|
||||
description="抖音码速达兑换券:查询本地券码、手动核销 / 冲正 / 重发,并与平台侧对账"
|
||||
/>
|
||||
|
||||
<Card>
|
||||
<Space wrap className="filter-form">
|
||||
<Input
|
||||
allowClear
|
||||
placeholder="券码(我方或平台侧)"
|
||||
value={filters.certNo}
|
||||
onChange={(event) => updateFilters({ certNo: event.target.value })}
|
||||
/>
|
||||
<Input
|
||||
allowClear
|
||||
placeholder="平台订单号"
|
||||
value={filters.orderId}
|
||||
onChange={(event) => updateFilters({ orderId: event.target.value })}
|
||||
/>
|
||||
<Select
|
||||
allowClear
|
||||
placeholder="券状态"
|
||||
style={{ width: 152 }}
|
||||
value={filters.status || undefined}
|
||||
options={STATUS_OPTIONS}
|
||||
disabled={filters.abnormalOnly}
|
||||
onChange={(value) => updateFilters({ status: value || '' })}
|
||||
/>
|
||||
<Select
|
||||
allowClear
|
||||
placeholder="同步状态"
|
||||
style={{ width: 152 }}
|
||||
value={filters.syncStatus || undefined}
|
||||
options={SYNC_STATUS_OPTIONS}
|
||||
disabled={filters.abnormalOnly}
|
||||
onChange={(value) => updateFilters({ syncStatus: value || '' })}
|
||||
/>
|
||||
<Button
|
||||
icon={<SearchOutlined />}
|
||||
type={filters.abnormalOnly ? 'primary' : 'default'}
|
||||
onClick={() =>
|
||||
void loadCertificates({ ...filters, abnormalOnly: !filters.abnormalOnly, page: 1 })
|
||||
}
|
||||
>
|
||||
{filters.abnormalOnly ? '只看同步失败(点击取消)' : '只看同步失败'}
|
||||
</Button>
|
||||
<Button
|
||||
icon={<SearchOutlined />}
|
||||
type="primary"
|
||||
loading={loading}
|
||||
onClick={() => void loadCertificates({ page: 1 })}
|
||||
>
|
||||
查询
|
||||
</Button>
|
||||
<Button onClick={() => void loadCertificates(createEmptyFilters())}>清空筛选</Button>
|
||||
</Space>
|
||||
</Card>
|
||||
|
||||
<div className="kuaishou-industry-split">
|
||||
<Card title="本地券码" className="kuaishou-industry-voucher-card">
|
||||
<Table
|
||||
rowKey="id"
|
||||
size="middle"
|
||||
tableLayout="fixed"
|
||||
columns={columns}
|
||||
dataSource={certificates}
|
||||
loading={loading}
|
||||
rowClassName={(row) =>
|
||||
selected?.id === row.id ? 'kuaishou-industry-row-selected' : ''
|
||||
}
|
||||
scroll={{ x: 1046 }}
|
||||
pagination={buildAdminTablePagination({
|
||||
current: filters.page,
|
||||
pageSize: filters.pageSize,
|
||||
total,
|
||||
onChange: (page, pageSize) => {
|
||||
void loadCertificates({ page, pageSize })
|
||||
},
|
||||
})}
|
||||
/>
|
||||
</Card>
|
||||
|
||||
<Card title="券码操作" className="kuaishou-industry-tool-card">
|
||||
<div className="kuaishou-industry-tool-section">
|
||||
<div className="kuaishou-industry-tool-section-title">当前卡券</div>
|
||||
{selected ? (
|
||||
<div className="kuaishou-industry-selected-voucher">
|
||||
<div className="kuaishou-industry-selected-header">
|
||||
<Typography.Text
|
||||
strong
|
||||
copyable={{ text: selected.verifyCertNo }}
|
||||
className="kuaishou-industry-selected-code"
|
||||
>
|
||||
{selected.verifyCertNo || '-'}
|
||||
</Typography.Text>
|
||||
<Space size={4}>
|
||||
<Tag color={resolveStatusColor(selected.status)}>
|
||||
{resolveStatusLabel(selected.status)}
|
||||
</Tag>
|
||||
<Button type="link" size="small" onClick={() => setSelected(null)}>
|
||||
清除
|
||||
</Button>
|
||||
</Space>
|
||||
</div>
|
||||
<div className="kuaishou-industry-selected-meta">
|
||||
<div>
|
||||
<span>订单号</span>
|
||||
<Typography.Text copyable={{ text: selected.orderId }}>
|
||||
{selected.orderId || '-'}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
<div>
|
||||
<span>本地订单</span>
|
||||
<strong>{selected.localOrderId ?? '-'}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>平台侧</span>
|
||||
<strong>{selected.platformStatusLabel || '未回查'}</strong>
|
||||
<small>{selected.platformCertNo || selected.platformCertId || '-'}</small>
|
||||
</div>
|
||||
<div>
|
||||
<span>兑换链接</span>
|
||||
<strong title={selected.certLink || undefined}>
|
||||
{selected.certLink || '-'}
|
||||
</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 kuaishou-industry-tool-field-wide">
|
||||
<span>券码</span>
|
||||
<Input
|
||||
disabled={Boolean(selected)}
|
||||
placeholder="我方 DYC… 或平台侧券码"
|
||||
value={manualCertNo}
|
||||
onChange={(event) => setManualCertNo(event.target.value)}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="kuaishou-industry-actions">
|
||||
<Button
|
||||
icon={<SyncOutlined />}
|
||||
loading={actionLoading === 'check'}
|
||||
onClick={() =>
|
||||
void runAction('check', '平台对账', '平台券码已查询', () =>
|
||||
checkAdminDouyinMashidaCertificate(buildPayload()),
|
||||
)
|
||||
}
|
||||
>
|
||||
平台对账
|
||||
</Button>
|
||||
<Button
|
||||
icon={<SendOutlined />}
|
||||
loading={actionLoading === 'resend'}
|
||||
onClick={() =>
|
||||
void runAction('resend', '重发券码', '券码已重发', () =>
|
||||
resendAdminDouyinMashidaCertificate(buildPayload()),
|
||||
)
|
||||
}
|
||||
>
|
||||
重发券码
|
||||
</Button>
|
||||
<Button
|
||||
icon={<ReloadOutlined />}
|
||||
loading={actionLoading === 'sync'}
|
||||
onClick={() =>
|
||||
void runAction('sync', '重新同步', '券码已重新同步', () =>
|
||||
syncAdminDouyinMashidaCertificate(buildPayload()),
|
||||
)
|
||||
}
|
||||
>
|
||||
重新同步
|
||||
</Button>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<CheckCircleOutlined />}
|
||||
loading={actionLoading === 'verify'}
|
||||
onClick={() =>
|
||||
void runAction('verify', '手动核销', '券码已核销', () =>
|
||||
verifyAdminDouyinMashidaCertificate(buildPayload()),
|
||||
)
|
||||
}
|
||||
>
|
||||
手动核销
|
||||
</Button>
|
||||
<Button
|
||||
danger
|
||||
icon={<RollbackOutlined />}
|
||||
loading={actionLoading === 'cancel'}
|
||||
onClick={() =>
|
||||
void runAction('cancel', '冲正', '券码已冲正', () =>
|
||||
cancelAdminDouyinMashidaCertificateVerify(buildPayload()),
|
||||
)
|
||||
}
|
||||
>
|
||||
冲正
|
||||
</Button>
|
||||
<Popconfirm
|
||||
title="确认作废该券码?"
|
||||
description="已核销的券码不能作废,请先冲正。"
|
||||
okText="作废"
|
||||
cancelText="取消"
|
||||
disabled={!canInvalidate}
|
||||
onConfirm={() =>
|
||||
void runAction('invalidate', '作废券码', '券码已作废', () =>
|
||||
invalidateAdminDouyinMashidaCertificate({
|
||||
...buildPayload(),
|
||||
reason: '后台手动作废',
|
||||
}),
|
||||
)
|
||||
}
|
||||
>
|
||||
<Button danger type="primary" icon={<DeleteOutlined />} disabled={!canInvalidate}>
|
||||
作废
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
</div>
|
||||
|
||||
{selected?.status === 'verified' ? (
|
||||
<Alert
|
||||
style={{ marginTop: 12 }}
|
||||
type="info"
|
||||
showIcon
|
||||
message="该券已核销:退款请走售后流程,误核销请先冲正再作废。"
|
||||
/>
|
||||
) : null}
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<Card title="操作结果">
|
||||
{result ? (
|
||||
<div className="page-stack">
|
||||
<Space wrap>
|
||||
<Tag color={result.success ? 'green' : 'red'}>{result.title}</Tag>
|
||||
<span className="muted">{formatAdminDateTime(result.executedAt)}</span>
|
||||
</Space>
|
||||
<Typography.Text type={result.success ? undefined : 'danger'}>
|
||||
{result.message}
|
||||
</Typography.Text>
|
||||
{result.key === 'check' || result.key === 'sync' ? (
|
||||
<Descriptions size="small" column={2} bordered>
|
||||
{renderPlatformCouponDescriptions(result.raw)}
|
||||
</Descriptions>
|
||||
) : null}
|
||||
</div>
|
||||
) : (
|
||||
<span className="muted">暂无操作记录</span>
|
||||
)}
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
/** 从「对账 / 重新同步」的返回里取出平台侧券信息。 */
|
||||
function renderPlatformCouponDescriptions(raw: unknown) {
|
||||
const coupon = extractPlatformCoupon(raw)
|
||||
|
||||
return [
|
||||
<Descriptions.Item key="certNo" label="平台券码">
|
||||
{coupon?.certNo || '-'}
|
||||
</Descriptions.Item>,
|
||||
<Descriptions.Item key="status" label="平台状态">
|
||||
{coupon?.statusLabel || '-'}
|
||||
</Descriptions.Item>,
|
||||
<Descriptions.Item key="verifyNum" label="核销次数">
|
||||
{coupon ? String(coupon.verifyNum) : '-'}
|
||||
</Descriptions.Item>,
|
||||
<Descriptions.Item key="certLink" label="平台链接">
|
||||
{coupon?.certLink || '-'}
|
||||
</Descriptions.Item>,
|
||||
]
|
||||
}
|
||||
|
||||
function extractPlatformCoupon(raw: unknown): AdminDouyinMashidaPlatformCoupon | null {
|
||||
if (!raw || typeof raw !== 'object') {
|
||||
return null
|
||||
}
|
||||
|
||||
const data = raw as { data?: unknown }
|
||||
const source = (data.data && typeof data.data === 'object' ? data.data : raw) as Record<
|
||||
string,
|
||||
unknown
|
||||
>
|
||||
const platform = source.platform
|
||||
|
||||
return platform && typeof platform === 'object'
|
||||
? (platform as AdminDouyinMashidaPlatformCoupon)
|
||||
: null
|
||||
}
|
||||
|
||||
function resolveStatusColor(status: string): string {
|
||||
switch (String(status || '').trim()) {
|
||||
case 'verified':
|
||||
return 'green'
|
||||
case 'synced':
|
||||
return 'blue'
|
||||
case 'invalid':
|
||||
return 'red'
|
||||
case 'issued':
|
||||
return 'gold'
|
||||
default:
|
||||
return 'default'
|
||||
}
|
||||
}
|
||||
|
||||
function resolveStatusLabel(status: string): string {
|
||||
switch (String(status || '').trim()) {
|
||||
case 'verified':
|
||||
return '已核销'
|
||||
case 'synced':
|
||||
return '已同步'
|
||||
case 'invalid':
|
||||
return '已作废'
|
||||
case 'issued':
|
||||
return '已生成'
|
||||
default:
|
||||
return status || '未知'
|
||||
}
|
||||
}
|
||||
|
||||
function resolveSyncLabel(syncStatus: string): string {
|
||||
switch (String(syncStatus || '').trim()) {
|
||||
case 'success':
|
||||
return '成功'
|
||||
case 'failed':
|
||||
return '失败'
|
||||
case 'pending':
|
||||
return '待同步'
|
||||
default:
|
||||
return syncStatus || '-'
|
||||
}
|
||||
}
|
||||
|
||||
/** 平台侧状态:3 已使用是真核销,其余都按待处理/异常提示。 */
|
||||
function resolvePlatformColor(platformStatus: number | null): string {
|
||||
switch (Number(platformStatus)) {
|
||||
case 2:
|
||||
return 'blue'
|
||||
case 3:
|
||||
case 6:
|
||||
return 'green'
|
||||
case 4:
|
||||
case 5:
|
||||
return 'red'
|
||||
case 1:
|
||||
return 'gold'
|
||||
default:
|
||||
return 'default'
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user