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
@@ -9,6 +9,7 @@ import {
listAdminKuaishouIndustryRefunds,
listAdminKuaishouIndustryShops,
listAdminKuaishouIndustryVouchers,
reissueAndConsumeAdminKuaishouIndustryVoucher,
resendAdminKuaishouIndustryVoucherCode,
reverseAdminKuaishouIndustryVoucher,
} from '../../services/admin/kuaishou-industry-admin-service.js'
@@ -21,6 +22,7 @@ import type {
AdminKuaishouIndustryVoucherCheckAvailableRouteBody,
AdminKuaishouIndustryVoucherConsumeRouteBody,
AdminKuaishouIndustryVoucherDestroyRouteBody,
AdminKuaishouIndustryVoucherReissueConsumeRouteBody,
AdminKuaishouIndustryVoucherResendRouteBody,
AdminKuaishouIndustryVoucherReverseRouteBody,
} from '../../types/admin/route-inputs.js'
@@ -204,6 +206,24 @@ router.post(
),
)
router.post(
'/kuaishou-industry/vouchers/reissue-consume',
requireAdminRoles(['admin', 'operator']),
createJsonHandler(
(req) =>
reissueAndConsumeAdminKuaishouIndustryVoucher(
req.body as AdminKuaishouIndustryVoucherReissueConsumeRouteBody,
),
{
successMessage: '历史券补发并核销已执行',
errorMessage: '历史券补发并核销失败',
scope: '[admin/kuaishou-industry/vouchers/reissue-consume]',
audit: (req, data) =>
buildKuaishouIndustryAudit('kuaishou_industry_voucher_reissue_consume', req.body, data),
},
),
)
router.post(
'/kuaishou-industry/vouchers/destroy',
requireAdminRoles(['admin', 'operator']),
@@ -238,6 +258,13 @@ function buildKuaishouIndustryAudit(action: string, body: unknown, data: unknown
success: Boolean(result.success),
httpStatus: Number(result.httpStatus || 0) || 0,
error: String(result.error || ''),
stage: String(result.stage || ''),
reason: String(result.reason || ''),
validMinutes: Number(result.validMinutes || 0) || 0,
originalValidEndTime: Number(result.originalValidEndTime || 0) || 0,
validEndTime: Number(result.validEndTime || 0) || 0,
sendSuccess: Boolean(result.sendSuccess),
consumeSuccess: Boolean(result.consumeSuccess),
},
}
}
@@ -27,6 +27,7 @@ import { resendKuaishouIndustryVoucherSendCallback } from '../platforms/kuaishou
import {
consumeKuaishouIndustryVoucher,
destroyKuaishouIndustryVoucher,
isKuaishouIndustryVoucherSendCallbackSuccess,
resolveKuaishouIndustryEticketType,
} from '../platforms/kuaishou-industry/voucher-service.js'
import {
@@ -39,6 +40,8 @@ import { attachKuaishouIndustryVoucherToTask } from '../platforms/kuaishou-indus
import type { KuaishouIndustryOpenApiCallResult } from '../platforms/kuaishou-industry/openapi-client.js'
import type { KuaishouIndustryVoucherRow, TaskRow } from '../../types/repository/rows.js'
const historicalReissueInFlight = new Set<string>()
export async function listAdminKuaishouIndustryVouchers(
input: KuaishouIndustryVoucherAdminListQuery = {},
) {
@@ -201,11 +204,204 @@ export async function resendAdminKuaishouIndustryVoucherCode(input: JsonObject =
return {
success: result.success,
response: 'response' in result ? result.response || null : null,
error: result.error || '',
error:
result.error ||
resolveKuaishouOpenApiErrorMessage('response' in result ? result.response : null),
voucher: result.voucher ? mapAdminKuaishouIndustryVoucher(result.voucher) : null,
}
}
/**
* 历史发码失败券的受控补偿入口。
*
* 只允许未使用券进入;先把本地有效期临时改到未来,再复用原券码发码。
* 发码成功后立即核销,避免为了补回调把历史券重新暴露给买家。
*/
export async function reissueAndConsumeAdminKuaishouIndustryVoucher(input: JsonObject = {}) {
const voucher = await resolveRequiredVoucher(input)
const voucherCode = String(voucher.voucher_code || '').trim()
const accountStatus = String(voucher.status || '')
.trim()
.toUpperCase()
const nowMs = Date.now()
const originalValidEndTime = Number(voucher.valid_end_time || 0) || 0
if (accountStatus !== 'UNUSED') {
throw createHttpError('仅未使用电子凭证可以补发并核销', {
statusCode: 409,
errorCode: 'admin_kuaishou_industry_reissue_voucher_not_unused',
})
}
if (isKuaishouIndustryVoucherSendCallbackSuccess(voucher) && originalValidEndTime > nowMs) {
throw createHttpError('该电子凭证已发码成功且仍在有效期,请直接执行手动核销', {
statusCode: 409,
errorCode: 'admin_kuaishou_industry_reissue_voucher_already_active',
})
}
if (historicalReissueInFlight.has(voucherCode)) {
throw createHttpError('该电子凭证正在补发,请勿重复提交', {
statusCode: 409,
errorCode: 'admin_kuaishou_industry_reissue_in_progress',
})
}
const validity = resolveKuaishouIndustryHistoricalReissueWindow(input.validMinutes, nowMs)
const reason = normalizeKuaishouIndustryHistoricalReissueReason(input.reason)
const originalValidStartTime = Number(voucher.valid_start_time || 0) || 0
const validStartTime =
originalValidStartTime > 0 ? Math.min(originalValidStartTime, nowMs - 1_000) : nowMs - 1_000
historicalReissueInFlight.add(voucherCode)
try {
const prepared = await updateKuaishouIndustryVoucherByCode(voucherCode, {
validStartTime,
validEndTime: validity.validEndTime,
sendCallbackStatus: 'pending',
sendCallbackLastError: '',
sendCallbackResponseJson: {},
sendCallbackSentAt: null,
updatedAt: new Date().toISOString(),
})
if (!prepared) {
throw createHttpError('准备历史券补发时未找到电子凭证', {
statusCode: 404,
errorCode: 'admin_kuaishou_industry_reissue_voucher_not_found',
})
}
const sendResult = await resendAdminKuaishouIndustryVoucherCode({
voucherCode,
oid: voucher.oid,
orderId: voucher.oid,
})
if (!sendResult.success) {
return {
success: false,
stage: 'send',
reason,
validMinutes: validity.validMinutes,
originalValidEndTime,
validEndTime: validity.validEndTime,
sendSuccess: false,
consumeSuccess: false,
response: sendResult.response,
error: sendResult.error || '电子凭证发码回调失败',
voucher: sendResult.voucher,
task: null,
}
}
const sentVoucher = await findKuaishouIndustryVoucherByCode(voucherCode, voucher.oid)
if (!sentVoucher || !isKuaishouIndustryVoucherSendCallbackSuccess(sentVoucher)) {
return {
success: false,
stage: 'send',
reason,
validMinutes: validity.validMinutes,
originalValidEndTime,
validEndTime: validity.validEndTime,
sendSuccess: false,
consumeSuccess: false,
response: sendResult.response,
error: '电子凭证发码回调返回成功,但本地状态未更新',
voucher: sentVoucher ? mapAdminKuaishouIndustryVoucher(sentVoucher) : null,
task: null,
}
}
const task = sentVoucher.task_id ? await getTaskById(sentVoucher.task_id) : null
const consumeType = String(input.consumeType || '').trim()
const consumeInput: Parameters<typeof consumeKuaishouIndustryVoucher>[1] = {
source: 'admin_historical_reissue_consume',
token: String(input.token || sentVoucher.token || '').trim(),
consumeTime: Date.now(),
...(consumeType ? { consumeType } : {}),
...(input.serialNum ? { serialNum: String(input.serialNum).trim() } : {}),
...(input.storeName ? { storeName: String(input.storeName).trim() } : {}),
...(input.storeAddress ? { storeAddress: String(input.storeAddress).trim() } : {}),
...(input.expressCode ? { expressCode: String(input.expressCode).trim() } : {}),
...(input.expressNo ? { expressNo: String(input.expressNo).trim() } : {}),
}
if (task) {
consumeInput.task = task
}
const consumeResult = await consumeKuaishouIndustryVoucher(sentVoucher, consumeInput)
if (!consumeResult.ok || !consumeResult.voucher) {
return {
success: false,
stage: 'consume',
reason,
validMinutes: validity.validMinutes,
originalValidEndTime,
validEndTime: validity.validEndTime,
sendSuccess: true,
consumeSuccess: false,
response: sendResult.response,
error: consumeResult.errorMessage || '电子凭证发码成功,但核销失败',
voucher: consumeResult.voucher
? mapAdminKuaishouIndustryVoucher(consumeResult.voucher)
: mapAdminKuaishouIndustryVoucher(sentVoucher),
task: task ? mapTaskReference(task) : null,
}
}
return {
success: true,
stage: 'completed',
reason,
validMinutes: validity.validMinutes,
originalValidEndTime,
validEndTime: validity.validEndTime,
sendSuccess: true,
consumeSuccess: true,
response: sendResult.response,
error: '',
voucher: mapAdminKuaishouIndustryVoucher(consumeResult.voucher),
task: task ? mapTaskReference(task) : null,
}
} finally {
historicalReissueInFlight.delete(voucherCode)
}
}
export function resolveKuaishouIndustryHistoricalReissueWindow(
rawMinutes: unknown,
nowMs = Date.now(),
) {
const validMinutes = Number(rawMinutes)
if (!Number.isInteger(validMinutes) || validMinutes < 1 || validMinutes > 60) {
throw createHttpError('临时有效期必须是 1 到 60 分钟的整数', {
statusCode: 400,
errorCode: 'admin_kuaishou_industry_reissue_valid_minutes_invalid',
})
}
return {
validMinutes,
validEndTime: nowMs + validMinutes * 60_000,
}
}
export function normalizeKuaishouIndustryHistoricalReissueReason(rawReason: unknown) {
const reason = String(rawReason || '').trim()
if (!reason) {
throw createHttpError('补发原因不能为空', {
statusCode: 400,
errorCode: 'admin_kuaishou_industry_reissue_reason_required',
})
}
if (reason.length > 200) {
throw createHttpError('补发原因不能超过 200 个字符', {
statusCode: 400,
errorCode: 'admin_kuaishou_industry_reissue_reason_too_long',
})
}
return reason
}
export async function destroyAdminKuaishouIndustryVoucherByCode(input: JsonObject = {}) {
const voucher = await resolveRequiredVoucher(input)
const checkPayload = buildVoucherOpenApiPayload(input, voucher)
@@ -485,6 +681,14 @@ function mapOpenApiResult(result: KuaishouIndustryOpenApiCallResult) {
}
}
function resolveKuaishouOpenApiErrorMessage(response: unknown) {
if (!response || typeof response !== 'object' || Array.isArray(response)) {
return ''
}
const current = response as Record<string, unknown>
return String(current.error_msg || current.sub_msg || current.message || current.msg || '').trim()
}
function sanitizeOpenApiRequest(request: JsonObject | undefined): JsonObject | null {
if (!request) {
return null
@@ -0,0 +1,76 @@
import assert from 'node:assert/strict'
import { readFile } from 'node:fs/promises'
import test from 'node:test'
import {
normalizeKuaishouIndustryHistoricalReissueReason,
resolveKuaishouIndustryHistoricalReissueWindow,
} from './kuaishou-industry-admin-service.js'
test('历史券补发窗口只允许 1 到 60 分钟', () => {
const nowMs = 1_800_000_000_000
assert.deepEqual(resolveKuaishouIndustryHistoricalReissueWindow(10, nowMs), {
validMinutes: 10,
validEndTime: nowMs + 10 * 60_000,
})
assert.throws(() => resolveKuaishouIndustryHistoricalReissueWindow(0, nowMs), /1 到 60 分钟/)
assert.throws(() => resolveKuaishouIndustryHistoricalReissueWindow(61, nowMs), /1 到 60 分钟/)
assert.throws(() => resolveKuaishouIndustryHistoricalReissueWindow('1.5', nowMs), /1 到 60 分钟/)
})
test('历史券补发原因必填且限制长度', () => {
assert.equal(
normalizeKuaishouIndustryHistoricalReissueReason(' 历史发码失败补发 '),
'历史发码失败补发',
)
assert.throws(() => normalizeKuaishouIndustryHistoricalReissueReason(' '), /不能为空/)
assert.throws(
() => normalizeKuaishouIndustryHistoricalReissueReason('x'.repeat(201)),
/不能超过 200/,
)
})
test('受控补发必须先重置有效期,再发码,最后立即核销', async () => {
const source = await readFile(
new URL('./kuaishou-industry-admin-service.ts', import.meta.url),
'utf8',
)
const start = source.indexOf(
'export async function reissueAndConsumeAdminKuaishouIndustryVoucher',
)
assert.ok(start > 0, '未找到历史券补发并核销服务')
const body = source.slice(
start,
source.indexOf('\nexport function resolveKuaishouIndustryHistoricalReissueWindow', start),
)
const prepareIndex = body.indexOf('updateKuaishouIndustryVoucherByCode')
const sendIndex = body.indexOf('resendAdminKuaishouIndustryVoucherCode')
const consumeIndex = body.indexOf('consumeKuaishouIndustryVoucher(sentVoucher')
assert.ok(prepareIndex > 0, '必须先更新本地临时有效期')
assert.ok(sendIndex > prepareIndex, '必须先重置有效期再重发发码')
assert.ok(consumeIndex > sendIndex, '必须等发码成功后再执行核销')
assert.ok(body.includes("source: 'admin_historical_reissue_consume'"), '核销来源必须可审计')
assert.ok(body.includes("sendCallbackStatus: 'pending'"), '重发前必须重置发码状态')
assert.ok(body.includes("stage: 'send'"), '发码失败必须返回明确阶段')
assert.ok(body.includes("stage: 'consume'"), '核销失败必须返回明确阶段')
assert.ok(body.includes("stage: 'completed'"), '成功必须返回完成阶段')
})
test('补发并核销路由只允许管理员和运营执行', async () => {
const source = await readFile(
new URL('../../routes/admin/kuaishou-industry.ts', import.meta.url),
'utf8',
)
const routeIndex = source.indexOf("'/kuaishou-industry/vouchers/reissue-consume'")
assert.ok(routeIndex > 0, '未注册历史券补发并核销路由')
const routeBody = source.slice(routeIndex, source.indexOf('\nrouter.post(', routeIndex + 1))
assert.ok(routeBody.includes("requireAdminRoles(['admin', 'operator'])"), '必须限制管理员和运营')
assert.ok(
routeBody.includes("'kuaishou_industry_voucher_reissue_consume'"),
'必须写入独立审计动作',
)
})
@@ -20,6 +20,7 @@ import type {
AdminKuaishouIndustryVoucherCheckAvailableInput,
AdminKuaishouIndustryVoucherConsumeInput,
AdminKuaishouIndustryVoucherDestroyInput,
AdminKuaishouIndustryVoucherReissueConsumeInput,
AdminKuaishouIndustryVoucherResendInput,
AdminKuaishouIndustryVoucherReverseInput,
AdminCloudtentaclesTestLoginInput,
@@ -49,6 +50,8 @@ export type AdminKuaishouIndustryVoucherCheckAvailableRouteBody =
export type AdminKuaishouIndustryVoucherReverseRouteBody = AdminKuaishouIndustryVoucherReverseInput
export type AdminKuaishouIndustryVoucherConsumeRouteBody = AdminKuaishouIndustryVoucherConsumeInput
export type AdminKuaishouIndustryVoucherResendRouteBody = AdminKuaishouIndustryVoucherResendInput
export type AdminKuaishouIndustryVoucherReissueConsumeRouteBody =
AdminKuaishouIndustryVoucherReissueConsumeInput
export type AdminKuaishouIndustryVoucherDestroyRouteBody = AdminKuaishouIndustryVoucherDestroyInput
export type AdminNotificationConfigRouteBody = AdminNotificationConfigInput
export type AdminNotificationTestRouteBody = AdminNotificationTestInput
@@ -354,6 +354,25 @@ export type AdminKuaishouIndustryVoucherResendInput = {
voucherCode?: string
}
/**
* 历史券补偿:临时重建有效期,重发原券码发码回调,成功后立即核销。
* validMinutes 只允许较短窗口,避免发码成功但核销失败时长期暴露可用券。
*/
export type AdminKuaishouIndustryVoucherReissueConsumeInput = {
oid?: string
orderId?: string
taskId?: number | string
voucherCode?: string
validMinutes?: number | string
reason?: string
consumeType?: string
serialNum?: string
storeName?: string
storeAddress?: string
expressCode?: string
expressNo?: string
}
export type AdminKuaishouIndustryVoucherDestroyInput = {
oid?: string
orderId?: string
@@ -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, '补发原因'"), '结果必须展示补偿原因')
})