diff --git a/apps/backend/src/services/admin/write/kuaishou-cloud-actions.js b/apps/backend/src/services/admin/write/kuaishou-cloud-actions.ts similarity index 92% rename from apps/backend/src/services/admin/write/kuaishou-cloud-actions.js rename to apps/backend/src/services/admin/write/kuaishou-cloud-actions.ts index fd7fb4fa..20b270c3 100644 --- a/apps/backend/src/services/admin/write/kuaishou-cloud-actions.js +++ b/apps/backend/src/services/admin/write/kuaishou-cloud-actions.ts @@ -1,5 +1,3 @@ -// @ts-check - import { getOrderById } from '../../../repositories/order-repo.js' import { updateTask } from '../../../repositories/task-repo.js' import { createTaskEvent } from '../../../repositories/task-event-repo.js' @@ -46,15 +44,17 @@ import { resolvePersistedCloudtentaclesContext, } from './kuaishou-cloud-helpers.js' -/** @typedef {import('../../../types/admin-read-inputs.js').AdminEntityIdInput} AdminEntityIdInput */ -/** @typedef {import('../../../types/admin-read-inputs.js').AdminViewerSessionInput} AdminViewerSessionInput */ -/** @typedef {import('../../../types/admin-write-inputs.js').AdminTaskKuaishouCloudDispatchInput} AdminTaskKuaishouCloudDispatchInput */ -/** @typedef {import('../../../types/admin-write-models.js').AdminTaskActionResponse} AdminTaskActionResponse */ +import type { + AdminEntityIdInput, + AdminViewerSessionInput, +} from '../../../types/admin-read-inputs.js' +import type { AdminTaskKuaishouCloudDispatchInput } from '../../../types/admin-write-inputs.js' +import type { AdminTaskActionResponse } from '../../../types/admin-write-models.js' -/** @returns {Promise} */ -/** @param {AdminEntityIdInput} taskId */ -/** @param {AdminViewerSessionInput | null} [session] */ -export async function prepareAdminTaskKuaishouCloudFulfillment(taskId, session = null) { +export async function prepareAdminTaskKuaishouCloudFulfillment( + taskId: AdminEntityIdInput, + session: AdminViewerSessionInput | null = null, +): Promise { const task = await getRequiredTask(taskId) const now = nowIso() const viewerContext = createAdminViewerContext(session) @@ -226,11 +226,11 @@ export async function prepareAdminTaskKuaishouCloudFulfillment(taskId, session = } } -/** @returns {Promise} */ -/** @param {AdminEntityIdInput} taskId */ -/** @param {AdminTaskKuaishouCloudDispatchInput} [payload] */ -/** @param {AdminViewerSessionInput | null} [session] */ -export async function dispatchAdminTaskKuaishouCloudFulfillment(taskId, payload = {}, session = null) { +export async function dispatchAdminTaskKuaishouCloudFulfillment( + taskId: AdminEntityIdInput, + payload: AdminTaskKuaishouCloudDispatchInput = {}, + session: AdminViewerSessionInput | null = null, +): Promise { const task = await getRequiredTask(taskId) const now = nowIso() const viewerContext = createAdminViewerContext(session) @@ -334,10 +334,10 @@ export async function dispatchAdminTaskKuaishouCloudFulfillment(taskId, payload } } -/** @returns {Promise} */ -/** @param {AdminEntityIdInput} taskId */ -/** @param {AdminViewerSessionInput | null} [session] */ -export async function refreshAdminTaskKuaishouCloudRoleInfo(taskId, session = null) { +export async function refreshAdminTaskKuaishouCloudRoleInfo( + taskId: AdminEntityIdInput, + session: AdminViewerSessionInput | null = null, +): Promise { const task = await getRequiredTask(taskId) const now = nowIso() const viewerContext = createAdminViewerContext(session) @@ -418,10 +418,10 @@ export async function refreshAdminTaskKuaishouCloudRoleInfo(taskId, session = nu } } -/** @returns {Promise} */ -/** @param {AdminEntityIdInput} taskId */ -/** @param {AdminViewerSessionInput | null} [session] */ -export async function returnNumberAdminTaskKuaishouCloudFulfillment(taskId, session = null) { +export async function returnNumberAdminTaskKuaishouCloudFulfillment( + taskId: AdminEntityIdInput, + session: AdminViewerSessionInput | null = null, +): Promise { const task = await getRequiredTask(taskId) const now = nowIso() const viewerContext = createAdminViewerContext(session) diff --git a/apps/backend/src/services/admin/write/kuaishou-cloud-helpers.js b/apps/backend/src/services/admin/write/kuaishou-cloud-helpers.ts similarity index 81% rename from apps/backend/src/services/admin/write/kuaishou-cloud-helpers.js rename to apps/backend/src/services/admin/write/kuaishou-cloud-helpers.ts index 83d04994..ea87ef94 100644 --- a/apps/backend/src/services/admin/write/kuaishou-cloud-helpers.js +++ b/apps/backend/src/services/admin/write/kuaishou-cloud-helpers.ts @@ -1,5 +1,3 @@ -// @ts-check - import { normalizeProductName } from '../../order/product-match-service.js' import { getCloudtentaclesSourceConfig } from '../../platforms/cloudtentacles/source-config-service.js' import { getCloudtentaclesSessionState } from '../../platforms/cloudtentacles/session-state-service.js' @@ -16,11 +14,58 @@ import { createHttpError } from '../../../utils/http.js' export const KUAISHOU_CLOUD_FIXED_VN_KEY = '1' -export function isKuaishouCloudTask(task) { - return String(task?.executor_key || '').trim() === 'kuaishou_ct_assisted' +export type JsonObject = Record + +export type CloudtentaclesContext = { + baseUrl: string + token: string + deviceId: string + deviceType: number } -export function resolvePersistedCloudtentaclesContext() { +export type CloudSkuLikeItem = { + id?: number | string + name?: string + price?: number | string + count?: number | string + [key: string]: unknown +} + +export type KuaishouCloudFlow = JsonObject & { + configId: string + internalSkuCode: string + internalSkuName: string + ticket: JsonObject + binding: JsonObject + role: JsonObject + purchase: JsonObject + dispatch: JsonObject + returnNumber: JsonObject + consume: JsonObject +} + +export type KuaishouCloudBindingResources = { + skuId: number + skuName: string + vnKey: string + skuItem: CloudSkuLikeItem | null + knapsackItem: CloudSkuLikeItem | null + resolvedByName: boolean +} + +export type PreparedKuaishouCloudBindResource = { + vnKey: string + vnId: number + vnPhone: string + bindUrl: string +} + +export function isKuaishouCloudTask(task: unknown): boolean { + const record: JsonObject = task && typeof task === 'object' ? task : {} + return String(record.executor_key || '').trim() === 'kuaishou_ct_assisted' +} + +export function resolvePersistedCloudtentaclesContext(): CloudtentaclesContext { const source = getCloudtentaclesSourceConfig() const session = getCloudtentaclesSessionState() const token = String(session.token || '').trim() @@ -40,8 +85,8 @@ export function resolvePersistedCloudtentaclesContext() { } } -export function normalizeKuaishouCloudFlow(value) { - const source = value && typeof value === 'object' ? value : {} +export function normalizeKuaishouCloudFlow(value: unknown): KuaishouCloudFlow { + const source: JsonObject = value && typeof value === 'object' ? value : {} const binding = source.binding && typeof source.binding === 'object' ? source.binding : {} const role = source.role && typeof source.role === 'object' ? source.role : {} const purchase = source.purchase && typeof source.purchase === 'object' ? source.purchase : {} @@ -125,8 +170,8 @@ export function normalizeKuaishouCloudFlow(value) { } } -export function normalizeKuaishouCloudRoleInfo(value) { - const rawInfo = value && typeof value === 'object' ? value : null +export function normalizeKuaishouCloudRoleInfo(value: unknown): { name: string, rid: string, rawInfo: JsonObject | null } { + const rawInfo: JsonObject | null = value && typeof value === 'object' ? value : null const nestedBindInfo = rawInfo?.sBindInfo && typeof rawInfo.sBindInfo === 'object' ? rawInfo.sBindInfo : null const source = nestedBindInfo || rawInfo @@ -137,7 +182,7 @@ export function normalizeKuaishouCloudRoleInfo(value) { } } -export function resolveKuaishouCloudBindUrlExpiresAt(preparedAt) { +export function resolveKuaishouCloudBindUrlExpiresAt(preparedAt: unknown): string | null { const preparedTime = Date.parse(String(preparedAt || '')) if (!Number.isFinite(preparedTime)) { return null @@ -148,7 +193,10 @@ export function resolveKuaishouCloudBindUrlExpiresAt(preparedAt) { return new Date(preparedTime + Math.max(1, ttlSeconds) * 1000).toISOString() } -export function resolveKuaishouCloudBindingResources(flow, { skuItems = [], knapsackItems = [] } = {}) { +export function resolveKuaishouCloudBindingResources( + flow: KuaishouCloudFlow, + { skuItems = [], knapsackItems = [] }: { skuItems?: unknown[], knapsackItems?: unknown[] } = {}, +): KuaishouCloudBindingResources { const normalizedSkuItems = Array.isArray(skuItems) ? skuItems.filter(isCloudSkuLikeItem) : [] const normalizedKnapsackItems = Array.isArray(knapsackItems) ? knapsackItems.filter(isCloudSkuLikeItem) : [] const currentSkuId = Number(flow?.binding?.skuId || 0) || 0 @@ -192,20 +240,17 @@ export function resolveKuaishouCloudBindingResources(flow, { skuItems = [], knap } } -/** - * @param {{ flow?: any, binding?: any }} [input] - */ -export function resolveKuaishouCloudVnKeyCandidates(input = {}) { +export function resolveKuaishouCloudVnKeyCandidates(input: { flow?: KuaishouCloudFlow, binding?: KuaishouCloudBindingResources } = {}): string[] { return [KUAISHOU_CLOUD_FIXED_VN_KEY] } -/** - * @param {{ cloudContext?: Record, vnKeyCandidates?: string[] }} [input] - */ -export async function prepareKuaishouCloudBindResourceWithFallback(input = {}) { +export async function prepareKuaishouCloudBindResourceWithFallback(input: { + cloudContext?: CloudtentaclesContext + vnKeyCandidates?: string[] +} = {}): Promise { const { cloudContext = {}, vnKeyCandidates = [] } = input const candidates = Array.isArray(vnKeyCandidates) ? vnKeyCandidates : [] - let lastError = null + let lastError: unknown = null for (const vnKey of candidates) { let vnId = 0 @@ -284,7 +329,7 @@ export async function prepareKuaishouCloudBindResourceWithFallback(input = {}) { }) } -function collectKuaishouCloudNameCandidates(flow) { +function collectKuaishouCloudNameCandidates(flow: KuaishouCloudFlow): string[] { return Array.from(new Set([ String(flow?.binding?.skuName || '').trim(), String(flow?.internalSkuName || '').trim(), @@ -292,7 +337,7 @@ function collectKuaishouCloudNameCandidates(flow) { ].filter(Boolean))) } -function findCloudItemByNames(items, nameCandidates, preferredId = 0) { +function findCloudItemByNames(items: CloudSkuLikeItem[], nameCandidates: string[], preferredId = 0): CloudSkuLikeItem | null { const normalizedItems = Array.isArray(items) ? items : [] const normalizedNames = nameCandidates .map((item) => ({ @@ -333,11 +378,12 @@ function findCloudItemByNames(items, nameCandidates, preferredId = 0) { return null } -function isCloudSkuLikeItem(item) { - return Boolean(item) && typeof item === 'object' && Number(item.id || 0) > 0 +function isCloudSkuLikeItem(item: unknown): item is CloudSkuLikeItem { + const record: JsonObject = item && typeof item === 'object' ? item : {} + return Number(record.id || 0) > 0 } -function isRecoverableKuaishouCloudVnKeyError(error) { +function isRecoverableKuaishouCloudVnKeyError(error: any): boolean { const errorCode = String(error?.errorCode || error?.code || '').trim() const errorMessage = String(error?.message || '').trim() return errorCode === 'cloudtentacles_vn_bind_url_failed' diff --git a/docs/backend-typescript-migration-plan.md b/docs/backend-typescript-migration-plan.md index 8948e195..4f69ea39 100644 --- a/docs/backend-typescript-migration-plan.md +++ b/docs/backend-typescript-migration-plan.md @@ -776,6 +776,15 @@ - `npm run typecheck` - `npm run build` - `npm test` 共 139 个用例通过 +198. 后台 Admin 快手 Cloud 写侧 helper / actions 迁移到 `.ts`: + - `src/services/admin/write/kuaishou-cloud-helpers.ts` + - `src/services/admin/write/kuaishou-cloud-actions.ts` +199. 快手 Cloud flow 规范化、SKU / 背包资源匹配、VN Key 候选、绑定资源准备与回滚、准备绑定、发货、刷新角色、退号和快手核销动作已进入 TS 编译链路;cloud context、flow、SKU-like item、binding resources、prepared bind resource、dispatch input 与 action response 补齐类型 +200. Docker 内验证通过: + - `src/services/admin/*.test.js` 共 8 个用例通过 + - `npm run typecheck` + - `npm run build` + - `npm test` 共 139 个用例通过 ## 下一步建议