From d7f9918d0695e36e7a762b138e979a4ea764ae0d Mon Sep 17 00:00:00 2001 From: yml2213 Date: Thu, 23 Jul 2026 11:32:57 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E5=8D=A1=E5=88=B8=E9=94=80?= =?UTF-8?q?=E6=AF=81=E5=86=BB=E7=BB=93=E7=8A=B6=E6=80=81=E6=A0=A1=E9=AA=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../admin/kuaishou-industry-admin-service.ts | 53 +++++++++++++- .../check-available-service.test.ts | 44 ++++++++++++ .../check-available-service.ts | 69 +++++++++++++++++++ 3 files changed, 163 insertions(+), 3 deletions(-) create mode 100644 apps/backend/src/services/platforms/kuaishou-industry/check-available-service.test.ts diff --git a/apps/backend/src/services/admin/kuaishou-industry-admin-service.ts b/apps/backend/src/services/admin/kuaishou-industry-admin-service.ts index d24a096e..ff1ba098 100644 --- a/apps/backend/src/services/admin/kuaishou-industry-admin-service.ts +++ b/apps/backend/src/services/admin/kuaishou-industry-admin-service.ts @@ -1,4 +1,4 @@ -import { asJsonObject, type JsonObject } from '../../types/json.js' +import { asJsonObject, type JsonObject } from '../../types/json.js' import { findKuaishouIndustryVoucherByCode, listKuaishouIndustryVouchersByOid, @@ -12,7 +12,12 @@ import { getOrderById } from '../../repositories/order-repo.js' import { getTaskById } from '../../repositories/task-repo.js' import { createHttpError } from '../../utils/http.js' import { maskSecret } from '../../utils/masking.js' -import { checkKuaishouIndustryEticketAvailable } from '../platforms/kuaishou-industry/check-available-service.js' +import { + checkKuaishouIndustryEticketAvailable, + isKuaishouIndustryEticketDestroyableStatus, + KUAISHOU_INDUSTRY_DESTROYABLE_DISPLAY_STATUS, + resolveKuaishouIndustryEticketDisplayStatus, +} from '../platforms/kuaishou-industry/check-available-service.js' import { getKuaishouIndustrySourceConfig, listKuaishouIndustryShopConfigs, @@ -108,7 +113,10 @@ export async function checkAdminKuaishouIndustryVoucherAvailable(input: JsonObje const payload = buildVoucherOpenApiPayload(input, voucher) assertRequired(payload.sellerId, '卖家编号未填写') - assertRequired(payload.eticketType || payload.bizTypeCode, '电子凭证类型未填写(请选择券码或手动填写类型)') + assertRequired( + payload.eticketType || payload.bizTypeCode, + '电子凭证类型未填写(请选择券码或手动填写类型)', + ) assertEtickets(payload.etickets, '电子凭证列表未填写') const result = mapOpenApiResult(await checkKuaishouIndustryEticketAvailable(payload)) @@ -200,6 +208,45 @@ export async function resendAdminKuaishouIndustryVoucherCode(input: JsonObject = export async function destroyAdminKuaishouIndustryVoucherByCode(input: JsonObject = {}) { const voucher = await resolveRequiredVoucher(input) + const checkPayload = buildVoucherOpenApiPayload(input, voucher) + + assertRequired(checkPayload.sellerId, '卖家编号未填写,无法在销毁前查询卡券状态') + assertRequired( + checkPayload.eticketType || checkPayload.bizTypeCode, + '电子凭证类型未填写,无法在销毁前查询卡券状态', + ) + assertEtickets(checkPayload.etickets, '电子凭证列表未填写,无法在销毁前查询卡券状态') + + const checkResult = await checkKuaishouIndustryEticketAvailable(checkPayload) + if (!checkResult.success || checkResult.skippedReason) { + const platformMessage = String( + checkResult.error || checkResult.response?.error_msg || checkResult.response?.msg || '', + ).trim() + throw createHttpError( + `销毁前查询卡券状态失败,已取消销毁${platformMessage ? `:${platformMessage}` : ''}`, + { + statusCode: 409, + errorCode: 'admin_kuaishou_industry_destroy_status_check_failed', + }, + ) + } + + const displayStatus = resolveKuaishouIndustryEticketDisplayStatus( + checkResult.response, + voucher.voucher_code, + ) + if (!isKuaishouIndustryEticketDestroyableStatus(displayStatus)) { + throw createHttpError( + displayStatus + ? `卡券当前展示状态为 ${displayStatus},仅 ${KUAISHOU_INDUSTRY_DESTROYABLE_DISPLAY_STATUS}(冻结)状态可以销毁` + : `未获取到卡券展示状态,仅 ${KUAISHOU_INDUSTRY_DESTROYABLE_DISPLAY_STATUS}(冻结)状态可以销毁`, + { + statusCode: 409, + errorCode: 'admin_kuaishou_industry_voucher_not_frozen', + }, + ) + } + const task = voucher.task_id ? await getTaskById(voucher.task_id) : null const destroyInput: Parameters[1] = { source: 'admin_tool_manual_destroy', diff --git a/apps/backend/src/services/platforms/kuaishou-industry/check-available-service.test.ts b/apps/backend/src/services/platforms/kuaishou-industry/check-available-service.test.ts new file mode 100644 index 00000000..17cf3487 --- /dev/null +++ b/apps/backend/src/services/platforms/kuaishou-industry/check-available-service.test.ts @@ -0,0 +1,44 @@ +import test from 'node:test' +import assert from 'node:assert/strict' + +import { + isKuaishouIndustryEticketDestroyableStatus, + resolveKuaishouIndustryEticketDisplayStatus, +} from './check-available-service.js' + +test('resolveKuaishouIndustryEticketDisplayStatus 按券码读取展示状态', () => { + const response = { + etickets: [ + { code: 'OTHER-CODE', displayStatus: 'available' }, + { code: 'TARGET-CODE', displayStatus: ' Frozen ' }, + ], + } + + assert.equal(resolveKuaishouIndustryEticketDisplayStatus(response, 'target-code'), 'frozen') + assert.equal(isKuaishouIndustryEticketDestroyableStatus('frozen'), true) +}) + +test('resolveKuaishouIndustryEticketDisplayStatus 兼容 data 内的 status 字段', () => { + const response = { + data: { + eTicketList: [{ id: 'TARGET-CODE', status: 'FROZEN' }], + }, + } + + assert.equal(resolveKuaishouIndustryEticketDisplayStatus(response, 'TARGET-CODE'), 'frozen') +}) + +test('isKuaishouIndustryEticketDestroyableStatus 拒绝非冻结和缺失状态', () => { + assert.equal(isKuaishouIndustryEticketDestroyableStatus('available'), false) + assert.equal(isKuaishouIndustryEticketDestroyableStatus(''), false) +}) + +test('resolveKuaishouIndustryEticketDisplayStatus 拒绝其他券码的冻结状态', () => { + assert.equal( + resolveKuaishouIndustryEticketDisplayStatus( + { etickets: [{ code: 'OTHER-CODE', displayStatus: 'frozen' }] }, + 'TARGET-CODE', + ), + '', + ) +}) diff --git a/apps/backend/src/services/platforms/kuaishou-industry/check-available-service.ts b/apps/backend/src/services/platforms/kuaishou-industry/check-available-service.ts index bfc0ccbb..5dcd60e6 100644 --- a/apps/backend/src/services/platforms/kuaishou-industry/check-available-service.ts +++ b/apps/backend/src/services/platforms/kuaishou-industry/check-available-service.ts @@ -6,6 +6,8 @@ import { type JsonObject, } from './openapi-values.js' +export const KUAISHOU_INDUSTRY_DESTROYABLE_DISPLAY_STATUS = 'frozen' + export type KuaishouIndustryAvailableEticketInput = { id?: unknown code?: unknown @@ -44,6 +46,41 @@ export function checkKuaishouIndustryEticketAvailable( }) } +export function resolveKuaishouIndustryEticketDisplayStatus( + response: unknown, + voucherCode: unknown, +): string { + const etickets = extractAvailableEtickets(response) + const normalizedVoucherCode = String(voucherCode || '') + .trim() + .toLowerCase() + const matched = normalizedVoucherCode + ? etickets.find((eticket) => + [eticket.code, eticket.id].some( + (value) => + String(value || '') + .trim() + .toLowerCase() === normalizedVoucherCode, + ), + ) + : undefined + const soleEticket = etickets.length === 1 ? etickets[0] : undefined + const soleEticketHasIdentity = Boolean(String(soleEticket?.code || soleEticket?.id || '').trim()) + const target = matched || (!soleEticketHasIdentity ? soleEticket : undefined) + + return String(target?.displayStatus || target?.status || '') + .trim() + .toLowerCase() +} + +export function isKuaishouIndustryEticketDestroyableStatus(displayStatus: unknown): boolean { + return ( + String(displayStatus || '') + .trim() + .toLowerCase() === KUAISHOU_INDUSTRY_DESTROYABLE_DISPLAY_STATUS + ) +} + function normalizeAvailableEtickets(value: unknown): JsonObject[] { if (!Array.isArray(value)) { return [] @@ -70,3 +107,35 @@ function normalizeAvailableEticket(value: unknown): JsonObject | null { return pickDefinedBizParams({ id, code, num }) } + +function extractAvailableEtickets(value: unknown): JsonObject[] { + if (!value || typeof value !== 'object' || Array.isArray(value)) { + return [] + } + + const current = value as JsonObject + const data = + current.data && typeof current.data === 'object' && !Array.isArray(current.data) + ? (current.data as JsonObject) + : null + const candidates = [ + current.etickets, + current.eTicketList, + current.eticketList, + current.list, + data?.etickets, + data?.eTicketList, + data?.eticketList, + data?.list, + ] + + for (const candidate of candidates) { + if (Array.isArray(candidate)) { + return candidate.filter((item): item is JsonObject => + Boolean(item && typeof item === 'object' && !Array.isArray(item)), + ) + } + } + + return [] +}