diff --git a/apps/backend/src/services/platforms/kuaishou-eticket/consume-service.js b/apps/backend/src/services/platforms/kuaishou-eticket/consume-service.ts similarity index 81% rename from apps/backend/src/services/platforms/kuaishou-eticket/consume-service.js rename to apps/backend/src/services/platforms/kuaishou-eticket/consume-service.ts index 6703d81d..006d7732 100644 --- a/apps/backend/src/services/platforms/kuaishou-eticket/consume-service.js +++ b/apps/backend/src/services/platforms/kuaishou-eticket/consume-service.ts @@ -1,10 +1,23 @@ -// @ts-check - import { createHttpError } from '../../../utils/http.js' import { kuaishouEticketRequest } from './http-client.js' import { resolveKuaishouEticketConfig } from './shared.js' -export async function queryKuaishouEticketConsumeDetail(payload = {}) { +type JsonObject = Record + +type KuaishouEticketDetailPayload = { + eTicketId: string + oid: string + formToken: string + leftCount: number + [key: string]: unknown +} + +type KuaishouEticketDetailResult = { + detail: KuaishouEticketDetailPayload | null + [key: string]: any +} + +export async function queryKuaishouEticketConsumeDetail(payload: JsonObject = {}) { const context = resolveKuaishouEticketActionContext(payload) const response = await kuaishouEticketRequest(context.detailPath, { baseUrl: context.baseUrl, @@ -21,7 +34,7 @@ export async function queryKuaishouEticketConsumeDetail(payload = {}) { }) } -export async function consumeKuaishouEticket(payload = {}) { +export async function consumeKuaishouEticket(payload: JsonObject = {}) { const context = resolveKuaishouEticketActionContext(payload) const detailResult = shouldResolveConsumeDetail(payload) ? await queryKuaishouEticketConsumeDetail({ @@ -51,7 +64,7 @@ export async function consumeKuaishouEticket(payload = {}) { }) } -export function mapKuaishouEticketDetailResult(payload = {}, context = {}) { +export function mapKuaishouEticketDetailResult(payload: JsonObject = {}, context: JsonObject = {}) { const normalized = normalizeKuaishouEticketApiResult(payload) const detail = mapKuaishouEticketDetailPayload(payload?.data?.eTicket) const goods = mapKuaishouEticketGoodsPayload(payload?.data?.goods) @@ -71,7 +84,7 @@ export function mapKuaishouEticketDetailResult(payload = {}, context = {}) { } } -export function mapKuaishouEticketConsumeResult(payload = {}, context = {}) { +export function mapKuaishouEticketConsumeResult(payload: JsonObject = {}, context: JsonObject = {}) { const normalized = normalizeKuaishouEticketApiResult(payload) return { @@ -93,7 +106,10 @@ export function mapKuaishouEticketConsumeResult(payload = {}, context = {}) { } } -export function resolveKuaishouEticketConsumePayload(payload = {}, detailResult = null) { +export function resolveKuaishouEticketConsumePayload( + payload: JsonObject = {}, + detailResult: KuaishouEticketDetailResult | null = null, +) { const detail = detailResult?.detail || null const eTicketId = pickFirstNonEmpty([payload.eTicketId, detail?.eTicketId]) const oid = pickFirstNonEmpty([payload.oid, detail?.oid]) @@ -131,7 +147,7 @@ export function resolveKuaishouEticketConsumePayload(payload = {}, detailResult } } -export function normalizeKuaishouEticketApiResult(payload = {}) { +export function normalizeKuaishouEticketApiResult(payload: JsonObject = {}) { const result = Number(payload?.result || 0) return { @@ -143,7 +159,7 @@ export function normalizeKuaishouEticketApiResult(payload = {}) { } } -export function mapKuaishouEticketDetailPayload(value = {}) { +export function mapKuaishouEticketDetailPayload(value: unknown): KuaishouEticketDetailPayload | null { if (!isPlainObject(value)) { return null } @@ -164,7 +180,7 @@ export function mapKuaishouEticketDetailPayload(value = {}) { } } -export function mapKuaishouEticketGoodsPayload(value = {}) { +export function mapKuaishouEticketGoodsPayload(value: unknown) { if (!isPlainObject(value)) { return null } @@ -179,7 +195,7 @@ export function mapKuaishouEticketGoodsPayload(value = {}) { } } -function resolveKuaishouEticketActionContext(payload = {}) { +function resolveKuaishouEticketActionContext(payload: JsonObject = {}) { const config = resolveKuaishouEticketConfig(payload) const eTicketId = String(payload.eTicketId || '').trim() @@ -203,11 +219,11 @@ function resolveKuaishouEticketActionContext(payload = {}) { } } -function shouldResolveConsumeDetail(payload = {}) { +function shouldResolveConsumeDetail(payload: JsonObject = {}): boolean { return !String(payload.oid || '').trim() || !String(payload.formToken || '').trim() } -function pickFirstNonEmpty(values) { +function pickFirstNonEmpty(values: unknown[]): string { for (const value of values) { const normalized = String(value || '').trim() if (normalized) { @@ -218,16 +234,16 @@ function pickFirstNonEmpty(values) { return '' } -function normalizePositiveInteger(value, fallback) { +function normalizePositiveInteger(value: unknown, fallback: number): number { const parsed = Number(value) return Number.isInteger(parsed) && parsed > 0 ? parsed : fallback } -function normalizeInteger(value, fallback) { +function normalizeInteger(value: unknown, fallback: number): number { const parsed = Number(value) return Number.isInteger(parsed) ? parsed : fallback } -function isPlainObject(value) { +function isPlainObject(value: unknown): value is JsonObject { return Object.prototype.toString.call(value) === '[object Object]' } diff --git a/apps/backend/src/services/platforms/kuaishou-eticket/http-client.js b/apps/backend/src/services/platforms/kuaishou-eticket/http-client.ts similarity index 79% rename from apps/backend/src/services/platforms/kuaishou-eticket/http-client.js rename to apps/backend/src/services/platforms/kuaishou-eticket/http-client.ts index ec11ebae..f9e7fe0e 100644 --- a/apps/backend/src/services/platforms/kuaishou-eticket/http-client.js +++ b/apps/backend/src/services/platforms/kuaishou-eticket/http-client.ts @@ -1,14 +1,35 @@ -// @ts-check - import http from 'node:http' import https from 'node:https' +import type { IncomingHttpHeaders } from 'node:http' import { parseJsonObject } from '../../../utils/json.js' import { createHttpError } from '../../../utils/http.js' import { logInfo } from '../../../utils/logger.js' import { buildKuaishouEticketHeaders, buildKuaishouEticketUrl, resolveKuaishouEticketConfig } from './shared.js' -export async function kuaishouEticketRequest(pathname, options = {}) { +type JsonObject = Record + +type KuaishouEticketRequestOptions = JsonObject & { + method?: string + body?: unknown + headers?: Record + timeoutMs?: number | string + cookie?: string + contentType?: string | null +} + +type HeaderAdapter = { + get(name: string): string | null +} + +type NodeHttpResponse = { + ok: boolean + status: number + headers: HeaderAdapter + bodyText: string +} + +export async function kuaishouEticketRequest(pathname: string, options: KuaishouEticketRequestOptions = {}) { const config = resolveKuaishouEticketConfig(options) const url = buildKuaishouEticketUrl(config.baseUrl, pathname) const timeoutMs = Number(options.timeoutMs || config.timeoutMs || 8000) @@ -69,7 +90,7 @@ export async function kuaishouEticketRequest(pathname, options = {}) { } } -function inferContentType(body) { +function inferContentType(body: unknown): string { if (body == null) { return '' } @@ -81,7 +102,7 @@ function inferContentType(body) { return 'application/json' } -function normalizeRequestBody(body, contentType) { +function normalizeRequestBody(body: unknown, contentType: unknown): string | undefined { if (body == null) { return undefined } @@ -97,7 +118,15 @@ function normalizeRequestBody(body, contentType) { return String(body) } -async function requestViaNodeHttp(url, { method, headers, body, signal }) { +async function requestViaNodeHttp( + url: URL, + { method, headers, body, signal }: { + method: string + headers: Record + body?: string + signal: AbortSignal + }, +): Promise { const isHttps = url.protocol === 'https:' const transport = isHttps ? https : http @@ -107,7 +136,7 @@ async function requestViaNodeHttp(url, { method, headers, body, signal }) { headers, rejectUnauthorized: false, }, (response) => { - const chunks = [] + const chunks: Buffer[] = [] response.on('data', (chunk) => { chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)) @@ -148,8 +177,8 @@ async function requestViaNodeHttp(url, { method, headers, body, signal }) { }) } -function createHeaderAdapter(headers) { - const normalized = new Map() +function createHeaderAdapter(headers: IncomingHttpHeaders): HeaderAdapter { + const normalized = new Map() for (const [key, value] of Object.entries(headers || {})) { if (Array.isArray(value)) { @@ -163,7 +192,7 @@ function createHeaderAdapter(headers) { } return { - get(name) { + get(name: string) { return normalized.get(String(name || '').toLowerCase()) || null }, } diff --git a/apps/backend/src/services/platforms/kuaishou-eticket/info-service.js b/apps/backend/src/services/platforms/kuaishou-eticket/info-service.ts similarity index 83% rename from apps/backend/src/services/platforms/kuaishou-eticket/info-service.js rename to apps/backend/src/services/platforms/kuaishou-eticket/info-service.ts index 496247cf..f180f44a 100644 --- a/apps/backend/src/services/platforms/kuaishou-eticket/info-service.js +++ b/apps/backend/src/services/platforms/kuaishou-eticket/info-service.ts @@ -1,10 +1,10 @@ -// @ts-check - import { createHttpError } from '../../../utils/http.js' import { kuaishouEticketRequest } from './http-client.js' import { resolveKuaishouEticketConfig } from './shared.js' -export async function queryKuaishouEticketStableInfo(payload = {}) { +type JsonObject = Record + +export async function queryKuaishouEticketStableInfo(payload: JsonObject = {}) { const config = resolveKuaishouEticketConfig(payload) const cookie = String(payload.cookie || config.cookie || '').trim() @@ -29,7 +29,7 @@ export async function queryKuaishouEticketStableInfo(payload = {}) { }) } -export function mapKuaishouEticketStableInfoResult(payload = {}, context = {}) { +export function mapKuaishouEticketStableInfoResult(payload: JsonObject = {}, context: JsonObject = {}) { const result = Number(payload?.result || 0) const userInfo = isPlainObject(payload?.data?.userInfo) ? payload.data.userInfo : {} const bizStatus = isPlainObject(payload?.data?.bizStatus) ? payload.data.bizStatus : {} @@ -51,11 +51,11 @@ export function mapKuaishouEticketStableInfoResult(payload = {}, context = {}) { } } -function normalizeInteger(value, fallback) { +function normalizeInteger(value: unknown, fallback: number): number { const parsed = Number(value) return Number.isInteger(parsed) ? parsed : fallback } -function isPlainObject(value) { +function isPlainObject(value: unknown): value is JsonObject { return Object.prototype.toString.call(value) === '[object Object]' } diff --git a/apps/backend/src/services/platforms/kuaishou-eticket/shared.js b/apps/backend/src/services/platforms/kuaishou-eticket/shared.ts similarity index 68% rename from apps/backend/src/services/platforms/kuaishou-eticket/shared.js rename to apps/backend/src/services/platforms/kuaishou-eticket/shared.ts index 51552c4b..e966bcb3 100644 --- a/apps/backend/src/services/platforms/kuaishou-eticket/shared.js +++ b/apps/backend/src/services/platforms/kuaishou-eticket/shared.ts @@ -1,10 +1,41 @@ -// @ts-check - const DEFAULT_BASE_URL = 'https://s.kwaixiaodian.com' const DEFAULT_REFERER_PATH = '/zone/industry/voucher/verify-list' const DEFAULT_USER_AGENT = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/147.0.0.0 Safari/537.36' -export function resolveKuaishouEticketConfig(overrides = {}) { +export type KuaishouEticketConfigInput = { + enabled?: boolean + baseUrl?: string + cookie?: string + timeoutMs?: number | string + infoPath?: string + detailPath?: string + consumePath?: string + refererPath?: string + userAgent?: string +} + +export type KuaishouEticketConfig = { + enabled: boolean + baseUrl: string + cookie: string + timeoutMs: number + infoPath: string + detailPath: string + consumePath: string + refererPath: string + userAgent: string +} + +type HeaderInput = { + baseUrl?: string + refererPath?: string + userAgent?: string + cookie?: string + contentType?: string + extra?: Record +} + +export function resolveKuaishouEticketConfig(overrides: KuaishouEticketConfigInput = {}): KuaishouEticketConfig { return { enabled: overrides.enabled !== false, baseUrl: normalizeBaseUrl(overrides.baseUrl || DEFAULT_BASE_URL) || DEFAULT_BASE_URL, @@ -18,7 +49,7 @@ export function resolveKuaishouEticketConfig(overrides = {}) { } } -export function buildKuaishouEticketUrl(baseUrl, pathname) { +export function buildKuaishouEticketUrl(baseUrl: unknown, pathname: unknown): URL { return new URL(normalizePath(pathname, '/'), normalizeBaseUrl(baseUrl) || DEFAULT_BASE_URL) } @@ -29,7 +60,7 @@ export function buildKuaishouEticketHeaders({ cookie = '', contentType = 'application/json', extra = {}, -} = {}) { +}: HeaderInput = {}): Record { return { accept: 'application/json', 'accept-language': 'zh-CN,zh;q=0.9', @@ -44,11 +75,11 @@ export function buildKuaishouEticketHeaders({ } } -function normalizeBaseUrl(value) { +function normalizeBaseUrl(value: unknown): string { return String(value || '').trim().replace(/\/+$/, '') } -function normalizePath(value, fallback) { +function normalizePath(value: unknown, fallback: string): string { const normalized = String(value || '').trim() if (!normalized) { return fallback @@ -57,12 +88,12 @@ function normalizePath(value, fallback) { return normalized.startsWith('/') ? normalized : `/${normalized}` } -function normalizePositiveInteger(value, fallback) { +function normalizePositiveInteger(value: unknown, fallback: number): number { const parsed = Number(value) return Number.isInteger(parsed) && parsed > 0 ? parsed : fallback } -function normalizeHeaderMap(extra) { +function normalizeHeaderMap(extra: unknown): Record { if (!extra || typeof extra !== 'object') { return {} } diff --git a/apps/backend/src/services/platforms/kuaishou-eticket/source-config-service.js b/apps/backend/src/services/platforms/kuaishou-eticket/source-config-service.ts similarity index 63% rename from apps/backend/src/services/platforms/kuaishou-eticket/source-config-service.js rename to apps/backend/src/services/platforms/kuaishou-eticket/source-config-service.ts index 9d20270b..01664c51 100644 --- a/apps/backend/src/services/platforms/kuaishou-eticket/source-config-service.js +++ b/apps/backend/src/services/platforms/kuaishou-eticket/source-config-service.ts @@ -1,5 +1,3 @@ -// @ts-check - import fs from 'node:fs' import path from 'node:path' @@ -8,26 +6,42 @@ import { PROJECT_ROOT } from '../../../config/runtime.js' const KUAISHOU_ETICKET_SOURCE_FILE_PATH = path.join(PROJECT_ROOT, 'data', 'kuaishou-eticket-source.json') const DEFAULT_BASE_URL = 'https://s.kwaixiaodian.com' -export function getKuaishouEticketSourceFilePath() { +type JsonObject = Record + +export type KuaishouEticketShopConfig = { + shopId: string + kshopName: string + cookie: string + userAvatar: string + enabled: boolean +} + +export type KuaishouEticketSourceConfig = { + enabled: boolean + baseUrl: string + shops: KuaishouEticketShopConfig[] +} + +export function getKuaishouEticketSourceFilePath(): string { return KUAISHOU_ETICKET_SOURCE_FILE_PATH } -export function getKuaishouEticketSourceConfig() { +export function getKuaishouEticketSourceConfig(): KuaishouEticketSourceConfig { return loadKuaishouEticketSourceConfigFromFile() } -export function saveKuaishouEticketSourceConfig(rawValue) { +export function saveKuaishouEticketSourceConfig(rawValue: unknown): KuaishouEticketSourceConfig { const normalized = normalizeKuaishouEticketSourceConfig(rawValue) fs.mkdirSync(path.dirname(KUAISHOU_ETICKET_SOURCE_FILE_PATH), { recursive: true }) fs.writeFileSync(KUAISHOU_ETICKET_SOURCE_FILE_PATH, `${JSON.stringify(normalized, null, 2)}\n`, 'utf8') return normalized } -export function listKuaishouEticketShopConfigs(source = getKuaishouEticketSourceConfig()) { +export function listKuaishouEticketShopConfigs(source: KuaishouEticketSourceConfig = getKuaishouEticketSourceConfig()): KuaishouEticketShopConfig[] { return Array.isArray(source?.shops) ? source.shops : [] } -export function findKuaishouEticketShopConfig(shopId, source = getKuaishouEticketSourceConfig()) { +export function findKuaishouEticketShopConfig(shopId: unknown, source: KuaishouEticketSourceConfig = getKuaishouEticketSourceConfig()): KuaishouEticketShopConfig | null { const normalizedShopId = String(shopId || '').trim() if (!normalizedShopId) { return null @@ -36,7 +50,7 @@ export function findKuaishouEticketShopConfig(shopId, source = getKuaishouEticke return listKuaishouEticketShopConfigs(source).find((item) => String(item.shopId || '').trim() === normalizedShopId) || null } -export function findKuaishouEticketShopConfigByName(kshopName, source = getKuaishouEticketSourceConfig()) { +export function findKuaishouEticketShopConfigByName(kshopName: unknown, source: KuaishouEticketSourceConfig = getKuaishouEticketSourceConfig()): KuaishouEticketShopConfig | null { const normalizedName = String(kshopName || '').trim() if (!normalizedName) { return null @@ -46,9 +60,9 @@ export function findKuaishouEticketShopConfigByName(kshopName, source = getKuais } export function resolveKuaishouEticketShopConfig( - { shopId = '', kshopName = '', shopName = '' } = {}, - source = getKuaishouEticketSourceConfig(), -) { + { shopId = '', kshopName = '', shopName = '' }: { shopId?: unknown, kshopName?: unknown, shopName?: unknown } = {}, + source: KuaishouEticketSourceConfig = getKuaishouEticketSourceConfig(), +): KuaishouEticketShopConfig | null { const byId = findKuaishouEticketShopConfig(shopId, source) if (byId) { return byId @@ -62,11 +76,11 @@ export function resolveKuaishouEticketShopConfig( return findKuaishouEticketShopConfigByName(normalizedName, source) } -export function getFirstAvailableKuaishouEticketShop(source = getKuaishouEticketSourceConfig()) { +export function getFirstAvailableKuaishouEticketShop(source: KuaishouEticketSourceConfig = getKuaishouEticketSourceConfig()): KuaishouEticketShopConfig | null { return listKuaishouEticketShopConfigs(source).find((item) => item.enabled !== false && String(item.cookie || '').trim()) || null } -function loadKuaishouEticketSourceConfigFromFile() { +function loadKuaishouEticketSourceConfigFromFile(): KuaishouEticketSourceConfig { if (!fs.existsSync(KUAISHOU_ETICKET_SOURCE_FILE_PATH)) { return createDefaultKuaishouEticketSourceConfig() } @@ -79,7 +93,7 @@ function loadKuaishouEticketSourceConfigFromFile() { } } -function normalizeKuaishouEticketSourceConfig(rawValue) { +function normalizeKuaishouEticketSourceConfig(rawValue: unknown): KuaishouEticketSourceConfig { const source = isPlainObject(rawValue) ? rawValue : {} const shops = Array.isArray(source.shops) ? source.shops @@ -94,7 +108,7 @@ function normalizeKuaishouEticketSourceConfig(rawValue) { } } -function normalizeKuaishouEticketShopConfig(rawValue) { +function normalizeKuaishouEticketShopConfig(rawValue: unknown): KuaishouEticketShopConfig | null { if (!isPlainObject(rawValue)) { return null } @@ -118,7 +132,7 @@ function normalizeKuaishouEticketShopConfig(rawValue) { } } -function buildLegacySingleShopList(source) { +function buildLegacySingleShopList(source: JsonObject): JsonObject[] { const cookie = String(source.cookie || '').trim() if (!cookie) { return [] @@ -135,7 +149,7 @@ function buildLegacySingleShopList(source) { ] } -function createDefaultKuaishouEticketSourceConfig() { +function createDefaultKuaishouEticketSourceConfig(): KuaishouEticketSourceConfig { return { enabled: true, baseUrl: DEFAULT_BASE_URL, @@ -143,6 +157,6 @@ function createDefaultKuaishouEticketSourceConfig() { } } -function isPlainObject(value) { +function isPlainObject(value: unknown): value is JsonObject { return Object.prototype.toString.call(value) === '[object Object]' } diff --git a/docs/backend-typescript-migration-plan.md b/docs/backend-typescript-migration-plan.md index 4ed247b0..8070edc4 100644 --- a/docs/backend-typescript-migration-plan.md +++ b/docs/backend-typescript-migration-plan.md @@ -795,6 +795,19 @@ - `npm run build` - `npm test` 共 139 个用例通过 204. `src/services/admin` 目录下非测试 `.js` 服务文件已全部迁移为 `.ts` +205. 快手电子券平台服务迁移到 `.ts`: + - `src/services/platforms/kuaishou-eticket/shared.ts` + - `src/services/platforms/kuaishou-eticket/http-client.ts` + - `src/services/platforms/kuaishou-eticket/source-config-service.ts` + - `src/services/platforms/kuaishou-eticket/info-service.ts` + - `src/services/platforms/kuaishou-eticket/consume-service.ts` +206. 快手小店核销配置、Node HTTP 请求、店铺来源配置读写、店铺稳定信息查询、电子券详情查询与核销 payload / result 映射已进入 TS 编译链路;config、headers、HTTP response、source/shop config、detail result 与动态 payload 补齐类型 +207. Docker 内验证通过: + - `src/services/platforms/kuaishou-eticket/*.test.js` 共 4 个用例通过 + - `npm run typecheck` + - `npm run build` + - `npm test` 共 139 个用例通过 +208. `src/services` 目录下非测试 `.js` 服务文件已全部迁移为 `.ts` ## 下一步建议