feat(kuaishou): 增加历史电子券补发并核销

This commit is contained in:
yml2213
2026-11-06 10:38:26 +08:00
parent 1bf1280877
commit 398eafa911
10 changed files with 607 additions and 1 deletions
@@ -6,15 +6,18 @@ import {
SearchOutlined,
SendOutlined,
SyncOutlined,
ThunderboltOutlined,
} from '@ant-design/icons'
import {
Alert,
Button,
Card,
Checkbox,
Collapse,
Descriptions,
Input,
InputNumber,
Modal,
Popconfirm,
Select,
Space,
@@ -39,6 +42,7 @@ import {
fetchAdminKuaishouIndustryShops,
fetchAdminKuaishouIndustryVouchers,
listAdminKuaishouIndustryRefunds,
reissueAndConsumeAdminKuaishouIndustryVoucher,
resendAdminKuaishouIndustryVoucherCode,
reverseAdminKuaishouIndustryVoucher,
} from '@/services/admin'
@@ -69,6 +73,7 @@ type IndustryActionKind =
| 'destroy'
| 'reverse'
| 'resend'
| 'reissue-consume'
| 'refund-list'
| 'refund-approve'
| 'refund-disagree'
@@ -120,6 +125,11 @@ export default function AdminKuaishouIndustryPage() {
const [actionLoading, setActionLoading] = useState('')
const [consumingVoucherId, setConsumingVoucherId] = useState<number | null>(null)
const [reversingVoucherId, setReversingVoucherId] = useState<number | null>(null)
const [reissueConsumeOpen, setReissueConsumeOpen] = useState(false)
const [reissueConsumeMinutes, setReissueConsumeMinutes] = useState<number>(10)
const [reissueConsumeReason, setReissueConsumeReason] =
useState('历史发码失败补偿,补发后立即核销')
const [reissueConsumeConfirmed, setReissueConsumeConfirmed] = useState(false)
const [vouchers, setVouchers] = useState<AdminKuaishouIndustryVoucher[]>([])
const [selectedVoucher, setSelectedVoucher] = useState<AdminKuaishouIndustryVoucher | null>(null)
const [voucherTotal, setVoucherTotal] = useState(0)
@@ -646,6 +656,64 @@ export default function AdminKuaishouIndustryPage() {
}
}
function openReissueConsumeModal() {
if (!selectedVoucher && !String(toolForm.voucherCode || '').trim()) {
showError('请先选择或填写本地券码')
return
}
if (selectedVoucher) {
const sendSucceeded = selectedVoucher.sendCallbackStatus === 'success'
const stillValid = Number(selectedVoucher.validEndTime || 0) > Date.now()
if (selectedVoucher.status !== 'UNUSED') {
showError('仅未使用电子凭证可以补发并核销')
return
}
if (sendSucceeded && stillValid) {
showError('该券已发码成功且仍在有效期,请直接执行手动核销')
return
}
}
setReissueConsumeConfirmed(false)
setReissueConsumeOpen(true)
}
async function runReissueConsume() {
if (!reissueConsumeConfirmed) {
showError('请先确认已核实订单仍需履约')
return
}
setActionLoading('reissue-consume')
try {
const response = await reissueAndConsumeAdminKuaishouIndustryVoucher({
...buildVoucherToolPayload(),
validMinutes: reissueConsumeMinutes,
reason: reissueConsumeReason,
})
publishActionResult(
buildIndustryActionResultView('reissue-consume', '历史券补发并核销', response.data),
)
if (response.data.success) {
showSuccess('原券码已同步到快手并完成核销')
setReissueConsumeOpen(false)
} else {
showError(response.data.error || '历史券补发并核销失败')
}
await loadVouchers()
} catch (error) {
const message = error instanceof Error ? error.message : '历史券补发并核销失败'
publishActionResult(
buildIndustryActionResultView('reissue-consume', '历史券补发并核销', {
success: false,
error: message,
}),
)
showError(message)
} finally {
setActionLoading('')
}
}
async function runDestroy() {
setActionLoading('destroy')
try {
@@ -870,6 +938,98 @@ export default function AdminKuaishouIndustryPage() {
onChange={(key) => setActiveTab(key as IndustryTab)}
items={tabItems}
/>
<Modal
title="历史券补发并核销"
open={reissueConsumeOpen}
okText="补发并核销"
cancelText="取消"
confirmLoading={actionLoading === 'reissue-consume'}
okButtonProps={{
disabled:
!reissueConsumeConfirmed ||
!String(reissueConsumeReason || '').trim() ||
reissueConsumeMinutes < 1 ||
reissueConsumeMinutes > 60,
}}
onOk={runReissueConsume}
onCancel={() => setReissueConsumeOpen(false)}
destroyOnHidden
>
<Space direction="vertical" size={16} className="full-width">
<Alert
type="warning"
showIcon
message="该操作会重发原券码并立即核销"
description="仅用于已确认订单仍需履约、但历史发码失败或凭证已过期的补偿场景。操作会先临时延长本地券有效期,发码成功后立即核销。"
/>
<Descriptions
size="small"
bordered
column={1}
items={[
{
key: 'voucherCode',
label: '本地券码',
children:
selectedVoucher?.voucherCode || String(toolForm.voucherCode || '').trim() || '-',
},
{
key: 'oid',
label: '平台订单',
children:
selectedVoucher?.oid ||
String(toolForm.oid || toolForm.orderId || '').trim() ||
'-',
},
{
key: 'validEndTime',
label: '原有效期至',
children: selectedVoucher
? formatAdminDateTime(
new Date(Number(selectedVoucher.validEndTime || 0)).toISOString(),
)
: '-',
},
{
key: 'sendError',
label: '最近发码错误',
children: selectedVoucher?.sendCallbackLastError || '-',
},
]}
/>
<label className="kuaishou-industry-tool-field">
<span>临时有效期(分钟)</span>
<InputNumber
min={1}
max={60}
precision={0}
value={reissueConsumeMinutes}
onChange={(value) => setReissueConsumeMinutes(Number(value || 10))}
style={{ width: '100%' }}
/>
</label>
<label className="kuaishou-industry-tool-field">
<span>补发原因</span>
<Input
maxLength={200}
showCount
value={reissueConsumeReason}
onChange={(event) => setReissueConsumeReason(event.target.value)}
/>
</label>
<Checkbox
checked={reissueConsumeConfirmed}
onChange={(event) => setReissueConsumeConfirmed(event.target.checked)}
>
我已核实该订单仍需履约,并确认补发后立即核销
</Checkbox>
</Space>
</Modal>
</div>
)
@@ -1210,6 +1370,15 @@ export default function AdminKuaishouIndustryPage() {
>
手动核销
</Button>
<Button
danger
icon={<ThunderboltOutlined />}
loading={actionLoading === 'reissue-consume'}
disabled={!hasVoucherTarget}
onClick={openReissueConsumeModal}
>
历史补发并核销
</Button>
<Button
danger
icon={<RollbackOutlined />}
@@ -1677,6 +1846,7 @@ function buildIndustryActionResultView(
const responseData = asRecord(response?.data)
const success = resolveActionSuccess(kind, record, openApi)
const platformMessage = pickFirstString([
...(kind === 'reissue-consume' ? [record.error] : []),
response?.error_msg,
response?.msg,
response?.sub_msg,
@@ -1788,6 +1958,24 @@ function buildIndustryActionResultView(
}
}
if (kind === 'reissue-consume') {
pushHighlight(highlights, '执行阶段', String(record.stage || '-'))
if (record.validMinutes) {
pushHighlight(highlights, '临时有效期', `${String(record.validMinutes)} 分钟`)
}
for (const [label, value] of [
['发码成功', record.sendSuccess],
['核销成功', record.consumeSuccess],
] as const) {
if (typeof value === 'boolean') {
pushHighlight(highlights, label, value ? '是' : '否')
}
}
if (record.reason) {
pushHighlight(highlights, '补发原因', String(record.reason))
}
}
if (platformMessage) {
pushHighlight(highlights, '平台消息', platformMessage)
}
@@ -1855,6 +2043,12 @@ function buildActionSummary(
: context.platformMessage || '重发发码失败。'
}
if (kind === 'reissue-consume') {
return success
? '原券码已同步到快手并完成核销。'
: context.platformMessage || '历史券补发并核销失败。'
}
if (kind === 'refund-list') {
return success
? `查询成功,共 ${context.refundCount ?? 0} 条售后单,已填充列表。`
@@ -8,6 +8,8 @@ import type {
AdminKuaishouIndustryVoucherConsumeResult,
AdminKuaishouIndustryVoucherDestroyResult,
AdminKuaishouIndustryVoucherListResult,
AdminKuaishouIndustryVoucherReissueConsumePayload,
AdminKuaishouIndustryVoucherReissueConsumeResult,
AdminKuaishouIndustryVoucherResendResult,
AdminKuaishouIndustryVoucherToolPayload,
} from '@/types/admin'
@@ -84,6 +86,15 @@ export function resendAdminKuaishouIndustryVoucherCode(
)
}
export function reissueAndConsumeAdminKuaishouIndustryVoucher(
payload: AdminKuaishouIndustryVoucherReissueConsumePayload,
) {
return apiPost<AdminKuaishouIndustryVoucherReissueConsumeResult>(
'/api/v1/admin/kuaishou-industry/vouchers/reissue-consume',
payload,
)
}
export function destroyAdminKuaishouIndustryVoucher(
payload: AdminKuaishouIndustryVoucherToolPayload,
) {
+2
View File
@@ -81,6 +81,8 @@ export type {
AdminKuaishouIndustryShopListResult,
AdminKuaishouIndustryVoucherConsumeResult,
AdminKuaishouIndustryVoucherDestroyResult,
AdminKuaishouIndustryVoucherReissueConsumeResult,
AdminKuaishouIndustryVoucherReissueConsumePayload,
AdminKuaishouIndustryVoucherResendResult,
AdminKuaishouIndustryRefundListPayload,
AdminKuaishouIndustryRefundApprovePayload,
@@ -94,6 +94,26 @@ export interface AdminKuaishouIndustryVoucherResendResult {
voucher: AdminKuaishouIndustryVoucher | null
}
export interface AdminKuaishouIndustryVoucherReissueConsumeResult {
success: boolean
stage: 'send' | 'consume' | 'completed' | string
reason: string
validMinutes: number
originalValidEndTime: number
validEndTime: number
sendSuccess: boolean
consumeSuccess: boolean
response: Record<string, unknown> | null
error: string
voucher: AdminKuaishouIndustryVoucher | null
task: null | {
taskId: number
taskNo: string
status: string
deliveryStatus: string
}
}
export interface AdminKuaishouIndustryRefundListPayload {
sellerId?: string
beginTime?: number | string
@@ -149,3 +169,8 @@ export interface AdminKuaishouIndustryVoucherToolPayload {
expressNo?: string
etickets?: Array<{ id?: string; code?: string; num?: number | string }>
}
export interface AdminKuaishouIndustryVoucherReissueConsumePayload extends AdminKuaishouIndustryVoucherToolPayload {
validMinutes: number | string
reason: string
}
@@ -0,0 +1,45 @@
import assert from 'node:assert/strict'
import { readFileSync } from 'node:fs'
import test from 'node:test'
test('快手电子凭证页提供受控的历史券补发并核销入口', () => {
const page = readFileSync(
new URL('../src/pages/admin/AdminKuaishouIndustryPage.tsx', import.meta.url).pathname,
'utf8',
)
const service = readFileSync(
new URL('../src/services/admin/kuaishou-industry.ts', import.meta.url).pathname,
'utf8',
)
assert.ok(page.includes('历史补发并核销'), '页面必须提供补发并核销按钮')
assert.ok(page.includes('reissueAndConsumeAdminKuaishouIndustryVoucher'), '页面必须调用专用接口')
assert.ok(page.includes('reissueConsumeOpen'), '操作必须通过确认弹窗执行')
assert.ok(page.includes('reissueConsumeConfirmed'), '提交前必须显式勾选确认')
assert.ok(page.includes('临时有效期(分钟)'), '必须允许设置短期临时有效期')
assert.ok(
page.includes("source: 'admin_historical_reissue_consume'") === false,
'前端不能伪造核销来源',
)
assert.ok(
service.includes('/api/v1/admin/kuaishou-industry/vouchers/reissue-consume'),
'必须调用后端补发并核销接口',
)
})
test('历史券补发并核销结果必须展示发码、核销两个阶段', () => {
const page = readFileSync(
new URL('../src/pages/admin/AdminKuaishouIndustryPage.tsx', import.meta.url).pathname,
'utf8',
)
assert.ok(page.includes("kind === 'reissue-consume'"), '结果视图必须识别补发并核销动作')
assert.ok(
page.includes("...(kind === 'reissue-consume' ? [record.error] : [])"),
'核销阶段失败时必须优先展示消费错误,而不是发码成功的 success 响应',
)
assert.ok(page.includes("['发码成功', record.sendSuccess]"), '结果必须展示发码阶段状态')
assert.ok(page.includes("['核销成功', record.consumeSuccess]"), '结果必须展示核销阶段状态')
assert.ok(page.includes("pushHighlight(highlights, '补发原因'"), '结果必须展示补偿原因')
})