diff --git a/apps/backend/src/services/claim/session/redeem.js b/apps/backend/src/services/claim/session/redeem.ts similarity index 66% rename from apps/backend/src/services/claim/session/redeem.js rename to apps/backend/src/services/claim/session/redeem.ts index 96130b72..cb50ecc2 100644 --- a/apps/backend/src/services/claim/session/redeem.js +++ b/apps/backend/src/services/claim/session/redeem.ts @@ -1,5 +1,3 @@ -// @ts-check - import { reloadTencentBrowserSession, redeemTencentBrowserSession } from '../../session/session.js' import { classifyTencentRedeemResult } from '../../session/session-redeem.js' import { createTaskEvent } from '../../../repositories/task-event-repo.js' @@ -11,14 +9,127 @@ import { reserveInventoryForTask } from '../../order/inventory-service.js' import { createHttpError } from '../../../utils/http.js' import { nowIso } from '../../../utils/time.js' import { maskCode, REDEEM_REPLACEMENT_LIMIT } from './shared.js' +import type { InventoryItemRow, OrderItemRow, TaskRow } from '../../../types/repository-rows.js' -export async function redeemClaimTaskWithInventoryFallback(context, initialInventoryItem) { +type ClaimRedeemContext = { + task: Pick + orderItem: Pick +} + +type TencentRedeemPayload = { + [key: string]: unknown + iRet?: unknown + ret?: unknown +} + +type TencentRedeemSession = { + [key: string]: unknown + sessionId?: string + artifacts?: { + [key: string]: unknown + hasScreenshot?: boolean + } | null + redeem?: { + final?: { + redeem?: TencentRedeemPayload | null + } | null + } | null +} + +type TencentRedeemClassification = { + success: boolean + outcome: string + message: string + retCode?: unknown + [key: string]: unknown +} + +type RedeemAttemptSummary = { + attempt: number + inventoryItemId: number + codeMasked: string + credentialType: string + outcome: string + resultCode: string + resultMessage: string +} + +type RedeemTaskState = { + taskStatus: string + inventoryStatus: string + deliveryStatus: string + lastError: string + attempts: RedeemAttemptSummary[] + classification: TencentRedeemClassification | null +} + +type RedeemTaskStateError = Error & { + statusCode?: number + errorCode?: string + redeemTaskState: RedeemTaskState +} + +type RedeemTaskStateErrorPayload = { + statusCode?: number + errorCode?: string + taskStatus?: string + inventoryStatus?: string + deliveryStatus?: string + attempts?: RedeemAttemptSummary[] + classification?: TencentRedeemClassification | null +} + +type RedeemClaimTaskDeps = { + reloadTencentBrowserSession?: (sessionId: string) => Promise + redeemTencentBrowserSession?: ( + sessionId: string, + payload: { code: string }, + ) => Promise + classifyTencentRedeemResult?: (finalRedeem: TencentRedeemPayload | null) => TencentRedeemClassification + markInventoryItemConsumed?: ( + inventoryItemId: number | string, + reason: string, + consumedAt: string, + ) => Promise + createTaskEvent?: ( + taskId: number | string, + eventType: string, + payload?: unknown, + createdAt?: string, + ) => Promise + invalidateReservedInventoryItem?: ( + inventoryItemId: number | string, + reason: string, + updatedAt: string, + ) => Promise + reserveInventoryForTask?: (payload: { + skuCode: string + taskId: number | string + credentialType?: string + roleKey?: string + inventoryGroupCodes?: string[] | null + }) => Promise + nowIso?: () => string + redeemReplacementLimit?: number +} + +type RedeemClaimTaskResult = { + session: TencentRedeemSession + inventoryItem: InventoryItemRow + classification: TencentRedeemClassification + attempts: RedeemAttemptSummary[] +} + +export async function redeemClaimTaskWithInventoryFallback( + context: ClaimRedeemContext, + initialInventoryItem: InventoryItemRow, +): Promise { return redeemClaimTaskWithInventoryFallbackWithDeps(context, initialInventoryItem) } export async function redeemClaimTaskWithInventoryFallbackWithDeps( - context, - initialInventoryItem, + context: ClaimRedeemContext, + initialInventoryItem: InventoryItemRow, { reloadTencentBrowserSession: reloadRedeemSession = reloadTencentBrowserSession, redeemTencentBrowserSession: redeemSession = redeemTencentBrowserSession, @@ -29,9 +140,9 @@ export async function redeemClaimTaskWithInventoryFallbackWithDeps( reserveInventoryForTask: reserveReplacementInventory = reserveInventoryForTask, nowIso: getNowIso = nowIso, redeemReplacementLimit = REDEEM_REPLACEMENT_LIMIT, - } = {}, -) { - const attempts = [] + }: RedeemClaimTaskDeps = {}, +): Promise { + const attempts: RedeemAttemptSummary[] = [] let currentInventoryItem = initialInventoryItem let shouldReloadBeforeNextAttempt = false @@ -156,15 +267,14 @@ export async function redeemClaimTaskWithInventoryFallbackWithDeps( }) } -/** - * @param {string} message - * @param {{ statusCode?: number, errorCode?: string, taskStatus?: string, inventoryStatus?: string, deliveryStatus?: string, attempts?: unknown[], classification?: { retCode?: unknown } | null }} [payload] - */ -function createRedeemTaskStateError(message, payload = {}) { - const error = /** @type {Error & import('../../../utils/http.js').HttpErrorLike & { redeemTaskState: { taskStatus: string, inventoryStatus: string, deliveryStatus: string, lastError: string, attempts: unknown[], classification: { retCode?: unknown } | null } }} */ (createHttpError(message, { +function createRedeemTaskStateError( + message: string, + payload: RedeemTaskStateErrorPayload = {}, +): RedeemTaskStateError { + const error = createHttpError(message, { statusCode: payload.statusCode || 409, errorCode: payload.errorCode || 'claim_redeem_failed', - })) + }) as unknown as RedeemTaskStateError error.redeemTaskState = { taskStatus: payload.taskStatus || 'retry_pending', @@ -178,7 +288,10 @@ function createRedeemTaskStateError(message, payload = {}) { return error } -export function resolveTencentRedeemResultCode(finalRedeem, classification) { +export function resolveTencentRedeemResultCode( + finalRedeem: TencentRedeemPayload | null, + classification: Pick | null, +): string { if (classification?.retCode != null) { return String(classification.retCode) } diff --git a/docs/backend-typescript-migration-plan.md b/docs/backend-typescript-migration-plan.md index 153c8f96..a654fb59 100644 --- a/docs/backend-typescript-migration-plan.md +++ b/docs/backend-typescript-migration-plan.md @@ -264,13 +264,21 @@ - `npm run typecheck` - `npm run build` - `npm test` 共 125 个用例通过 +28. claim 换码服务迁移到 `.ts`: + - `src/services/claim/session/redeem.ts` +29. 换码流程的上下文、腾讯兑换返回、分类结果、尝试记录、错误状态、依赖注入函数已显式类型化 +30. Docker 内验证通过: + - claim 换码相关 2 个测试文件共 8 个用例通过 + - `npm run typecheck` + - `npm run build` + - `npm test` 共 125 个用例通过 ## 下一步建议 第一批继续推进时,建议按这个顺序: 1. 继续迁移服务层中最核心、最常改的订单 / 履约 / claim 模块 -2. 逐步移除服务层 `@ts-nocheck`,优先处理 claim session、webhook service、自动发货 +2. 逐步移除服务层 `@ts-nocheck`,优先处理 webhook service、自动发货 ## 执行原则