后端迁移快手电子券平台服务

This commit is contained in:
yml
2026-05-21 17:15:58 +08:00
parent 807c0f95e9
commit 717866e5fb
6 changed files with 162 additions and 59 deletions
@@ -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<string, any>
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]'
}
@@ -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<string, any>
type KuaishouEticketRequestOptions = JsonObject & {
method?: string
body?: unknown
headers?: Record<string, unknown>
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<string, string>
body?: string
signal: AbortSignal
},
): Promise<NodeHttpResponse> {
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<string, string>()
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
},
}
@@ -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<string, any>
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]'
}
@@ -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<string, unknown>
}
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<string, string> {
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<string, string> {
if (!extra || typeof extra !== 'object') {
return {}
}
@@ -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<string, any>
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]'
}
+13
View File
@@ -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`
## 下一步建议