feat(douyin-mashida): 新增抖音券核销后台界面(列表/核销/冲正/同步/对账/作废)

抖音订单并非纯自动发货:内部 SKU 命中 AFFILIATE_DASH_SKU_MAPPING 才走自动通道,
未命中显式回落人工,且核销依赖买家自己到 /redeem 输订单号。此前核销侧在后台完全
不可见:自动核销是 void .catch(warn) 只落日志,cancelVerify 出网方法已就绪但没有入口。

照快手电子凭证的骨架做一套抖音版(两张表两套状态机,不复用):
- 券码状态、平台侧状态、同步错误全部可视化,支持只看同步失败
- 核销/冲正/重新同步/平台对账/重发券码/作废,每个写操作都带审计
- 券码列显示「核销实际会用的券码」(platform_cert_no 优先),平台改过名时额外标注
- 冲正先打平台 cancelVerify 再回退本地,避免本地与平台状态漂移
This commit is contained in:
yml2213
2026-10-12 19:45:06 +08:00
parent 0dd26da4ee
commit 5a73fb74a9
14 changed files with 1896 additions and 0 deletions
+7
View File
@@ -23,6 +23,9 @@ const AdminDashboardPage = lazy(() => import('@/pages/admin/AdminDashboardPage')
const AdminFinancePage = lazy(() => import('@/pages/admin/AdminFinancePage'))
const AdminAuditLogsPage = lazy(() => import('@/pages/admin/AdminAuditLogsPage'))
const AdminKuaishouIndustryPage = lazy(() => import('@/pages/admin/AdminKuaishouIndustryPage'))
const AdminDouyinMashidaCertificatePage = lazy(
() => import('@/pages/admin/AdminDouyinMashidaCertificatePage'),
)
const AdminLoginPage = lazy(() => import('@/pages/admin/AdminLoginPage'))
const AdminOrderDetailPage = lazy(() => import('@/pages/admin/AdminOrderDetailPage'))
const AdminOrdersPage = lazy(() => import('@/pages/admin/AdminOrdersPage'))
@@ -141,6 +144,10 @@ function AdminApplication() {
<Route path="worker-platform" element={<AdminWorkerPlatformPage />} />
<Route element={<RequireRole roles={['admin', 'operator', 'support']} />}>
<Route path="kuaishou-industry" element={<AdminKuaishouIndustryPage />} />
<Route
path="douyin-mashida-certificates"
element={<AdminDouyinMashidaCertificatePage />}
/>
<Route path="dev-mock" element={<AdminDevMockPage />} />
</Route>
<Route element={<RequireRole roles={['admin']} />}>
@@ -139,6 +139,11 @@ export default function AdminLayout() {
icon: <SafetyCertificateOutlined />,
label: '电子凭证',
},
{
key: '/admin/douyin-mashida-certificates',
icon: <SafetyCertificateOutlined />,
label: '抖音券核销',
},
]
const items: MenuProps['items'] = [
@@ -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'
}
}
@@ -0,0 +1,72 @@
import { apiGet, apiPost } from '@/lib/http'
import type {
AdminDouyinMashidaCertificateActionPayload,
AdminDouyinMashidaCertificateCheckResult,
AdminDouyinMashidaCertificateInvalidateResult,
AdminDouyinMashidaCertificateListPayload,
AdminDouyinMashidaCertificateListResult,
AdminDouyinMashidaCertificateResendResult,
AdminDouyinMashidaCertificateSyncResult,
AdminDouyinMashidaCertificateVerifyResult,
AdminDouyinMashidaOrderDetailResult,
} from '@/types/admin'
/** 后台「抖音券核销」接口。与快手电子凭证(kuaishou-industry)是两套独立的表与状态机。 */
const BASE = '/api/v1/admin/douyin-mashida'
export function fetchAdminDouyinMashidaCertificates(
params: AdminDouyinMashidaCertificateListPayload = {},
) {
return apiGet<AdminDouyinMashidaCertificateListResult>(
`${BASE}/certificates`,
params as Record<string, unknown>,
)
}
export function verifyAdminDouyinMashidaCertificate(
payload: AdminDouyinMashidaCertificateActionPayload,
) {
return apiPost<AdminDouyinMashidaCertificateVerifyResult>(`${BASE}/certificates/verify`, payload)
}
export function cancelAdminDouyinMashidaCertificateVerify(
payload: AdminDouyinMashidaCertificateActionPayload,
) {
return apiPost<AdminDouyinMashidaCertificateVerifyResult>(
`${BASE}/certificates/cancel-verify`,
payload,
)
}
export function syncAdminDouyinMashidaCertificate(
payload: AdminDouyinMashidaCertificateActionPayload,
) {
return apiPost<AdminDouyinMashidaCertificateSyncResult>(`${BASE}/certificates/sync`, payload)
}
export function checkAdminDouyinMashidaCertificate(
payload: AdminDouyinMashidaCertificateActionPayload,
) {
return apiPost<AdminDouyinMashidaCertificateCheckResult>(`${BASE}/certificates/check`, payload)
}
export function resendAdminDouyinMashidaCertificate(
payload: AdminDouyinMashidaCertificateActionPayload,
) {
return apiPost<AdminDouyinMashidaCertificateResendResult>(`${BASE}/certificates/resend`, payload)
}
export function invalidateAdminDouyinMashidaCertificate(
payload: AdminDouyinMashidaCertificateActionPayload,
) {
return apiPost<AdminDouyinMashidaCertificateInvalidateResult>(
`${BASE}/certificates/invalidate`,
payload,
)
}
export function fetchAdminDouyinMashidaOrderDetail(
payload: AdminDouyinMashidaCertificateActionPayload,
) {
return apiPost<AdminDouyinMashidaOrderDetailResult>(`${BASE}/order-detail`, payload)
}
@@ -5,6 +5,7 @@ export * from './users'
export * from './audit-logs'
export * from './platform-config'
export * from './kuaishou-industry'
export * from './douyin-mashida-certificate'
export * from './orders'
export * from './tasks'
export * from './dev-mock'
@@ -0,0 +1,126 @@
import type { AdminPagination } from './common'
/**
* 后台「抖音码速达兑换券」视图模型。
*
* 与后端 `services/admin/douyin-mashida-certificate-service.ts` 的出参一一对应。
* 券码**不掩码**:这是后台运营界面,不是买家侧兑换页(`/redeem` 那边才掩码)。
*/
export interface AdminDouyinMashidaCertificate {
id: number
provider: string
/** 我方生成的券码(DYC 前缀) */
certNo: string
/** 核销实际会用的券码(平台侧优先,平台可能改写过) */
verifyCertNo: string
/** 平台侧实际券码 */
platformCertNo: string
platformCertId: string
/** 平台侧券状态:1未激活/2未使用/3已使用/4已废弃/5已过期/6已完成 */
platformStatus: number | null
platformStatusLabel: string
platformSyncedAt: string | null
/** syncV2 / order.message 用的订单号(子单优先、父单兜底) */
orderId: string
parentOrderId: string
localOrderId: number | null
orderItemId: number | null
/** 交给平台的兑换/领取链接 */
certLink: string
/** issued 已生成 / synced 已同步 / verified 已核销 / invalid 已作废 */
status: string
/** pending / success / failed */
syncStatus: string
syncError: string
grantTime: string | null
syncedAt: string | null
verifiedAt: string | null
/** 仅列表 join 有值 */
taskNo: string
skuCode: string
skuName: string
rawPayload: Record<string, unknown>
createdAt: string
updatedAt: string
}
/** 平台侧 `coupons.list` 返回的那张券(对账用)。 */
export interface AdminDouyinMashidaPlatformCoupon {
certNo: string
certId: string
status: number | null
statusLabel: string
verifyNum: number
certLink: string
verified: boolean
}
export interface AdminDouyinMashidaCertificateListResult {
items: AdminDouyinMashidaCertificate[]
pagination: AdminPagination
}
export interface AdminDouyinMashidaCertificateListPayload {
certNo?: string
orderId?: string
platformOrderId?: string
localOrderId?: number | string
orderItemId?: number | string
status?: string
syncStatus?: string
page?: number | string
pageSize?: number | string
}
export interface AdminDouyinMashidaCertificateActionPayload {
certNo?: string
orderId?: string
platformOrderId?: string
localOrderId?: number | string
orderItemId?: number | string
batchNo?: string
reason?: string
}
export interface AdminDouyinMashidaCertificateVerifyResult {
success: boolean
certificate: AdminDouyinMashidaCertificate
}
export interface AdminDouyinMashidaCertificateCancelResult {
success: boolean
certificate: AdminDouyinMashidaCertificate
}
export interface AdminDouyinMashidaCertificateSyncResult {
success: boolean
certificate: AdminDouyinMashidaCertificate
platform: AdminDouyinMashidaPlatformCoupon | null
}
export interface AdminDouyinMashidaCertificateCheckResult {
success: boolean
certificate: AdminDouyinMashidaCertificate
platform: AdminDouyinMashidaPlatformCoupon | null
platformRowCount: number
}
export interface AdminDouyinMashidaCertificateResendResult {
success: boolean
certificate: AdminDouyinMashidaCertificate
order: {
id: number | null
platformOrderId: string
}
}
export interface AdminDouyinMashidaCertificateInvalidateResult {
success: boolean
certificate: AdminDouyinMashidaCertificate
}
export interface AdminDouyinMashidaOrderDetailResult {
success: boolean
orderId: string
detail: Record<string, unknown>
}
+16
View File
@@ -52,6 +52,22 @@ export type {
AdminKuaishouIndustryVoucherToolPayload,
} from './kuaishou-industry'
// Douyin Mashida redeem certificates (抖音码速达兑换券)
export type {
AdminDouyinMashidaCertificate,
AdminDouyinMashidaPlatformCoupon,
AdminDouyinMashidaCertificateListResult,
AdminDouyinMashidaCertificateListPayload,
AdminDouyinMashidaCertificateActionPayload,
AdminDouyinMashidaCertificateVerifyResult,
AdminDouyinMashidaCertificateCancelResult,
AdminDouyinMashidaCertificateSyncResult,
AdminDouyinMashidaCertificateCheckResult,
AdminDouyinMashidaCertificateResendResult,
AdminDouyinMashidaCertificateInvalidateResult,
AdminDouyinMashidaOrderDetailResult,
} from './douyin-mashida-certificate'
// Platform config types
export type {
AdminNotificationBarkRecipient,