优化券码操作类型, 增加手动销毁
This commit is contained in:
@@ -0,0 +1,17 @@
|
|||||||
|
-- 003_kuaishou_industry_voucher_eticket_type.sql
|
||||||
|
-- 电子凭证类型落库,避免后台默认 DINING_OPEN_TICKET 覆盖真实类目类型。
|
||||||
|
|
||||||
|
ALTER TABLE kuaishou_industry_vouchers
|
||||||
|
ADD COLUMN IF NOT EXISTS eticket_type TEXT NOT NULL DEFAULT '';
|
||||||
|
|
||||||
|
COMMENT ON COLUMN kuaishou_industry_vouchers.eticket_type IS
|
||||||
|
'快手电子凭证类型,发码请求 eticketType(如 GAME_OPEN_TICKET_CONSUME)';
|
||||||
|
|
||||||
|
-- 从历史 raw_payload 回填
|
||||||
|
UPDATE kuaishou_industry_vouchers
|
||||||
|
SET eticket_type = COALESCE(
|
||||||
|
NULLIF(BTRIM(raw_payload_json #>> '{body,eticketType}'), ''),
|
||||||
|
NULLIF(BTRIM(raw_payload_json ->> 'eticketType'), ''),
|
||||||
|
eticket_type
|
||||||
|
)
|
||||||
|
WHERE BTRIM(COALESCE(eticket_type, '')) = '';
|
||||||
@@ -66,6 +66,7 @@ async function insertKuaishouIndustryVoucher(
|
|||||||
unit_index,
|
unit_index,
|
||||||
seller_id,
|
seller_id,
|
||||||
token,
|
token,
|
||||||
|
eticket_type,
|
||||||
status,
|
status,
|
||||||
valid_start_time,
|
valid_start_time,
|
||||||
valid_end_time,
|
valid_end_time,
|
||||||
@@ -94,17 +95,18 @@ async function insertKuaishouIndustryVoucher(
|
|||||||
$9,
|
$9,
|
||||||
$10,
|
$10,
|
||||||
$11,
|
$11,
|
||||||
$12::jsonb,
|
$12,
|
||||||
$13,
|
$13::jsonb,
|
||||||
$14,
|
$14,
|
||||||
$15,
|
$15,
|
||||||
$16,
|
$16,
|
||||||
$17,
|
$17,
|
||||||
$18::jsonb,
|
$18,
|
||||||
$19,
|
$19::jsonb,
|
||||||
$20::jsonb,
|
$20,
|
||||||
$21,
|
$21::jsonb,
|
||||||
$22
|
$22,
|
||||||
|
$23
|
||||||
)
|
)
|
||||||
ON CONFLICT (oid, unit_index) DO UPDATE
|
ON CONFLICT (oid, unit_index) DO UPDATE
|
||||||
SET
|
SET
|
||||||
@@ -116,6 +118,10 @@ async function insertKuaishouIndustryVoucher(
|
|||||||
WHEN EXCLUDED.token <> '' THEN EXCLUDED.token
|
WHEN EXCLUDED.token <> '' THEN EXCLUDED.token
|
||||||
ELSE kuaishou_industry_vouchers.token
|
ELSE kuaishou_industry_vouchers.token
|
||||||
END,
|
END,
|
||||||
|
eticket_type = CASE
|
||||||
|
WHEN EXCLUDED.eticket_type <> '' THEN EXCLUDED.eticket_type
|
||||||
|
ELSE kuaishou_industry_vouchers.eticket_type
|
||||||
|
END,
|
||||||
order_id = COALESCE(kuaishou_industry_vouchers.order_id, EXCLUDED.order_id),
|
order_id = COALESCE(kuaishou_industry_vouchers.order_id, EXCLUDED.order_id),
|
||||||
task_id = COALESCE(kuaishou_industry_vouchers.task_id, EXCLUDED.task_id),
|
task_id = COALESCE(kuaishou_industry_vouchers.task_id, EXCLUDED.task_id),
|
||||||
valid_start_time = EXCLUDED.valid_start_time,
|
valid_start_time = EXCLUDED.valid_start_time,
|
||||||
@@ -157,6 +163,7 @@ async function insertKuaishouIndustryVoucher(
|
|||||||
input.unitIndex,
|
input.unitIndex,
|
||||||
input.sellerId || '',
|
input.sellerId || '',
|
||||||
input.token || '',
|
input.token || '',
|
||||||
|
input.eticketType || '',
|
||||||
input.status || 'UNUSED',
|
input.status || 'UNUSED',
|
||||||
input.validStartTime || 0,
|
input.validStartTime || 0,
|
||||||
input.validEndTime || 0,
|
input.validEndTime || 0,
|
||||||
@@ -407,6 +414,10 @@ function normalizeVoucherPatchColumns(
|
|||||||
columns.push({ column: 'seller_id', value: patch.sellerId || '' })
|
columns.push({ column: 'seller_id', value: patch.sellerId || '' })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (patch.eticketType !== undefined) {
|
||||||
|
columns.push({ column: 'eticket_type', value: patch.eticketType || '' })
|
||||||
|
}
|
||||||
|
|
||||||
if (patch.status !== undefined) {
|
if (patch.status !== undefined) {
|
||||||
columns.push({ column: 'status', value: patch.status || 'UNUSED' })
|
columns.push({ column: 'status', value: patch.status || 'UNUSED' })
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import {
|
|||||||
approveAdminKuaishouIndustryRefund,
|
approveAdminKuaishouIndustryRefund,
|
||||||
checkAdminKuaishouIndustryVoucherAvailable,
|
checkAdminKuaishouIndustryVoucherAvailable,
|
||||||
consumeAdminKuaishouIndustryVoucherByCode,
|
consumeAdminKuaishouIndustryVoucherByCode,
|
||||||
|
destroyAdminKuaishouIndustryVoucherByCode,
|
||||||
disagreeAdminKuaishouIndustryRefund,
|
disagreeAdminKuaishouIndustryRefund,
|
||||||
listAdminKuaishouIndustryRefunds,
|
listAdminKuaishouIndustryRefunds,
|
||||||
listAdminKuaishouIndustryShops,
|
listAdminKuaishouIndustryShops,
|
||||||
@@ -17,6 +18,7 @@ import type {
|
|||||||
AdminKuaishouIndustryRefundListRouteBody,
|
AdminKuaishouIndustryRefundListRouteBody,
|
||||||
AdminKuaishouIndustryVoucherCheckAvailableRouteBody,
|
AdminKuaishouIndustryVoucherCheckAvailableRouteBody,
|
||||||
AdminKuaishouIndustryVoucherConsumeRouteBody,
|
AdminKuaishouIndustryVoucherConsumeRouteBody,
|
||||||
|
AdminKuaishouIndustryVoucherDestroyRouteBody,
|
||||||
AdminKuaishouIndustryVoucherResendRouteBody,
|
AdminKuaishouIndustryVoucherResendRouteBody,
|
||||||
AdminKuaishouIndustryVoucherReverseRouteBody,
|
AdminKuaishouIndustryVoucherReverseRouteBody,
|
||||||
} from '../../types/admin/route-inputs.js'
|
} from '../../types/admin/route-inputs.js'
|
||||||
@@ -156,6 +158,23 @@ router.post(
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
router.post(
|
||||||
|
'/kuaishou-industry/vouchers/destroy',
|
||||||
|
requireAdminRoles(['admin', 'operator']),
|
||||||
|
createJsonHandler(
|
||||||
|
(req) =>
|
||||||
|
destroyAdminKuaishouIndustryVoucherByCode(
|
||||||
|
req.body as AdminKuaishouIndustryVoucherDestroyRouteBody,
|
||||||
|
),
|
||||||
|
{
|
||||||
|
successMessage: '电子凭证已手动销毁',
|
||||||
|
errorMessage: '手动销毁电子凭证失败',
|
||||||
|
scope: '[admin/kuaishou-industry/vouchers/destroy]',
|
||||||
|
audit: (req, data) => buildKuaishouIndustryAudit('kuaishou_industry_voucher_destroy', req.body, data),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
function buildKuaishouIndustryAudit(action: string, body: unknown, data: unknown) {
|
function buildKuaishouIndustryAudit(action: string, body: unknown, data: unknown) {
|
||||||
const payload = body && typeof body === 'object' ? body as Record<string, unknown> : {}
|
const payload = body && typeof body === 'object' ? body as Record<string, unknown> : {}
|
||||||
const result = data && typeof data === 'object' ? data as Record<string, unknown> : {}
|
const result = data && typeof data === 'object' ? data as Record<string, unknown> : {}
|
||||||
|
|||||||
@@ -19,13 +19,18 @@ import {
|
|||||||
type KuaishouIndustrySourceConfig,
|
type KuaishouIndustrySourceConfig,
|
||||||
} from '../platforms/kuaishou-industry/source-config-service.js'
|
} from '../platforms/kuaishou-industry/source-config-service.js'
|
||||||
import { resendKuaishouIndustryVoucherSendCallback } from '../platforms/kuaishou-industry/send-code-service.js'
|
import { resendKuaishouIndustryVoucherSendCallback } from '../platforms/kuaishou-industry/send-code-service.js'
|
||||||
import { consumeKuaishouIndustryVoucher } from '../platforms/kuaishou-industry/voucher-service.js'
|
import {
|
||||||
|
consumeKuaishouIndustryVoucher,
|
||||||
|
destroyKuaishouIndustryVoucher,
|
||||||
|
resolveKuaishouIndustryEticketType,
|
||||||
|
} from '../platforms/kuaishou-industry/voucher-service.js'
|
||||||
import {
|
import {
|
||||||
approveKuaishouIndustryRefund,
|
approveKuaishouIndustryRefund,
|
||||||
disagreeKuaishouIndustryRefund,
|
disagreeKuaishouIndustryRefund,
|
||||||
listKuaishouIndustryRefunds,
|
listKuaishouIndustryRefunds,
|
||||||
} from '../platforms/kuaishou-industry/refund-service.js'
|
} from '../platforms/kuaishou-industry/refund-service.js'
|
||||||
import { reverseKuaishouIndustryCallback } from '../platforms/kuaishou-industry/reverse-callback-service.js'
|
import { reverseKuaishouIndustryCallback } from '../platforms/kuaishou-industry/reverse-callback-service.js'
|
||||||
|
import { attachKuaishouIndustryVoucherToTask } from '../platforms/kuaishou-industry/voucher-binding-service.js'
|
||||||
import type { KuaishouIndustryOpenApiCallResult } from '../platforms/kuaishou-industry/openapi-client.js'
|
import type { KuaishouIndustryOpenApiCallResult } from '../platforms/kuaishou-industry/openapi-client.js'
|
||||||
import type { KuaishouIndustryVoucherRow, TaskRow } from '../../types/repository/rows.js'
|
import type { KuaishouIndustryVoucherRow, TaskRow } from '../../types/repository/rows.js'
|
||||||
|
|
||||||
@@ -103,10 +108,15 @@ export async function checkAdminKuaishouIndustryVoucherAvailable(input: JsonObje
|
|||||||
const payload = buildVoucherOpenApiPayload(input, voucher)
|
const payload = buildVoucherOpenApiPayload(input, voucher)
|
||||||
|
|
||||||
assertRequired(payload.sellerId, '卖家编号未填写')
|
assertRequired(payload.sellerId, '卖家编号未填写')
|
||||||
assertRequired(payload.eticketType || payload.bizTypeCode, '电子凭证类型未填写')
|
assertRequired(payload.eticketType || payload.bizTypeCode, '电子凭证类型未填写(请选择券码或手动填写类型)')
|
||||||
assertEtickets(payload.etickets, '电子凭证列表未填写')
|
assertEtickets(payload.etickets, '电子凭证列表未填写')
|
||||||
|
|
||||||
return mapOpenApiResult(await checkKuaishouIndustryEticketAvailable(payload))
|
const result = mapOpenApiResult(await checkKuaishouIndustryEticketAvailable(payload))
|
||||||
|
return {
|
||||||
|
...result,
|
||||||
|
eticketType: String(payload.eticketType || payload.bizTypeCode || ''),
|
||||||
|
voucher: voucher ? mapAdminKuaishouIndustryVoucher(voucher) : null,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function reverseAdminKuaishouIndustryVoucher(input: JsonObject = {}) {
|
export async function reverseAdminKuaishouIndustryVoucher(input: JsonObject = {}) {
|
||||||
@@ -188,12 +198,55 @@ export async function resendAdminKuaishouIndustryVoucherCode(input: JsonObject =
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function destroyAdminKuaishouIndustryVoucherByCode(input: JsonObject = {}) {
|
||||||
|
const voucher = await resolveRequiredVoucher(input)
|
||||||
|
const task = voucher.task_id ? await getTaskById(voucher.task_id) : null
|
||||||
|
const destroyInput: Parameters<typeof destroyKuaishouIndustryVoucher>[1] = {
|
||||||
|
source: 'admin_tool_manual_destroy',
|
||||||
|
reason: String(input.reason || '').trim(),
|
||||||
|
token: String(input.token || voucher.token || '').trim(),
|
||||||
|
eticketType: resolveKuaishouIndustryEticketType(voucher, input.eticketType),
|
||||||
|
}
|
||||||
|
if (task) {
|
||||||
|
destroyInput.task = task
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await destroyKuaishouIndustryVoucher(voucher, destroyInput)
|
||||||
|
|
||||||
|
if (!result.ok || !result.voucher) {
|
||||||
|
throw createHttpError(result.errorMessage || '电子凭证销毁失败', {
|
||||||
|
statusCode: 409,
|
||||||
|
errorCode: 'admin_kuaishou_industry_destroy_failed',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const nextTask = result.task || task
|
||||||
|
if (nextTask && result.voucher && !result.alreadyDestroyed) {
|
||||||
|
await attachKuaishouIndustryVoucherToTask(nextTask, result.voucher, {
|
||||||
|
source: 'admin_tool_manual_destroy',
|
||||||
|
now: new Date().toISOString(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
alreadyDestroyed: Boolean(result.alreadyDestroyed),
|
||||||
|
reason: result.reason || '',
|
||||||
|
voucher: mapAdminKuaishouIndustryVoucher(result.voucher),
|
||||||
|
task: nextTask ? mapTaskReference(nextTask) : null,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function buildVoucherOpenApiPayload(
|
function buildVoucherOpenApiPayload(
|
||||||
input: JsonObject,
|
input: JsonObject,
|
||||||
voucher: KuaishouIndustryVoucherRow | null,
|
voucher: KuaishouIndustryVoucherRow | null,
|
||||||
): JsonObject {
|
): JsonObject {
|
||||||
const etickets = normalizeAdminEtickets(input.etickets)
|
const etickets = normalizeAdminEtickets(input.etickets)
|
||||||
const voucherCode = String(voucher?.voucher_code || input.voucherCode || '').trim()
|
const voucherCode = String(voucher?.voucher_code || input.voucherCode || '').trim()
|
||||||
|
const eticketType = resolveKuaishouIndustryEticketType(
|
||||||
|
voucher,
|
||||||
|
input.eticketType || input.bizTypeCode,
|
||||||
|
)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...input,
|
...input,
|
||||||
@@ -202,6 +255,8 @@ function buildVoucherOpenApiPayload(
|
|||||||
orderId: String(input.orderId || input.oid || voucher?.oid || '').trim(),
|
orderId: String(input.orderId || input.oid || voucher?.oid || '').trim(),
|
||||||
token: String(input.token || voucher?.token || '').trim(),
|
token: String(input.token || voucher?.token || '').trim(),
|
||||||
serialNum: String(input.serialNum || voucher?.consume_serial_num || '').trim(),
|
serialNum: String(input.serialNum || voucher?.consume_serial_num || '').trim(),
|
||||||
|
eticketType,
|
||||||
|
...(eticketType ? { bizTypeCode: eticketType } : {}),
|
||||||
etickets:
|
etickets:
|
||||||
etickets.length > 0
|
etickets.length > 0
|
||||||
? etickets
|
? etickets
|
||||||
@@ -414,6 +469,7 @@ function mapAdminKuaishouIndustryVoucher(
|
|||||||
skuCode: String(listRow.sku_code || '').trim(),
|
skuCode: String(listRow.sku_code || '').trim(),
|
||||||
skuName: String(listRow.sku_name || '').trim(),
|
skuName: String(listRow.sku_name || '').trim(),
|
||||||
tokenMasked: maskSecret(voucher.token),
|
tokenMasked: maskSecret(voucher.token),
|
||||||
|
eticketType: resolveKuaishouIndustryEticketType(voucher),
|
||||||
status: voucher.status,
|
status: voucher.status,
|
||||||
validStartTime: Number(voucher.valid_start_time || 0) || 0,
|
validStartTime: Number(voucher.valid_start_time || 0) || 0,
|
||||||
validEndTime: Number(voucher.valid_end_time || 0) || 0,
|
validEndTime: Number(voucher.valid_end_time || 0) || 0,
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import {
|
|||||||
} from './response.js'
|
} from './response.js'
|
||||||
import { destroyCallback } from './destroy-callback-service.js'
|
import { destroyCallback } from './destroy-callback-service.js'
|
||||||
import { attachKuaishouIndustryVoucherToTask } from './voucher-binding-service.js'
|
import { attachKuaishouIndustryVoucherToTask } from './voucher-binding-service.js'
|
||||||
|
import { resolveKuaishouIndustryEticketType } from './voucher-service.js'
|
||||||
|
|
||||||
export async function handleDestroyCode(rawBody: JsonObject = {}) {
|
export async function handleDestroyCode(rawBody: JsonObject = {}) {
|
||||||
const config = getKuaishouIndustryConfig()
|
const config = getKuaishouIndustryConfig()
|
||||||
@@ -39,6 +40,9 @@ export async function handleDestroyCode(rawBody: JsonObject = {}) {
|
|||||||
const callbackSellerId = params.sellerId
|
const callbackSellerId = params.sellerId
|
||||||
|| String(targetVouchers.find((voucher) => String(voucher.seller_id || '').trim())?.seller_id || '').trim()
|
|| String(targetVouchers.find((voucher) => String(voucher.seller_id || '').trim())?.seller_id || '').trim()
|
||||||
const callbackToken = params.token || resolveDestroyCallbackToken(targetVouchers)
|
const callbackToken = params.token || resolveDestroyCallbackToken(targetVouchers)
|
||||||
|
const eticketType = resolveKuaishouIndustryEticketType(
|
||||||
|
targetVouchers[0] || null,
|
||||||
|
)
|
||||||
|
|
||||||
for (const voucher of targetVouchers) {
|
for (const voucher of targetVouchers) {
|
||||||
const status = String(voucher.status || '').trim().toUpperCase()
|
const status = String(voucher.status || '').trim().toUpperCase()
|
||||||
@@ -81,6 +85,7 @@ export async function handleDestroyCode(rawBody: JsonObject = {}) {
|
|||||||
})),
|
})),
|
||||||
reason: params.reason,
|
reason: params.reason,
|
||||||
token: callbackToken,
|
token: callbackToken,
|
||||||
|
...(eticketType ? { eticketType } : {}),
|
||||||
})
|
})
|
||||||
|
|
||||||
return buildIndustrySuccessResponse({ oid: normalizedOid })
|
return buildIndustrySuccessResponse({ oid: normalizedOid })
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ import {
|
|||||||
buildKuaishouIndustryEticketFromVoucher,
|
buildKuaishouIndustryEticketFromVoucher,
|
||||||
isKuaishouIndustryVoucherSendCallbackSuccess,
|
isKuaishouIndustryVoucherSendCallbackSuccess,
|
||||||
KUAISHOU_INDUSTRY_SEND_CALLBACK_STATUS,
|
KUAISHOU_INDUSTRY_SEND_CALLBACK_STATUS,
|
||||||
|
resolveKuaishouIndustryEticketType,
|
||||||
resolveKuaishouIndustryVoucherValidity,
|
resolveKuaishouIndustryVoucherValidity,
|
||||||
} from './voucher-service.js'
|
} from './voucher-service.js'
|
||||||
import { bindKuaishouIndustryVouchersToOrderTasks } from './voucher-binding-service.js'
|
import { bindKuaishouIndustryVouchersToOrderTasks } from './voucher-binding-service.js'
|
||||||
@@ -83,6 +84,7 @@ export async function handleSendCode(rawBody: JsonObject = {}) {
|
|||||||
unitIndex,
|
unitIndex,
|
||||||
sellerId: params.sellerId,
|
sellerId: params.sellerId,
|
||||||
token: params.token,
|
token: params.token,
|
||||||
|
eticketType: params.eticketType,
|
||||||
orderId: order?.id || null,
|
orderId: order?.id || null,
|
||||||
taskId: task?.id || null,
|
taskId: task?.id || null,
|
||||||
status: 'UNUSED',
|
status: 'UNUSED',
|
||||||
@@ -558,7 +560,7 @@ function resolveSendCodeCallbackParams(
|
|||||||
sendType: String(body.sendType || 'VIRTUAL').trim() || 'VIRTUAL',
|
sendType: String(body.sendType || 'VIRTUAL').trim() || 'VIRTUAL',
|
||||||
sellerId: String(body.sellerId || voucher.seller_id || '').trim(),
|
sellerId: String(body.sellerId || voucher.seller_id || '').trim(),
|
||||||
token: String(body.token || voucher.token || '').trim(),
|
token: String(body.token || voucher.token || '').trim(),
|
||||||
eticketType: String(body.eticketType || '').trim(),
|
eticketType: resolveKuaishouIndustryEticketType(voucher, body.eticketType),
|
||||||
ext: String(body.ext || '').trim(),
|
ext: String(body.ext || '').trim(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ import { buildKuaishouIndustryVoucherContext } from './voucher-binding-service.j
|
|||||||
import {
|
import {
|
||||||
buildKuaishouIndustryEticketFromVoucher,
|
buildKuaishouIndustryEticketFromVoucher,
|
||||||
normalizeKuaishouIndustryConsumeType,
|
normalizeKuaishouIndustryConsumeType,
|
||||||
|
normalizeKuaishouIndustryDestroyReason,
|
||||||
|
resolveKuaishouIndustryEticketType,
|
||||||
resolveKuaishouIndustryVoucherValidity,
|
resolveKuaishouIndustryVoucherValidity,
|
||||||
} from './voucher-service.js'
|
} from './voucher-service.js'
|
||||||
|
|
||||||
@@ -82,6 +84,41 @@ test('buildKuaishouIndustryEticketFromVoucher mirrors voucher code into id and c
|
|||||||
assert.equal(eticket.eticketType, 'GAME_OPEN_TICKET_CONSUME')
|
assert.equal(eticket.eticketType, 'GAME_OPEN_TICKET_CONSUME')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('resolveKuaishouIndustryEticketType prefers explicit then column then raw payload', () => {
|
||||||
|
assert.equal(
|
||||||
|
resolveKuaishouIndustryEticketType(
|
||||||
|
{
|
||||||
|
eticket_type: 'GAME_OPEN_TICKET_CONSUME',
|
||||||
|
raw_payload_json: { body: { eticketType: 'DINING_OPEN_TICKET' } },
|
||||||
|
} as any,
|
||||||
|
'EXPLICIT_TYPE',
|
||||||
|
),
|
||||||
|
'EXPLICIT_TYPE',
|
||||||
|
)
|
||||||
|
|
||||||
|
assert.equal(
|
||||||
|
resolveKuaishouIndustryEticketType({
|
||||||
|
eticket_type: 'GAME_OPEN_TICKET_CONSUME',
|
||||||
|
raw_payload_json: { body: { eticketType: 'DINING_OPEN_TICKET' } },
|
||||||
|
} as any),
|
||||||
|
'GAME_OPEN_TICKET_CONSUME',
|
||||||
|
)
|
||||||
|
|
||||||
|
assert.equal(
|
||||||
|
resolveKuaishouIndustryEticketType({
|
||||||
|
eticket_type: '',
|
||||||
|
raw_payload_json: { body: { eticketType: 'GAME_OPEN_TICKET_CONSUME' } },
|
||||||
|
} as any),
|
||||||
|
'GAME_OPEN_TICKET_CONSUME',
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('normalizeKuaishouIndustryDestroyReason defaults to SYS_ADMIN_DESTROY', () => {
|
||||||
|
assert.equal(normalizeKuaishouIndustryDestroyReason(''), 'SYS_ADMIN_DESTROY')
|
||||||
|
assert.equal(normalizeKuaishouIndustryDestroyReason('supply_destroy'), 'SUPPLY_DESTROY')
|
||||||
|
assert.equal(normalizeKuaishouIndustryDestroyReason('unknown'), 'SYS_ADMIN_DESTROY')
|
||||||
|
})
|
||||||
|
|
||||||
test('buildKuaishouIndustryVoucherContext maps consumed voucher state', () => {
|
test('buildKuaishouIndustryVoucherContext maps consumed voucher state', () => {
|
||||||
const context = buildKuaishouIndustryVoucherContext(
|
const context = buildKuaishouIndustryVoucherContext(
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,13 +1,17 @@
|
|||||||
import { createTaskEvent } from '../../../repositories/task-event-repo.js'
|
import { createTaskEvent } from '../../../repositories/task-event-repo.js'
|
||||||
|
import { getTaskById, updateTask } from '../../../repositories/task-repo.js'
|
||||||
import { asJsonObject, type JsonObject } from '../../../types/json.js'
|
import { asJsonObject, type JsonObject } from '../../../types/json.js'
|
||||||
import {
|
import {
|
||||||
findKuaishouIndustryVoucherByCode,
|
findKuaishouIndustryVoucherByCode,
|
||||||
listKuaishouIndustryVouchersByTaskId,
|
listKuaishouIndustryVouchersByTaskId,
|
||||||
updateKuaishouIndustryVoucherByCode,
|
updateKuaishouIndustryVoucherByCode,
|
||||||
} from '../../../repositories/kuaishou-industry-voucher-repo.js'
|
} from '../../../repositories/kuaishou-industry-voucher-repo.js'
|
||||||
|
import { TASK_STATUS } from '../../../domain/task-status.js'
|
||||||
import { logWarn } from '../../../utils/logger.js'
|
import { logWarn } from '../../../utils/logger.js'
|
||||||
|
import { normalizeTimestampIso } from '../../../utils/time.js'
|
||||||
import { buildIndustryEticketItem } from './response.js'
|
import { buildIndustryEticketItem } from './response.js'
|
||||||
import { consumeCallback } from './consume-callback-service.js'
|
import { consumeCallback } from './consume-callback-service.js'
|
||||||
|
import { destroyCallback } from './destroy-callback-service.js'
|
||||||
import type { KuaishouIndustryVoucherRow, TaskRow } from '../../../types/repository/rows.js'
|
import type { KuaishouIndustryVoucherRow, TaskRow } from '../../../types/repository/rows.js'
|
||||||
|
|
||||||
export const KUAISHOU_INDUSTRY_DEFAULT_VALID_DAYS = 3
|
export const KUAISHOU_INDUSTRY_DEFAULT_VALID_DAYS = 3
|
||||||
@@ -17,6 +21,12 @@ export const KUAISHOU_INDUSTRY_SEND_CALLBACK_STATUS = {
|
|||||||
FAILED: 'failed',
|
FAILED: 'failed',
|
||||||
} as const
|
} as const
|
||||||
export const KUAISHOU_INDUSTRY_DEFAULT_CONSUME_TYPE = 'consume'
|
export const KUAISHOU_INDUSTRY_DEFAULT_CONSUME_TYPE = 'consume'
|
||||||
|
export const KUAISHOU_INDUSTRY_DESTROY_REASONS = {
|
||||||
|
ETICKET_EXPIRED: 'ETICKET_EXPIRED',
|
||||||
|
USER_APPLY_REFUND: 'USER_APPLY_REFUND',
|
||||||
|
SUPPLY_DESTROY: 'SUPPLY_DESTROY',
|
||||||
|
SYS_ADMIN_DESTROY: 'SYS_ADMIN_DESTROY',
|
||||||
|
} as const
|
||||||
|
|
||||||
export function resolveKuaishouIndustryVoucherValidity(
|
export function resolveKuaishouIndustryVoucherValidity(
|
||||||
input: {
|
input: {
|
||||||
@@ -50,6 +60,33 @@ export function resolveKuaishouIndustryVoucherValidity(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析电子凭证类型。优先级:
|
||||||
|
* 显式入参 > voucher.eticket_type > raw_payload.body.eticketType > raw_payload.eticketType
|
||||||
|
*/
|
||||||
|
export function resolveKuaishouIndustryEticketType(
|
||||||
|
voucher?: Pick<KuaishouIndustryVoucherRow, 'eticket_type' | 'raw_payload_json'> | null,
|
||||||
|
explicit?: unknown,
|
||||||
|
): string {
|
||||||
|
const fromInput = String(explicit || '').trim()
|
||||||
|
if (fromInput) {
|
||||||
|
return fromInput
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!voucher) {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
const fromColumn = String(voucher.eticket_type || '').trim()
|
||||||
|
if (fromColumn) {
|
||||||
|
return fromColumn
|
||||||
|
}
|
||||||
|
|
||||||
|
const rawPayload = parseJsonObject(voucher.raw_payload_json)
|
||||||
|
const body = parseJsonObject(rawPayload.body)
|
||||||
|
return String(body.eticketType || rawPayload.eticketType || '').trim()
|
||||||
|
}
|
||||||
|
|
||||||
export function buildKuaishouIndustryEticketFromVoucher(
|
export function buildKuaishouIndustryEticketFromVoucher(
|
||||||
voucher: KuaishouIndustryVoucherRow,
|
voucher: KuaishouIndustryVoucherRow,
|
||||||
eticketType = '',
|
eticketType = '',
|
||||||
@@ -61,7 +98,7 @@ export function buildKuaishouIndustryEticketFromVoucher(
|
|||||||
num: 1,
|
num: 1,
|
||||||
validStartTime: Number(voucher.valid_start_time || 0) || 0,
|
validStartTime: Number(voucher.valid_start_time || 0) || 0,
|
||||||
validEndTime: Number(voucher.valid_end_time || 0) || 0,
|
validEndTime: Number(voucher.valid_end_time || 0) || 0,
|
||||||
eticketType,
|
eticketType: resolveKuaishouIndustryEticketType(voucher, eticketType),
|
||||||
consumeDetails: resolveVoucherConsumeDetails(voucher),
|
consumeDetails: resolveVoucherConsumeDetails(voucher),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -274,6 +311,128 @@ export async function consumeKuaishouIndustryVoucher(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 主动销毁电子凭证(管理端/运维补救)。
|
||||||
|
* 先调快手销毁回调,成功后再落本地 DESTROYED 并关闭关联任务。
|
||||||
|
* 已 DESTROYED 时仅重试回调(幂等)。
|
||||||
|
*/
|
||||||
|
export async function destroyKuaishouIndustryVoucher(
|
||||||
|
voucher: KuaishouIndustryVoucherRow,
|
||||||
|
input: {
|
||||||
|
source?: string
|
||||||
|
reason?: string
|
||||||
|
token?: string
|
||||||
|
eticketType?: string
|
||||||
|
task?: TaskRow | null
|
||||||
|
skipCallback?: boolean
|
||||||
|
goodsValue?: number
|
||||||
|
} = {},
|
||||||
|
) {
|
||||||
|
const currentStatus = normalizeVoucherStatus(voucher.status)
|
||||||
|
if (currentStatus === 'CONSUMED') {
|
||||||
|
return {
|
||||||
|
ok: false,
|
||||||
|
voucher,
|
||||||
|
callbackSuccess: false,
|
||||||
|
errorMessage: '电子凭证已核销,不能销毁;如需作废请先冲正',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const reason = normalizeKuaishouIndustryDestroyReason(input.reason)
|
||||||
|
const now = normalizeTimestampIso(new Date().toISOString())
|
||||||
|
const token = String(input.token || voucher.token || '').trim()
|
||||||
|
const eticketType = resolveKuaishouIndustryEticketType(voucher, input.eticketType)
|
||||||
|
const alreadyDestroyed = currentStatus === 'DESTROYED'
|
||||||
|
|
||||||
|
const callbackResult = input.skipCallback
|
||||||
|
? { success: true as const }
|
||||||
|
: await destroyCallback({
|
||||||
|
oid: voucher.oid,
|
||||||
|
sellerId: String(voucher.seller_id || '').trim(),
|
||||||
|
etickets: [{
|
||||||
|
id: voucher.voucher_code,
|
||||||
|
code: voucher.voucher_code,
|
||||||
|
num: 1,
|
||||||
|
...(input.goodsValue != null ? { goodsValue: input.goodsValue } : {}),
|
||||||
|
}],
|
||||||
|
reason,
|
||||||
|
token,
|
||||||
|
...(eticketType ? { eticketType } : {}),
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!callbackResult.success) {
|
||||||
|
return {
|
||||||
|
ok: false,
|
||||||
|
voucher,
|
||||||
|
callbackSuccess: false,
|
||||||
|
errorMessage: callbackResult.error || '电子凭证销毁回调失败',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let updated = voucher
|
||||||
|
if (!alreadyDestroyed) {
|
||||||
|
const next = await updateKuaishouIndustryVoucherByCode(voucher.voucher_code, {
|
||||||
|
status: 'DESTROYED',
|
||||||
|
destroyedAt: now,
|
||||||
|
updatedAt: now,
|
||||||
|
...(eticketType && !String(voucher.eticket_type || '').trim()
|
||||||
|
? { eticketType }
|
||||||
|
: {}),
|
||||||
|
})
|
||||||
|
updated = next || voucher
|
||||||
|
}
|
||||||
|
|
||||||
|
let task =
|
||||||
|
input.task ||
|
||||||
|
(voucher.task_id ? await getTaskById(voucher.task_id).catch(() => null) : null)
|
||||||
|
|
||||||
|
if (task && !alreadyDestroyed) {
|
||||||
|
task = await updateTask(task.id, {
|
||||||
|
task_status: TASK_STATUS.CLOSED,
|
||||||
|
delivery_status: 'cancelled',
|
||||||
|
result_code: reason,
|
||||||
|
result_message: `电子凭证销毁: ${reason}`,
|
||||||
|
updated_at: now,
|
||||||
|
}) || task
|
||||||
|
|
||||||
|
await createTaskEvent(
|
||||||
|
task.id,
|
||||||
|
'kuaishou_industry_voucher_destroyed',
|
||||||
|
{
|
||||||
|
oid: voucher.oid,
|
||||||
|
voucherCode: voucher.voucher_code,
|
||||||
|
reason,
|
||||||
|
source: input.source || 'admin_tool_manual_destroy',
|
||||||
|
eticketType,
|
||||||
|
},
|
||||||
|
now,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
ok: true,
|
||||||
|
voucher: updated,
|
||||||
|
callbackSuccess: true,
|
||||||
|
alreadyDestroyed,
|
||||||
|
reason,
|
||||||
|
task: task || null,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function normalizeKuaishouIndustryDestroyReason(value: unknown): string {
|
||||||
|
const normalized = String(value || '').trim().toUpperCase()
|
||||||
|
if (
|
||||||
|
normalized === KUAISHOU_INDUSTRY_DESTROY_REASONS.ETICKET_EXPIRED ||
|
||||||
|
normalized === KUAISHOU_INDUSTRY_DESTROY_REASONS.USER_APPLY_REFUND ||
|
||||||
|
normalized === KUAISHOU_INDUSTRY_DESTROY_REASONS.SUPPLY_DESTROY ||
|
||||||
|
normalized === KUAISHOU_INDUSTRY_DESTROY_REASONS.SYS_ADMIN_DESTROY
|
||||||
|
) {
|
||||||
|
return normalized
|
||||||
|
}
|
||||||
|
|
||||||
|
return KUAISHOU_INDUSTRY_DESTROY_REASONS.SYS_ADMIN_DESTROY
|
||||||
|
}
|
||||||
|
|
||||||
function resolveTaskVouchers(task: TaskRow): Promise<KuaishouIndustryVoucherRow[]> {
|
function resolveTaskVouchers(task: TaskRow): Promise<KuaishouIndustryVoucherRow[]> {
|
||||||
const taskId = Number(task?.id || 0)
|
const taskId = Number(task?.id || 0)
|
||||||
if (taskId > 0) {
|
if (taskId > 0) {
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import type {
|
|||||||
AdminKuaishouIndustrySourceConfigInput,
|
AdminKuaishouIndustrySourceConfigInput,
|
||||||
AdminKuaishouIndustryVoucherCheckAvailableInput,
|
AdminKuaishouIndustryVoucherCheckAvailableInput,
|
||||||
AdminKuaishouIndustryVoucherConsumeInput,
|
AdminKuaishouIndustryVoucherConsumeInput,
|
||||||
|
AdminKuaishouIndustryVoucherDestroyInput,
|
||||||
AdminKuaishouIndustryVoucherResendInput,
|
AdminKuaishouIndustryVoucherResendInput,
|
||||||
AdminKuaishouIndustryVoucherReverseInput,
|
AdminKuaishouIndustryVoucherReverseInput,
|
||||||
AdminCloudtentaclesTestLoginInput,
|
AdminCloudtentaclesTestLoginInput,
|
||||||
@@ -44,6 +45,7 @@ export type AdminKuaishouIndustryVoucherCheckAvailableRouteBody = AdminKuaishouI
|
|||||||
export type AdminKuaishouIndustryVoucherReverseRouteBody = AdminKuaishouIndustryVoucherReverseInput
|
export type AdminKuaishouIndustryVoucherReverseRouteBody = AdminKuaishouIndustryVoucherReverseInput
|
||||||
export type AdminKuaishouIndustryVoucherConsumeRouteBody = AdminKuaishouIndustryVoucherConsumeInput
|
export type AdminKuaishouIndustryVoucherConsumeRouteBody = AdminKuaishouIndustryVoucherConsumeInput
|
||||||
export type AdminKuaishouIndustryVoucherResendRouteBody = AdminKuaishouIndustryVoucherResendInput
|
export type AdminKuaishouIndustryVoucherResendRouteBody = AdminKuaishouIndustryVoucherResendInput
|
||||||
|
export type AdminKuaishouIndustryVoucherDestroyRouteBody = AdminKuaishouIndustryVoucherDestroyInput
|
||||||
export type AdminNotificationConfigRouteBody = AdminNotificationConfigInput
|
export type AdminNotificationConfigRouteBody = AdminNotificationConfigInput
|
||||||
export type AdminNotificationTestRouteBody = AdminNotificationTestInput
|
export type AdminNotificationTestRouteBody = AdminNotificationTestInput
|
||||||
export type AdminScheduledJobsConfigRouteBody = AdminScheduledJobsConfigInput
|
export type AdminScheduledJobsConfigRouteBody = AdminScheduledJobsConfigInput
|
||||||
|
|||||||
@@ -327,3 +327,13 @@ export type AdminKuaishouIndustryVoucherResendInput = {
|
|||||||
taskId?: number | string
|
taskId?: number | string
|
||||||
voucherCode?: string
|
voucherCode?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type AdminKuaishouIndustryVoucherDestroyInput = {
|
||||||
|
oid?: string
|
||||||
|
orderId?: string
|
||||||
|
taskId?: number | string
|
||||||
|
voucherCode?: string
|
||||||
|
token?: string
|
||||||
|
eticketType?: string
|
||||||
|
reason?: string
|
||||||
|
}
|
||||||
|
|||||||
@@ -133,6 +133,7 @@ export type KuaishouIndustryVoucherUpsertInput = {
|
|||||||
unitIndex: number
|
unitIndex: number
|
||||||
sellerId?: string
|
sellerId?: string
|
||||||
token?: string
|
token?: string
|
||||||
|
eticketType?: string
|
||||||
orderId?: number | string | null
|
orderId?: number | string | null
|
||||||
taskId?: number | string | null
|
taskId?: number | string | null
|
||||||
status?: string
|
status?: string
|
||||||
|
|||||||
@@ -113,6 +113,7 @@ export type KuaishouIndustryVoucherRow = {
|
|||||||
unit_index: number
|
unit_index: number
|
||||||
seller_id: string
|
seller_id: string
|
||||||
token: string
|
token: string
|
||||||
|
eticket_type: string
|
||||||
status: string
|
status: string
|
||||||
valid_start_time: number | string
|
valid_start_time: number | string
|
||||||
valid_end_time: number | string
|
valid_end_time: number | string
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import {
|
import {
|
||||||
CheckCircleOutlined,
|
CheckCircleOutlined,
|
||||||
|
DeleteOutlined,
|
||||||
ReloadOutlined,
|
ReloadOutlined,
|
||||||
RollbackOutlined,
|
RollbackOutlined,
|
||||||
SearchOutlined,
|
SearchOutlined,
|
||||||
@@ -33,6 +34,7 @@ import {
|
|||||||
approveAdminKuaishouIndustryRefund,
|
approveAdminKuaishouIndustryRefund,
|
||||||
checkAdminKuaishouIndustryVoucherAvailable,
|
checkAdminKuaishouIndustryVoucherAvailable,
|
||||||
consumeAdminKuaishouIndustryVoucher,
|
consumeAdminKuaishouIndustryVoucher,
|
||||||
|
destroyAdminKuaishouIndustryVoucher,
|
||||||
disagreeAdminKuaishouIndustryRefund,
|
disagreeAdminKuaishouIndustryRefund,
|
||||||
fetchAdminKuaishouIndustryShops,
|
fetchAdminKuaishouIndustryShops,
|
||||||
fetchAdminKuaishouIndustryVouchers,
|
fetchAdminKuaishouIndustryVouchers,
|
||||||
@@ -64,6 +66,7 @@ type IndustryActionScope = 'vouchers' | 'refunds'
|
|||||||
type IndustryActionKind =
|
type IndustryActionKind =
|
||||||
| 'check'
|
| 'check'
|
||||||
| 'consume'
|
| 'consume'
|
||||||
|
| 'destroy'
|
||||||
| 'reverse'
|
| 'reverse'
|
||||||
| 'resend'
|
| 'resend'
|
||||||
| 'refund-list'
|
| 'refund-list'
|
||||||
@@ -107,8 +110,6 @@ type RefundListState = {
|
|||||||
negotiateStatus: string
|
negotiateStatus: string
|
||||||
}
|
}
|
||||||
|
|
||||||
const DEFAULT_ETICKET_TYPE = 'DINING_OPEN_TICKET'
|
|
||||||
|
|
||||||
export default function AdminKuaishouIndustryPage() {
|
export default function AdminKuaishouIndustryPage() {
|
||||||
const isSupportOnly = getAdminRole() === 'support'
|
const isSupportOnly = getAdminRole() === 'support'
|
||||||
const canManageRefund = hasAdminRole('operator')
|
const canManageRefund = hasAdminRole('operator')
|
||||||
@@ -130,8 +131,9 @@ export default function AdminKuaishouIndustryPage() {
|
|||||||
pageSize: 50,
|
pageSize: 50,
|
||||||
})
|
})
|
||||||
const [toolForm, setToolForm] = useState<AdminKuaishouIndustryVoucherToolPayload>({
|
const [toolForm, setToolForm] = useState<AdminKuaishouIndustryVoucherToolPayload>({
|
||||||
eticketType: DEFAULT_ETICKET_TYPE,
|
eticketType: '',
|
||||||
consumeType: 'consume',
|
consumeType: 'consume',
|
||||||
|
reason: 'SYS_ADMIN_DESTROY',
|
||||||
})
|
})
|
||||||
const [refundDateRange, setRefundDateRange] = useState<DateRangeValue>(() => [
|
const [refundDateRange, setRefundDateRange] = useState<DateRangeValue>(() => [
|
||||||
dayjs().subtract(1, 'day'),
|
dayjs().subtract(1, 'day'),
|
||||||
@@ -255,13 +257,20 @@ export default function AdminKuaishouIndustryPage() {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '状态',
|
title: '状态',
|
||||||
width: 118,
|
width: 148,
|
||||||
render: (_, row) => (
|
render: (_, row) => (
|
||||||
<div className="cell-stack">
|
<div className="cell-stack">
|
||||||
<Tag color={getVoucherStatusColor(row.status)}>{getVoucherStatusLabel(row.status)}</Tag>
|
<Tag color={getVoucherStatusColor(row.status)}>{getVoucherStatusLabel(row.status)}</Tag>
|
||||||
<Tag color={row.sendCallbackStatus === 'success' ? 'green' : 'orange'}>
|
<Tag color={row.sendCallbackStatus === 'success' ? 'green' : 'orange'}>
|
||||||
发码:{row.sendCallbackStatus || '-'}
|
发码:{row.sendCallbackStatus || '-'}
|
||||||
</Tag>
|
</Tag>
|
||||||
|
{row.eticketType ? (
|
||||||
|
<Typography.Text type="secondary" ellipsis={{ tooltip: row.eticketType }} className="muted">
|
||||||
|
{row.eticketType}
|
||||||
|
</Typography.Text>
|
||||||
|
) : (
|
||||||
|
<span className="muted">类型未知</span>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
@@ -425,7 +434,9 @@ export default function AdminKuaishouIndustryPage() {
|
|||||||
orderId: row.oid,
|
orderId: row.oid,
|
||||||
taskId: row.taskId || '',
|
taskId: row.taskId || '',
|
||||||
voucherCode: row.voucherCode,
|
voucherCode: row.voucherCode,
|
||||||
|
eticketType: row.eticketType || current.eticketType || '',
|
||||||
serialNum: row.consumeSerialNum || current.serialNum,
|
serialNum: row.consumeSerialNum || current.serialNum,
|
||||||
|
reason: current.reason || 'SYS_ADMIN_DESTROY',
|
||||||
}))
|
}))
|
||||||
showSuccess('已填入券码操作区')
|
showSuccess('已填入券码操作区')
|
||||||
}
|
}
|
||||||
@@ -440,7 +451,7 @@ export default function AdminKuaishouIndustryPage() {
|
|||||||
await runOpenApiAction('reverse', '电子凭证冲正回调', 'reverse', () =>
|
await runOpenApiAction('reverse', '电子凭证冲正回调', 'reverse', () =>
|
||||||
reverseAdminKuaishouIndustryVoucher({
|
reverseAdminKuaishouIndustryVoucher({
|
||||||
...buildVoucherToolPayload(),
|
...buildVoucherToolPayload(),
|
||||||
reason: toolForm.reason || '后台手动冲正',
|
reason: '后台手动冲正',
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
await loadVouchers()
|
await loadVouchers()
|
||||||
@@ -484,7 +495,7 @@ export default function AdminKuaishouIndustryPage() {
|
|||||||
orderId: row.oid,
|
orderId: row.oid,
|
||||||
taskId: row.taskId || '',
|
taskId: row.taskId || '',
|
||||||
voucherCode: row.voucherCode,
|
voucherCode: row.voucherCode,
|
||||||
eticketType: DEFAULT_ETICKET_TYPE,
|
eticketType: row.eticketType || undefined,
|
||||||
consumeType: 'consume',
|
consumeType: 'consume',
|
||||||
serialNum: row.consumeSerialNum || undefined,
|
serialNum: row.consumeSerialNum || undefined,
|
||||||
etickets: [{ id: row.voucherCode, code: row.voucherCode, num: 1 }],
|
etickets: [{ id: row.voucherCode, code: row.voucherCode, num: 1 }],
|
||||||
@@ -540,6 +551,36 @@ export default function AdminKuaishouIndustryPage() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function runDestroy() {
|
||||||
|
setActionLoading('destroy')
|
||||||
|
try {
|
||||||
|
const response = await destroyAdminKuaishouIndustryVoucher({
|
||||||
|
...buildVoucherToolPayload(),
|
||||||
|
reason: toolForm.reason || 'SYS_ADMIN_DESTROY',
|
||||||
|
})
|
||||||
|
publishActionResult(
|
||||||
|
buildIndustryActionResultView('destroy', '手动销毁', response.data),
|
||||||
|
)
|
||||||
|
showSuccess(
|
||||||
|
response.data.alreadyDestroyed
|
||||||
|
? `券码已是销毁状态,销毁回调已重试(${response.data.reason || 'SYS_ADMIN_DESTROY'})`
|
||||||
|
: `券码已销毁(${response.data.reason || 'SYS_ADMIN_DESTROY'})`,
|
||||||
|
)
|
||||||
|
await loadVouchers()
|
||||||
|
} catch (error) {
|
||||||
|
const message = error instanceof Error ? error.message : '手动销毁失败'
|
||||||
|
publishActionResult(
|
||||||
|
buildIndustryActionResultView('destroy', '手动销毁', {
|
||||||
|
success: false,
|
||||||
|
error: message,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
showError(message)
|
||||||
|
} finally {
|
||||||
|
setActionLoading('')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function runRefundList() {
|
async function runRefundList() {
|
||||||
const [begin, end] = refundDateRange || []
|
const [begin, end] = refundDateRange || []
|
||||||
const sellerId = resolveRefundSellerId(refundListForm.sellerId)
|
const sellerId = resolveRefundSellerId(refundListForm.sellerId)
|
||||||
@@ -888,7 +929,7 @@ export default function AdminKuaishouIndustryPage() {
|
|||||||
onChange={(event) => updateToolForm({ voucherCode: event.target.value })}
|
onChange={(event) => updateToolForm({ voucherCode: event.target.value })}
|
||||||
/>
|
/>
|
||||||
<Input
|
<Input
|
||||||
placeholder="电子凭证类型"
|
placeholder="电子凭证类型(选中券码自动填入)"
|
||||||
value={toolForm.eticketType}
|
value={toolForm.eticketType}
|
||||||
onChange={(event) => updateToolForm({ eticketType: event.target.value })}
|
onChange={(event) => updateToolForm({ eticketType: event.target.value })}
|
||||||
/>
|
/>
|
||||||
@@ -902,10 +943,18 @@ export default function AdminKuaishouIndustryPage() {
|
|||||||
value={toolForm.consumeType}
|
value={toolForm.consumeType}
|
||||||
onChange={(event) => updateToolForm({ consumeType: event.target.value })}
|
onChange={(event) => updateToolForm({ consumeType: event.target.value })}
|
||||||
/>
|
/>
|
||||||
<Input
|
<Select
|
||||||
placeholder="冲正原因"
|
allowClear
|
||||||
value={toolForm.reason}
|
placeholder="销毁/冲正原因"
|
||||||
onChange={(event) => updateToolForm({ reason: event.target.value })}
|
value={toolForm.reason || undefined}
|
||||||
|
onChange={(value) => updateToolForm({ reason: value || '' })}
|
||||||
|
options={[
|
||||||
|
{ label: 'SYS_ADMIN_DESTROY(后台作废)', value: 'SYS_ADMIN_DESTROY' },
|
||||||
|
{ label: 'SUPPLY_DESTROY(供应商作废)', value: 'SUPPLY_DESTROY' },
|
||||||
|
{ label: 'USER_APPLY_REFUND(用户退款)', value: 'USER_APPLY_REFUND' },
|
||||||
|
{ label: 'ETICKET_EXPIRED(凭证过期)', value: 'ETICKET_EXPIRED' },
|
||||||
|
]}
|
||||||
|
style={{ minWidth: 220 }}
|
||||||
/>
|
/>
|
||||||
<Input
|
<Input
|
||||||
placeholder="门店名称"
|
placeholder="门店名称"
|
||||||
@@ -942,6 +991,23 @@ export default function AdminKuaishouIndustryPage() {
|
|||||||
>
|
>
|
||||||
冲正
|
冲正
|
||||||
</Button>
|
</Button>
|
||||||
|
<Popconfirm
|
||||||
|
title="确认销毁该券码?"
|
||||||
|
description="将向快手发起销毁回调,并关闭关联任务。已销毁券会重试回调。"
|
||||||
|
okText="销毁"
|
||||||
|
cancelText="取消"
|
||||||
|
onConfirm={() => runDestroy()}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
danger
|
||||||
|
type="primary"
|
||||||
|
icon={<DeleteOutlined />}
|
||||||
|
loading={actionLoading === 'destroy'}
|
||||||
|
disabled={!canManageRefund}
|
||||||
|
>
|
||||||
|
手动销毁
|
||||||
|
</Button>
|
||||||
|
</Popconfirm>
|
||||||
<Button
|
<Button
|
||||||
icon={<SendOutlined />}
|
icon={<SendOutlined />}
|
||||||
loading={actionLoading === 'resend'}
|
loading={actionLoading === 'resend'}
|
||||||
@@ -1416,19 +1482,44 @@ function buildIndustryActionResultView(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (kind === 'check') {
|
if (kind === 'check') {
|
||||||
const etickets = extractAvailableEtickets(responseData)
|
const etickets = extractAvailableEtickets(response)
|
||||||
pushHighlight(highlights, '可核销券数', String(etickets.length || 0))
|
pushHighlight(highlights, '返回券数', String(etickets.length || 0))
|
||||||
|
const resolvedType = pickFirstString([
|
||||||
|
record.eticketType,
|
||||||
|
voucher?.eticketType,
|
||||||
|
asRecord(openApi.request)?.eticketType,
|
||||||
|
asRecord(asRecord(openApi.request)?.param)?.eticketType,
|
||||||
|
])
|
||||||
|
if (resolvedType) {
|
||||||
|
pushHighlight(highlights, '请求类型', resolvedType)
|
||||||
|
}
|
||||||
if (etickets[0]) {
|
if (etickets[0]) {
|
||||||
pushHighlight(
|
pushHighlight(
|
||||||
highlights,
|
highlights,
|
||||||
'首张券码',
|
'首张券码',
|
||||||
pickFirstString([etickets[0].code, etickets[0].id]) || '-',
|
pickFirstString([etickets[0].code, etickets[0].id]) || '-',
|
||||||
)
|
)
|
||||||
pushHighlight(highlights, '首张数量', String(etickets[0].num || 1))
|
pushHighlight(
|
||||||
|
highlights,
|
||||||
|
'展示状态',
|
||||||
|
pickFirstString([etickets[0].displayStatus, etickets[0].status]) || '-',
|
||||||
|
)
|
||||||
|
pushHighlight(
|
||||||
|
highlights,
|
||||||
|
'平台类型',
|
||||||
|
pickFirstString([etickets[0].eticketType]) || '-',
|
||||||
|
)
|
||||||
|
pushHighlight(highlights, '剩余数量', String(etickets[0].leftNum ?? etickets[0].num ?? 1))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (kind === 'consume' || kind === 'reverse' || kind === 'resend' || voucher) {
|
if (
|
||||||
|
kind === 'consume' ||
|
||||||
|
kind === 'destroy' ||
|
||||||
|
kind === 'reverse' ||
|
||||||
|
kind === 'resend' ||
|
||||||
|
voucher
|
||||||
|
) {
|
||||||
if (voucher) {
|
if (voucher) {
|
||||||
pushHighlight(highlights, '券码', String(voucher.voucherCode || '-'))
|
pushHighlight(highlights, '券码', String(voucher.voucherCode || '-'))
|
||||||
pushHighlight(
|
pushHighlight(
|
||||||
@@ -1436,6 +1527,15 @@ function buildIndustryActionResultView(
|
|||||||
'券状态',
|
'券状态',
|
||||||
getVoucherStatusLabel(String(voucher.status || '')),
|
getVoucherStatusLabel(String(voucher.status || '')),
|
||||||
)
|
)
|
||||||
|
if (voucher.eticketType) {
|
||||||
|
pushHighlight(highlights, '凭证类型', String(voucher.eticketType))
|
||||||
|
}
|
||||||
|
if (kind === 'destroy' && record.reason) {
|
||||||
|
pushHighlight(highlights, '销毁原因', String(record.reason))
|
||||||
|
}
|
||||||
|
if (kind === 'destroy' && typeof record.alreadyDestroyed === 'boolean') {
|
||||||
|
pushHighlight(highlights, '已是销毁态', record.alreadyDestroyed ? '是(重试回调)' : '否')
|
||||||
|
}
|
||||||
if (voucher.consumeSerialNum) {
|
if (voucher.consumeSerialNum) {
|
||||||
pushHighlight(highlights, '核销序列号', String(voucher.consumeSerialNum))
|
pushHighlight(highlights, '核销序列号', String(voucher.consumeSerialNum))
|
||||||
}
|
}
|
||||||
@@ -1520,7 +1620,7 @@ function buildActionSummary(
|
|||||||
|
|
||||||
if (kind === 'check') {
|
if (kind === 'check') {
|
||||||
return success
|
return success
|
||||||
? context.platformMessage || '平台返回券码有效,可继续核销。'
|
? context.platformMessage || '检查成功,请结合展示状态(如 frozen)判断是否可核销。'
|
||||||
: context.platformMessage || '检查失败,请核对卖家、券码与电子凭证类型。'
|
: context.platformMessage || '检查失败,请核对卖家、券码与电子凭证类型。'
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1530,6 +1630,12 @@ function buildActionSummary(
|
|||||||
: context.platformMessage || '核销失败。'
|
: context.platformMessage || '核销失败。'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (kind === 'destroy') {
|
||||||
|
return success
|
||||||
|
? `销毁成功${context.voucherStatus ? `,券状态:${context.voucherStatus}` : ''}。`
|
||||||
|
: context.platformMessage || '销毁失败。'
|
||||||
|
}
|
||||||
|
|
||||||
if (kind === 'reverse') {
|
if (kind === 'reverse') {
|
||||||
return success
|
return success
|
||||||
? `冲正成功${context.voucherStatus ? `,券状态已回写为 ${context.voucherStatus}` : ''}。`
|
? `冲正成功${context.voucherStatus ? `,券状态已回写为 ${context.voucherStatus}` : ''}。`
|
||||||
@@ -1575,7 +1681,7 @@ function resolveActionSuccess(
|
|||||||
if (typeof record.success === 'boolean') {
|
if (typeof record.success === 'boolean') {
|
||||||
return record.success
|
return record.success
|
||||||
}
|
}
|
||||||
if (kind === 'consume') {
|
if (kind === 'consume' || kind === 'destroy') {
|
||||||
return Boolean(record.voucher)
|
return Boolean(record.voucher)
|
||||||
}
|
}
|
||||||
return openApi.success
|
return openApi.success
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import type {
|
|||||||
AdminKuaishouIndustryRefundListPayload,
|
AdminKuaishouIndustryRefundListPayload,
|
||||||
AdminKuaishouIndustryShopListResult,
|
AdminKuaishouIndustryShopListResult,
|
||||||
AdminKuaishouIndustryVoucherConsumeResult,
|
AdminKuaishouIndustryVoucherConsumeResult,
|
||||||
|
AdminKuaishouIndustryVoucherDestroyResult,
|
||||||
AdminKuaishouIndustryVoucherListResult,
|
AdminKuaishouIndustryVoucherListResult,
|
||||||
AdminKuaishouIndustryVoucherResendResult,
|
AdminKuaishouIndustryVoucherResendResult,
|
||||||
AdminKuaishouIndustryVoucherToolPayload,
|
AdminKuaishouIndustryVoucherToolPayload,
|
||||||
@@ -84,3 +85,12 @@ export function resendAdminKuaishouIndustryVoucherCode(
|
|||||||
payload,
|
payload,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function destroyAdminKuaishouIndustryVoucher(
|
||||||
|
payload: AdminKuaishouIndustryVoucherToolPayload,
|
||||||
|
) {
|
||||||
|
return apiPost<AdminKuaishouIndustryVoucherDestroyResult>(
|
||||||
|
'/api/v1/admin/kuaishou-industry/vouchers/destroy',
|
||||||
|
payload,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ export type {
|
|||||||
AdminKuaishouIndustryShopOption,
|
AdminKuaishouIndustryShopOption,
|
||||||
AdminKuaishouIndustryShopListResult,
|
AdminKuaishouIndustryShopListResult,
|
||||||
AdminKuaishouIndustryVoucherConsumeResult,
|
AdminKuaishouIndustryVoucherConsumeResult,
|
||||||
|
AdminKuaishouIndustryVoucherDestroyResult,
|
||||||
AdminKuaishouIndustryVoucherResendResult,
|
AdminKuaishouIndustryVoucherResendResult,
|
||||||
AdminKuaishouIndustryRefundListPayload,
|
AdminKuaishouIndustryRefundListPayload,
|
||||||
AdminKuaishouIndustryRefundApprovePayload,
|
AdminKuaishouIndustryRefundApprovePayload,
|
||||||
|
|||||||
@@ -28,6 +28,8 @@ export interface AdminKuaishouIndustryVoucher {
|
|||||||
/** 关联订单商品名称(列表 join) */
|
/** 关联订单商品名称(列表 join) */
|
||||||
skuName?: string
|
skuName?: string
|
||||||
tokenMasked: string
|
tokenMasked: string
|
||||||
|
/** 快手电子凭证类型,如 GAME_OPEN_TICKET_CONSUME */
|
||||||
|
eticketType: string
|
||||||
status: string
|
status: string
|
||||||
validStartTime: number
|
validStartTime: number
|
||||||
validEndTime: number
|
validEndTime: number
|
||||||
@@ -72,6 +74,19 @@ export interface AdminKuaishouIndustryVoucherConsumeResult {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface AdminKuaishouIndustryVoucherDestroyResult {
|
||||||
|
success: boolean
|
||||||
|
alreadyDestroyed: boolean
|
||||||
|
reason: string
|
||||||
|
voucher: AdminKuaishouIndustryVoucher
|
||||||
|
task: null | {
|
||||||
|
taskId: number
|
||||||
|
taskNo: string
|
||||||
|
status: string
|
||||||
|
deliveryStatus: string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export interface AdminKuaishouIndustryVoucherResendResult {
|
export interface AdminKuaishouIndustryVoucherResendResult {
|
||||||
success: boolean
|
success: boolean
|
||||||
response: Record<string, unknown> | null
|
response: Record<string, unknown> | null
|
||||||
|
|||||||
Reference in New Issue
Block a user