后端迁移领取换码服务
This commit is contained in:
+129
-16
@@ -1,5 +1,3 @@
|
|||||||
// @ts-check
|
|
||||||
|
|
||||||
import { reloadTencentBrowserSession, redeemTencentBrowserSession } from '../../session/session.js'
|
import { reloadTencentBrowserSession, redeemTencentBrowserSession } from '../../session/session.js'
|
||||||
import { classifyTencentRedeemResult } from '../../session/session-redeem.js'
|
import { classifyTencentRedeemResult } from '../../session/session-redeem.js'
|
||||||
import { createTaskEvent } from '../../../repositories/task-event-repo.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 { createHttpError } from '../../../utils/http.js'
|
||||||
import { nowIso } from '../../../utils/time.js'
|
import { nowIso } from '../../../utils/time.js'
|
||||||
import { maskCode, REDEEM_REPLACEMENT_LIMIT } from './shared.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<TaskRow, 'id' | 'browser_session_id'>
|
||||||
|
orderItem: Pick<OrderItemRow, 'sku_code'>
|
||||||
|
}
|
||||||
|
|
||||||
|
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<unknown>
|
||||||
|
redeemTencentBrowserSession?: (
|
||||||
|
sessionId: string,
|
||||||
|
payload: { code: string },
|
||||||
|
) => Promise<TencentRedeemSession>
|
||||||
|
classifyTencentRedeemResult?: (finalRedeem: TencentRedeemPayload | null) => TencentRedeemClassification
|
||||||
|
markInventoryItemConsumed?: (
|
||||||
|
inventoryItemId: number | string,
|
||||||
|
reason: string,
|
||||||
|
consumedAt: string,
|
||||||
|
) => Promise<InventoryItemRow | null>
|
||||||
|
createTaskEvent?: (
|
||||||
|
taskId: number | string,
|
||||||
|
eventType: string,
|
||||||
|
payload?: unknown,
|
||||||
|
createdAt?: string,
|
||||||
|
) => Promise<unknown>
|
||||||
|
invalidateReservedInventoryItem?: (
|
||||||
|
inventoryItemId: number | string,
|
||||||
|
reason: string,
|
||||||
|
updatedAt: string,
|
||||||
|
) => Promise<InventoryItemRow | null>
|
||||||
|
reserveInventoryForTask?: (payload: {
|
||||||
|
skuCode: string
|
||||||
|
taskId: number | string
|
||||||
|
credentialType?: string
|
||||||
|
roleKey?: string
|
||||||
|
inventoryGroupCodes?: string[] | null
|
||||||
|
}) => Promise<InventoryItemRow | null>
|
||||||
|
nowIso?: () => string
|
||||||
|
redeemReplacementLimit?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
type RedeemClaimTaskResult = {
|
||||||
|
session: TencentRedeemSession
|
||||||
|
inventoryItem: InventoryItemRow
|
||||||
|
classification: TencentRedeemClassification
|
||||||
|
attempts: RedeemAttemptSummary[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function redeemClaimTaskWithInventoryFallback(
|
||||||
|
context: ClaimRedeemContext,
|
||||||
|
initialInventoryItem: InventoryItemRow,
|
||||||
|
): Promise<RedeemClaimTaskResult> {
|
||||||
return redeemClaimTaskWithInventoryFallbackWithDeps(context, initialInventoryItem)
|
return redeemClaimTaskWithInventoryFallbackWithDeps(context, initialInventoryItem)
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function redeemClaimTaskWithInventoryFallbackWithDeps(
|
export async function redeemClaimTaskWithInventoryFallbackWithDeps(
|
||||||
context,
|
context: ClaimRedeemContext,
|
||||||
initialInventoryItem,
|
initialInventoryItem: InventoryItemRow,
|
||||||
{
|
{
|
||||||
reloadTencentBrowserSession: reloadRedeemSession = reloadTencentBrowserSession,
|
reloadTencentBrowserSession: reloadRedeemSession = reloadTencentBrowserSession,
|
||||||
redeemTencentBrowserSession: redeemSession = redeemTencentBrowserSession,
|
redeemTencentBrowserSession: redeemSession = redeemTencentBrowserSession,
|
||||||
@@ -29,9 +140,9 @@ export async function redeemClaimTaskWithInventoryFallbackWithDeps(
|
|||||||
reserveInventoryForTask: reserveReplacementInventory = reserveInventoryForTask,
|
reserveInventoryForTask: reserveReplacementInventory = reserveInventoryForTask,
|
||||||
nowIso: getNowIso = nowIso,
|
nowIso: getNowIso = nowIso,
|
||||||
redeemReplacementLimit = REDEEM_REPLACEMENT_LIMIT,
|
redeemReplacementLimit = REDEEM_REPLACEMENT_LIMIT,
|
||||||
} = {},
|
}: RedeemClaimTaskDeps = {},
|
||||||
) {
|
): Promise<RedeemClaimTaskResult> {
|
||||||
const attempts = []
|
const attempts: RedeemAttemptSummary[] = []
|
||||||
let currentInventoryItem = initialInventoryItem
|
let currentInventoryItem = initialInventoryItem
|
||||||
let shouldReloadBeforeNextAttempt = false
|
let shouldReloadBeforeNextAttempt = false
|
||||||
|
|
||||||
@@ -156,15 +267,14 @@ export async function redeemClaimTaskWithInventoryFallbackWithDeps(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
function createRedeemTaskStateError(
|
||||||
* @param {string} message
|
message: string,
|
||||||
* @param {{ statusCode?: number, errorCode?: string, taskStatus?: string, inventoryStatus?: string, deliveryStatus?: string, attempts?: unknown[], classification?: { retCode?: unknown } | null }} [payload]
|
payload: RedeemTaskStateErrorPayload = {},
|
||||||
*/
|
): RedeemTaskStateError {
|
||||||
function createRedeemTaskStateError(message, payload = {}) {
|
const error = createHttpError(message, {
|
||||||
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, {
|
|
||||||
statusCode: payload.statusCode || 409,
|
statusCode: payload.statusCode || 409,
|
||||||
errorCode: payload.errorCode || 'claim_redeem_failed',
|
errorCode: payload.errorCode || 'claim_redeem_failed',
|
||||||
}))
|
}) as unknown as RedeemTaskStateError
|
||||||
|
|
||||||
error.redeemTaskState = {
|
error.redeemTaskState = {
|
||||||
taskStatus: payload.taskStatus || 'retry_pending',
|
taskStatus: payload.taskStatus || 'retry_pending',
|
||||||
@@ -178,7 +288,10 @@ function createRedeemTaskStateError(message, payload = {}) {
|
|||||||
return error
|
return error
|
||||||
}
|
}
|
||||||
|
|
||||||
export function resolveTencentRedeemResultCode(finalRedeem, classification) {
|
export function resolveTencentRedeemResultCode(
|
||||||
|
finalRedeem: TencentRedeemPayload | null,
|
||||||
|
classification: Pick<TencentRedeemClassification, 'retCode'> | null,
|
||||||
|
): string {
|
||||||
if (classification?.retCode != null) {
|
if (classification?.retCode != null) {
|
||||||
return String(classification.retCode)
|
return String(classification.retCode)
|
||||||
}
|
}
|
||||||
@@ -264,13 +264,21 @@
|
|||||||
- `npm run typecheck`
|
- `npm run typecheck`
|
||||||
- `npm run build`
|
- `npm run build`
|
||||||
- `npm test` 共 125 个用例通过
|
- `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 模块
|
1. 继续迁移服务层中最核心、最常改的订单 / 履约 / claim 模块
|
||||||
2. 逐步移除服务层 `@ts-nocheck`,优先处理 claim session、webhook service、自动发货
|
2. 逐步移除服务层 `@ts-nocheck`,优先处理 webhook service、自动发货
|
||||||
|
|
||||||
## 执行原则
|
## 执行原则
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user