diff --git a/apps/backend/src/repositories/kuaishou-cloud-task-state-repo.ts b/apps/backend/src/repositories/kuaishou-cloud-task-state-repo.ts index a605a945..0e6673e5 100644 --- a/apps/backend/src/repositories/kuaishou-cloud-task-state-repo.ts +++ b/apps/backend/src/repositories/kuaishou-cloud-task-state-repo.ts @@ -3,10 +3,9 @@ import { TASK_STATUS } from '../domain/task-status.js' import { maskCode } from '../utils/masking.js' import { parseTaskContext } from '../utils/task-json.js' import { normalizeTimestampIso } from '../utils/time.js' +import type { JsonObject, JsonRecord } from '../types/json.js' type QueryExecutor = (text: string, params?: unknown[]) => Promise<{ rows: unknown[] }> -type JsonRecord = Record - export type KuaishouCloudSourceLoadStat = { sourceKey: string activeCount: number diff --git a/apps/backend/src/routes/admin/audit-logs.ts b/apps/backend/src/routes/admin/audit-logs.ts index bbb6d7fb..fa9a1ca2 100644 --- a/apps/backend/src/routes/admin/audit-logs.ts +++ b/apps/backend/src/routes/admin/audit-logs.ts @@ -1,9 +1,10 @@ import { Router } from 'express' +import type { JsonObject } from '../../types/json.js' import { getAdminAuditLogs } from '../../services/admin/admin-audit-service.js' import { createJsonHandler, requireAdminRoles } from './session.js' -type AdminAuditLogRouteQuery = Record +type AdminAuditLogRouteQuery = JsonObject const router = Router() diff --git a/apps/backend/src/routes/kuaishou-industry.ts b/apps/backend/src/routes/kuaishou-industry.ts index 12cddfa0..d5529da1 100644 --- a/apps/backend/src/routes/kuaishou-industry.ts +++ b/apps/backend/src/routes/kuaishou-industry.ts @@ -1,5 +1,6 @@ import process from 'node:process' import { Router } from 'express' +import type { JsonObject } from '../types/json.js' import { createRateLimitMiddleware } from '../middleware/rate-limit.js' import { handleSendCode } from '../services/platforms/kuaishou-industry/send-code-service.js' @@ -152,10 +153,10 @@ router.post('/consume-code', industryRateLimit, async (req, res) => { } }) -function mergeRequestParams(req: any): Record { +function mergeRequestParams(req: any): JsonObject { const query = req.query || {} const body = req.body || {} - const params: Record = { ...query } + const params: JsonObject = { ...query } for (const [key, value] of Object.entries(body)) { if (params[key] === undefined && key !== 'param') { diff --git a/apps/backend/src/services/admin/admin-audit-service.ts b/apps/backend/src/services/admin/admin-audit-service.ts index 4010838c..3ed239f4 100644 --- a/apps/backend/src/services/admin/admin-audit-service.ts +++ b/apps/backend/src/services/admin/admin-audit-service.ts @@ -1,8 +1,7 @@ import { createAdminAuditLog, listAdminAuditLogs } from '../../repositories/admin-audit-log-repo.js' import { nowIso } from '../../utils/time.js' import { normalizeDateQuery, normalizePage, normalizePageSize, safeParseJson } from './admin-query-utils.js' - -type JsonObject = Record +import type { JsonObject } from '../../types/json.js' export async function writeAdminAuditLog(session: JsonObject | null | undefined, payload: JsonObject = {}) { if (!session?.userId) { diff --git a/apps/backend/src/services/admin/admin-auth-service.ts b/apps/backend/src/services/admin/admin-auth-service.ts index 4fb558ba..d9aeaec8 100644 --- a/apps/backend/src/services/admin/admin-auth-service.ts +++ b/apps/backend/src/services/admin/admin-auth-service.ts @@ -1,4 +1,5 @@ import crypto from 'node:crypto' +import type { JsonObject } from '../../types/json.js' import { runtimeConfig } from '../../config/runtime.js' import { @@ -15,8 +16,6 @@ import { normalizePage, normalizePageSize } from './admin-query-utils.js' type AdminUserRow = NonNullable>> -type JsonObject = Record - export type AdminSession = { sessionId: string userId: number diff --git a/apps/backend/src/services/admin/admin-dashboard-service.ts b/apps/backend/src/services/admin/admin-dashboard-service.ts index 11933c89..b3243a7f 100644 --- a/apps/backend/src/services/admin/admin-dashboard-service.ts +++ b/apps/backend/src/services/admin/admin-dashboard-service.ts @@ -1,8 +1,7 @@ import { query } from '../../db/client.js' import { TASK_STATUS } from '../../domain/task-status.js' import { nowIso } from '../../utils/time.js' - -type JsonObject = Record +import type { JsonObject } from '../../types/json.js' export async function getAdminDashboardSummary() { const todayPrefix = nowIso().slice(0, 10) diff --git a/apps/backend/src/services/admin/admin-query-utils.ts b/apps/backend/src/services/admin/admin-query-utils.ts index 967f515b..71a3df6d 100644 --- a/apps/backend/src/services/admin/admin-query-utils.ts +++ b/apps/backend/src/services/admin/admin-query-utils.ts @@ -1,5 +1,4 @@ -type JsonObject = Record - +import type { JsonObject } from '../../types/json.js' export function normalizePage(rawValue: unknown) { const parsed = Number(rawValue) return Number.isFinite(parsed) && parsed > 0 ? Math.floor(parsed) : 1 diff --git a/apps/backend/src/services/admin/admin-read-service.ts b/apps/backend/src/services/admin/admin-read-service.ts index a2615c66..8c2fbae3 100644 --- a/apps/backend/src/services/admin/admin-read-service.ts +++ b/apps/backend/src/services/admin/admin-read-service.ts @@ -5,6 +5,7 @@ import { listOrderItemsByOrderId } from '../../repositories/order-item-repo.js' import { getOrderById, listOrders } from '../../repositories/order-repo.js' import { getTaskById, listTasks, listTasksByOrderId } from '../../repositories/task-repo.js' import { listTaskEventsByTaskId } from '../../repositories/task-event-repo.js' +import type { JsonObject, JsonRecord } from '../../types/json.js' import { listKuaishouIndustryVouchersByOid, listKuaishouIndustryVouchersByTaskId, @@ -58,6 +59,7 @@ import type { AdminOrderListResponse, AdminTaskListResponse, } from '../../types/admin/read-models.js' +import type { AdminTaskDetailView } from '../../types/admin/task-detail.js' import type { AdminEntityIdInput, AdminOrderListQueryInput, @@ -69,8 +71,6 @@ import type { TaskRow, } from '../../types/repository/rows.js' -type JsonRecord = Record - export async function getAdminOrders( query: AdminOrderListQueryInput = {}, ): Promise { @@ -218,7 +218,7 @@ export async function getAdminTasks( export async function getAdminTaskDetail( taskId: AdminEntityIdInput, session: AdminViewerSessionInput | null = null, -): Promise { +): Promise { const task = await getTaskById(Number(taskId)) if (!task) { diff --git a/apps/backend/src/services/admin/admin-read-shared-helpers.ts b/apps/backend/src/services/admin/admin-read-shared-helpers.ts index 0d5ff8f0..c12a90a5 100644 --- a/apps/backend/src/services/admin/admin-read-shared-helpers.ts +++ b/apps/backend/src/services/admin/admin-read-shared-helpers.ts @@ -1,5 +1,6 @@ import { safeParseJson } from './admin-query-utils.js' import { normalizeAdminRole } from './admin-auth-service.js' +import { asJsonObject, type JsonObject, JsonRecord } from '../../types/json.js' import { parseTaskContext as parseTaskContextValue, parseTaskState as parseTaskStateValue, @@ -9,7 +10,6 @@ import { canRegenerateClaimLinkStatus, isTaskFinalStatus } from '../../domain/ta import type { AdminViewerSessionInput } from '../../types/admin/read-inputs.js' import type { OrderItemRow, TaskRow } from '../../types/repository/rows.js' -type JsonRecord = Record type CloudSourceLabelMap = Map | Record type KuaishouCloudFulfillmentMapOptions = { @@ -74,14 +74,14 @@ export function mapKuaishouCloudFulfillmentContext( } const record = value as JsonRecord - const ticket = record.ticket && typeof record.ticket === 'object' ? record.ticket : {} - const binding = record.binding && typeof record.binding === 'object' ? record.binding : {} - const role = record.role && typeof record.role === 'object' ? record.role : {} - const purchase = record.purchase && typeof record.purchase === 'object' ? record.purchase : {} - const dispatch = record.dispatch && typeof record.dispatch === 'object' ? record.dispatch : {} + const ticket = asJsonObject(record.ticket) + const binding = asJsonObject(record.binding) + const role = asJsonObject(record.role) + const purchase = asJsonObject(record.purchase) + const dispatch = asJsonObject(record.dispatch) const returnNumber = - record.returnNumber && typeof record.returnNumber === 'object' ? record.returnNumber : {} - const consume = record.consume && typeof record.consume === 'object' ? record.consume : {} + asJsonObject(record.returnNumber) + const consume = asJsonObject(record.consume) const cloudSourceKeys = Array.isArray(binding.cloudSourceKeys) ? binding.cloudSourceKeys.map((value: unknown) => String(value || '').trim()).filter(Boolean) : [] diff --git a/apps/backend/src/services/admin/admin-task-read-helpers.ts b/apps/backend/src/services/admin/admin-task-read-helpers.ts index 022372e9..a399a247 100644 --- a/apps/backend/src/services/admin/admin-task-read-helpers.ts +++ b/apps/backend/src/services/admin/admin-task-read-helpers.ts @@ -1,6 +1,7 @@ import { getTaskById } from '../../repositories/task-repo.js' import { createHttpError } from '../../utils/http.js' import { safeParseJson } from './admin-query-utils.js' +import { asJsonObject, type JsonObject, JsonRecord } from '../../types/json.js' import { TASK_STATUS, isTaskFulfillmentCompletedStatus, @@ -22,8 +23,6 @@ import type { } from '../../types/repository/rows.js' import type { AdminViewerContext } from './admin-read-shared-helpers.js' -type JsonRecord = Record - type TaskFulfillmentState = { resourceStatus: string customerStatus: string @@ -263,7 +262,7 @@ function isTaskFulfillmentCompleted(task: TaskRow | null | undefined): boolean { } function normalizeRecord(value: unknown): JsonRecord { - return value && typeof value === 'object' && !Array.isArray(value) ? value : {} + return asJsonObject(value) } function getTaskRetryCount(task: TaskRow | null | undefined): number { diff --git a/apps/backend/src/services/admin/kuaishou-industry-admin-service.ts b/apps/backend/src/services/admin/kuaishou-industry-admin-service.ts index 1b1c6138..8a2d801a 100644 --- a/apps/backend/src/services/admin/kuaishou-industry-admin-service.ts +++ b/apps/backend/src/services/admin/kuaishou-industry-admin-service.ts @@ -1,3 +1,4 @@ +import { asJsonObject, type JsonObject } from '../../types/json.js' import { findKuaishouIndustryVoucherByCode, listKuaishouIndustryVouchersByOid, @@ -27,8 +28,6 @@ import { reverseKuaishouIndustryCallback } from '../platforms/kuaishou-industry/ import type { KuaishouIndustryOpenApiCallResult } from '../platforms/kuaishou-industry/openapi-client.js' import type { KuaishouIndustryVoucherRow, TaskRow } from '../../types/repository/rows.js' -type JsonObject = Record - export async function listAdminKuaishouIndustryVouchers( input: KuaishouIndustryVoucherAdminListQuery = {}, ) { @@ -462,7 +461,7 @@ function parseJsonObject(value: unknown): JsonObject { try { const parsed = JSON.parse(String(value || '{}')) - return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : {} + return asJsonObject(parsed) } catch { return {} } diff --git a/apps/backend/src/services/admin/platform-config/cloudtentacles/context.ts b/apps/backend/src/services/admin/platform-config/cloudtentacles/context.ts index c583632e..20d7dfe6 100644 --- a/apps/backend/src/services/admin/platform-config/cloudtentacles/context.ts +++ b/apps/backend/src/services/admin/platform-config/cloudtentacles/context.ts @@ -1,5 +1,3 @@ -type JsonObject = Record - export function pickFirstNonEmpty(values: unknown[]) { for (const value of values) { const normalized = String(value || "").trim(); @@ -17,6 +15,7 @@ export function isPlainObject(value: unknown): value is JsonObject { import { getCloudtentaclesSourceByKey } from "../../../platforms/cloudtentacles/source-config-service.js"; import { getCloudtentaclesSessionStateByKey } from "../../../platforms/cloudtentacles/session-state-service.js"; +import type { JsonObject } from '../../../../types/json.js' import { normalizeCloudtentaclesDeviceId, normalizeCloudtentaclesDeviceType, diff --git a/apps/backend/src/services/admin/platform-config/cloudtentacles/index.ts b/apps/backend/src/services/admin/platform-config/cloudtentacles/index.ts index 45a902b2..df2403f5 100644 --- a/apps/backend/src/services/admin/platform-config/cloudtentacles/index.ts +++ b/apps/backend/src/services/admin/platform-config/cloudtentacles/index.ts @@ -1,3 +1,4 @@ +import type { JsonObject } from '../../../../types/json.js' import { getCloudtentaclesSourceByKey, getCloudtentaclesSourcesFilePath, @@ -79,8 +80,6 @@ import type { AdminCloudtentaclesVirtualNumberInput, } from "../../../../types/admin/write-inputs.js"; -type JsonObject = Record; - type LocalCloudtentaclesTask = { taskId: number; taskNo: string; diff --git a/apps/backend/src/services/admin/platform-config/cloudtentacles/mappers.ts b/apps/backend/src/services/admin/platform-config/cloudtentacles/mappers.ts index 0c852ad4..e0d59080 100644 --- a/apps/backend/src/services/admin/platform-config/cloudtentacles/mappers.ts +++ b/apps/backend/src/services/admin/platform-config/cloudtentacles/mappers.ts @@ -1,3 +1,4 @@ +import type { JsonObject } from '../../../../types/json.js' import { maskPhone as maskPhoneValue, maskSecret as maskSecretValue, @@ -7,8 +8,6 @@ import { normalizeCloudtentaclesDeviceType, } from "../../../platforms/cloudtentacles/defaults.js"; -type JsonObject = Record - export function maskSecret(value: unknown) { return maskSecretValue(value); } diff --git a/apps/backend/src/services/admin/platform-config/cloudtentacles/payloads.ts b/apps/backend/src/services/admin/platform-config/cloudtentacles/payloads.ts index 56b6bfa3..9e1ee3a3 100644 --- a/apps/backend/src/services/admin/platform-config/cloudtentacles/payloads.ts +++ b/apps/backend/src/services/admin/platform-config/cloudtentacles/payloads.ts @@ -1,5 +1,6 @@ import { getCloudtentaclesSourceByKey } from "../../../platforms/cloudtentacles/source-config-service.js"; import { getCloudtentaclesSessionStateByKey } from "../../../platforms/cloudtentacles/session-state-service.js"; +import type { JsonObject } from '../../../../types/json.js' import { normalizeCloudtentaclesDeviceId, normalizeCloudtentaclesDeviceType, @@ -12,8 +13,6 @@ import { maskPhone, maskSecret } from "./mappers.js"; const DEFAULT_CLOUDTENTACLES_BASE_URL = "https://123.207.217.176"; -type JsonObject = Record - function _resolveSourceKey(payload: JsonObject = {}) { return String(payload.sourceKey || "").trim() || "default"; } diff --git a/apps/backend/src/services/admin/platform-config/cloudtentacles/writes.ts b/apps/backend/src/services/admin/platform-config/cloudtentacles/writes.ts index d72de37a..8ed13610 100644 --- a/apps/backend/src/services/admin/platform-config/cloudtentacles/writes.ts +++ b/apps/backend/src/services/admin/platform-config/cloudtentacles/writes.ts @@ -1,10 +1,9 @@ +import type { JsonObject } from '../../../../types/json.js' import { normalizeCloudtentaclesDeviceId, normalizeCloudtentaclesDeviceType, } from "../../../platforms/cloudtentacles/defaults.js"; -type JsonObject = Record; - export function normalizeAdminCloudtentaclesSourceConfigPayload( payload: JsonObject = {} ) { diff --git a/apps/backend/src/services/admin/platform-config/kuaishou-feifei-service.ts b/apps/backend/src/services/admin/platform-config/kuaishou-feifei-service.ts index e404167f..e7be0aa4 100644 --- a/apps/backend/src/services/admin/platform-config/kuaishou-feifei-service.ts +++ b/apps/backend/src/services/admin/platform-config/kuaishou-feifei-service.ts @@ -1,4 +1,5 @@ import { getKuaishouFeifeiConfig } from '../../platforms/kuaishou-feifei/config.js' +import type { JsonObject } from '../../../types/json.js' import { createKuaishouFeifeiOrder, listKuaishouFeifeiProducts, @@ -16,8 +17,6 @@ import { import { normalizeCloudtentaclesMatchName } from '../../order/cloudtentacles-match-utils.js' import { createHttpError } from '../../../utils/http.js' -type JsonObject = Record - export function getAdminKuaishouFeifeiConfig() { const source = getAdminEditableKuaishouFeifeiConfig() const effective = getKuaishouFeifeiConfig() diff --git a/apps/backend/src/services/admin/platform-config/kuaishou-industry-service.ts b/apps/backend/src/services/admin/platform-config/kuaishou-industry-service.ts index c9f7e4ef..03b7ac5c 100644 --- a/apps/backend/src/services/admin/platform-config/kuaishou-industry-service.ts +++ b/apps/backend/src/services/admin/platform-config/kuaishou-industry-service.ts @@ -1,5 +1,6 @@ import { maskSecret } from '../../../utils/masking.js' import { createHttpError } from '../../../utils/http.js' +import type { JsonObject } from '../../../types/json.js' import { findKuaishouIndustryShopConfig, getKuaishouIndustrySourceConfig, @@ -13,8 +14,6 @@ import { refreshKuaishouIndustryAccessToken, } from '../../platforms/kuaishou-industry/token-service.js' -type JsonObject = Record - const SECRET_FIELDS = [ 'appSecret', 'signSecret', diff --git a/apps/backend/src/services/admin/platform-config/ninetyone-service.ts b/apps/backend/src/services/admin/platform-config/ninetyone-service.ts index 461c1960..eefa2bac 100644 --- a/apps/backend/src/services/admin/platform-config/ninetyone-service.ts +++ b/apps/backend/src/services/admin/platform-config/ninetyone-service.ts @@ -1,11 +1,10 @@ +import type { JsonObject } from '../../../types/json.js' import { failOpen91Order, listOpen91Orders, retryOpen91Order, } from '../../platforms/ninetyone/order-service.js' -type JsonObject = Record - export async function getAdminNinetyoneOrders(query: JsonObject = {}) { return listOpen91Orders({ page: Number(query.page || 1) || 1, diff --git a/apps/backend/src/services/admin/platform-config/notification-service.ts b/apps/backend/src/services/admin/platform-config/notification-service.ts index 472360e4..cf4d0e0b 100644 --- a/apps/backend/src/services/admin/platform-config/notification-service.ts +++ b/apps/backend/src/services/admin/platform-config/notification-service.ts @@ -1,3 +1,4 @@ +import type { JsonObject } from '../../../types/json.js' import { getNotificationConfig, getNotificationConfigFilePath, @@ -24,8 +25,6 @@ import type { AdminScheduledJobsConfigInput, } from '../../../types/admin/write-inputs.js' -type JsonObject = Record - export function getAdminNotificationConfig() { const config = getNotificationConfig() diff --git a/apps/backend/src/services/admin/write/kuaishou-cloud-actions.ts b/apps/backend/src/services/admin/write/kuaishou-cloud-actions.ts index 8d9277e1..042a6c80 100644 --- a/apps/backend/src/services/admin/write/kuaishou-cloud-actions.ts +++ b/apps/backend/src/services/admin/write/kuaishou-cloud-actions.ts @@ -1,6 +1,7 @@ import { getOrderById } from '../../../repositories/order-repo.js' import { listTasksByOrderId, updateTask } from '../../../repositories/task-repo.js' import { createTaskEvent } from '../../../repositories/task-event-repo.js' +import type { JsonObject } from '../../../types/json.js' import { findKuaishouIndustryVoucherByCode, listKuaishouIndustryVouchersByOid, @@ -670,9 +671,9 @@ async function getRequiredIndustryVoucherForTask(task: TaskRow): Promise { +function normalizeAdminTaskIndustryVoucherContext(value: unknown): JsonObject { return value && typeof value === 'object' && !Array.isArray(value) - ? value as Record + ? value as JsonObject : {} } diff --git a/apps/backend/src/services/admin/write/kuaishou-cloud-helpers.ts b/apps/backend/src/services/admin/write/kuaishou-cloud-helpers.ts index 3358e966..f169d43c 100644 --- a/apps/backend/src/services/admin/write/kuaishou-cloud-helpers.ts +++ b/apps/backend/src/services/admin/write/kuaishou-cloud-helpers.ts @@ -1,8 +1,9 @@ import { createHttpError } from '../../../utils/http.js' import type { HttpErrorLike } from '../../../utils/http.js' -import { - resolvePersistedCloudtentaclesContextBySourceKeys, -} from '../../fulfillment/kuaishou-cloud/cloudtentacles-context.js' +import type { JsonObject } from '../../../types/json.js' +import { resolvePersistedCloudtentaclesContextBySourceKeys } from '../../fulfillment/kuaishou-cloud/cloudtentacles-context.js' + +export type { JsonObject } export { KUAISHOU_CLOUD_FIXED_VN_KEY, @@ -10,7 +11,6 @@ export { normalizeKuaishouCloudFlow, normalizeKuaishouCloudRoleInfo, resolveKuaishouCloudBindUrlExpiresAt, - type JsonObject, } from '../../fulfillment/kuaishou-cloud/domain.js' export { prepareKuaishouCloudBindResourceWithFallback, @@ -34,17 +34,17 @@ export type CloudSkuLikeItem = { [key: string]: unknown } -export type KuaishouCloudFlow = Record & { +export type KuaishouCloudFlow = JsonObject & { configId: string internalSkuCode: string internalSkuName: string - ticket: Record - binding: Record - role: Record - purchase: Record - dispatch: Record - returnNumber: Record - consume: Record + ticket: JsonObject + binding: JsonObject + role: JsonObject + purchase: JsonObject + dispatch: JsonObject + returnNumber: JsonObject + consume: JsonObject } export type KuaishouCloudBindingResources = { diff --git a/apps/backend/src/services/bootstrap/fulfillment-bootstrap-service.ts b/apps/backend/src/services/bootstrap/fulfillment-bootstrap-service.ts index e6e4d145..b3b2bb59 100644 --- a/apps/backend/src/services/bootstrap/fulfillment-bootstrap-service.ts +++ b/apps/backend/src/services/bootstrap/fulfillment-bootstrap-service.ts @@ -1,3 +1,4 @@ +import type { JsonObject } from '../../types/json.js' import { listFulfillmentProfileRequirements, replaceFulfillmentProfileRequirements, @@ -6,7 +7,6 @@ import { } from '../../repositories/fulfillment-profile-repo.js' import { nowIso } from '../../utils/time.js' -type JsonObject = Record type CoreProfile = { profileKey: string name: string diff --git a/apps/backend/src/services/claim/claim-identity.ts b/apps/backend/src/services/claim/claim-identity.ts index 5e95c486..fc7dbcfd 100644 --- a/apps/backend/src/services/claim/claim-identity.ts +++ b/apps/backend/src/services/claim/claim-identity.ts @@ -2,6 +2,7 @@ import { createHttpError } from '../../utils/http.js' import { parseTaskContext } from '../../utils/task-json.js' import { isTaskFinalStatus, normalizeTaskStatus, TASK_STATUS } from '../../domain/task-status.js' import type { TaskRow } from '../../types/repository/rows.js' +import type { JsonObject } from '../../types/json.js' export type ClaimIdentity = { expectedUid: string @@ -9,8 +10,6 @@ export type ClaimIdentity = { source: string } -type JsonObject = Record - const CLAIM_UID_MAX_LENGTH = 64 export function normalizeClaimUid(value: unknown): string { @@ -131,10 +130,22 @@ export function assertLewanAutoFulfillmentUidReady( return expectedUid } +export type ClaimIdentityAdminSummary = { + expectedUid: string + submittedAt: string | null + source: string + ready: boolean + boundUid: string + boundRoleName: string + uidMatched: boolean | null + compatibilityMode: 'uid' | 'legacy_no_uid' + note: string +} + export function buildClaimIdentityAdminSummary( context: unknown, options: { flowLike?: unknown; taskRoleId?: unknown; taskRoleName?: unknown } = {}, -) { +): ClaimIdentityAdminSummary { const identity = getClaimIdentityFromContext(context) const boundUid = resolveBoundRoleUid(options.flowLike) || normalizeClaimUid(options.taskRoleId) const flow = isPlainObject(options.flowLike) ? options.flowLike : {} diff --git a/apps/backend/src/services/claim/kuaishou-cloud-claim-context.ts b/apps/backend/src/services/claim/kuaishou-cloud-claim-context.ts index 0df49ca1..43323cee 100644 --- a/apps/backend/src/services/claim/kuaishou-cloud-claim-context.ts +++ b/apps/backend/src/services/claim/kuaishou-cloud-claim-context.ts @@ -11,9 +11,9 @@ import { buildClaimUrl } from './claim-service.js' import { buildClaimIdentityPayload, getClaimIdentityFromContext } from './claim-identity.js' import { resolveKuaishouFeifeiH5UrlWithUid } from '../fulfillment/kuaishou-feifei/index.js' import type { ClaimTokenRow, OrderItemRow, OrderRow, TaskRow } from '../../types/repository/rows.js' +import { asJsonObject, type JsonObject } from '../../types/json.js' export const CLAIM_TERMINAL_STATUSES = new Set([TASK_STATUS.EXPIRED, TASK_STATUS.CLOSED]) -type JsonObject = Record type ClaimContext = { claimToken: ClaimTokenRow task: TaskRow @@ -343,13 +343,13 @@ export function mapClaimKuaishouCloudFulfillment(task: TaskRow, order: OrderRow) return null } - 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 : {} - const ticket = source.ticket && typeof source.ticket === 'object' ? source.ticket : {} - const dispatch = source.dispatch && typeof source.dispatch === 'object' ? source.dispatch : {} - const returnNumber = source.returnNumber && typeof source.returnNumber === 'object' ? source.returnNumber : {} - const consume = source.consume && typeof source.consume === 'object' ? source.consume : {} + const binding = asJsonObject(source.binding) + const role = asJsonObject(source.role) + const purchase = asJsonObject(source.purchase) + const ticket = asJsonObject(source.ticket) + const dispatch = asJsonObject(source.dispatch) + const returnNumber = asJsonObject(source.returnNumber) + const consume = asJsonObject(source.consume) const roleName = String(role.name || binding.roleName || '').trim() const roleId = String(role.rid || binding.roleId || '').trim() const defaultRoleName = String(role.defaultName || role.defaultRoleName || '').trim() diff --git a/apps/backend/src/services/claim/kuaishou-cloud-claim-service.ts b/apps/backend/src/services/claim/kuaishou-cloud-claim-service.ts index d6171a6c..cda61e20 100644 --- a/apps/backend/src/services/claim/kuaishou-cloud-claim-service.ts +++ b/apps/backend/src/services/claim/kuaishou-cloud-claim-service.ts @@ -3,6 +3,7 @@ import { getTaskById, updateTask, updateTaskStatusIfCurrent } from '../../reposi import { createHttpError } from '../../utils/http.js' import { parseTaskContext as parseTaskContextValue } from '../../utils/task-json.js' import { addHours, nowIso } from '../../utils/time.js' +import { asJsonObject, type JsonObject } from '../../types/json.js' import { TASK_STATUS, canRedeemKuaishouCloudClaimStatus, @@ -29,7 +30,6 @@ import { buildClaimDetailPayload, getClaimContext } from './kuaishou-cloud-claim import { syncKuaishouCloudRoleInfo } from './kuaishou-cloud-sync-service.js' import type { TaskRow } from '../../types/repository/rows.js' -type JsonObject = Record type ClaimDetailPayload = ReturnType async function verifyIndustryVoucherTicket( @@ -537,9 +537,7 @@ function isKuaishouCloudMockTask(task: Partial | null | undefined) { function isKuaishouCloudMockContext(context: JsonObject = {}) { const flow = - context.kuaishouCloudFulfillment && typeof context.kuaishouCloudFulfillment === 'object' - ? context.kuaishouCloudFulfillment - : {} + asJsonObject(context.kuaishouCloudFulfillment) const mock = flow.mock && typeof flow.mock === 'object' ? flow.mock : context.mock return Boolean(mock && typeof mock === 'object' && mock.enabled === true) @@ -558,7 +556,7 @@ function buildMockVerifiedKuaishouCloudFlow(value: unknown, timestamp: string) { return { ...flow, mock: { - ...(source.mock && typeof source.mock === 'object' ? source.mock : {}), + ...(asJsonObject(source.mock)), enabled: true, }, binding: { @@ -593,7 +591,7 @@ async function completeMockKuaishouCloudClaimTask(task: TaskRow, timestamp: stri const nextFlow = { ...flow, mock: { - ...(source.mock && typeof source.mock === 'object' ? source.mock : {}), + ...(asJsonObject(source.mock)), enabled: true, }, dispatch: { diff --git a/apps/backend/src/services/claim/kuaishou-cloud-sync-service.ts b/apps/backend/src/services/claim/kuaishou-cloud-sync-service.ts index 3db29112..5b18fbf7 100644 --- a/apps/backend/src/services/claim/kuaishou-cloud-sync-service.ts +++ b/apps/backend/src/services/claim/kuaishou-cloud-sync-service.ts @@ -5,6 +5,7 @@ import { refreshKuaishouCloudTaskRoleInfo, } from '../fulfillment/kuaishou-cloud/index.js' import type { TaskRow } from '../../types/repository/rows.js' +import { parseTaskContext as parseTaskContextValue } from '../../utils/task-json.js' export async function syncKuaishouCloudRoleInfo(task: TaskRow) { const flow = normalizeKuaishouCloudFlow(parseTaskContext(task).kuaishouCloudFulfillment) @@ -45,20 +46,6 @@ export async function syncKuaishouCloudRoleInfo(task: TaskRow) { } } -function parseTaskContext(task: Partial | null | undefined): Record { - const rawValue = task?.context_json - - if (!rawValue) { - return {} - } - - if (typeof rawValue === 'object') { - return rawValue - } - - try { - return JSON.parse(String(rawValue || '{}')) - } catch { - return {} - } +function parseTaskContext(task: Partial | null | undefined) { + return parseTaskContextValue(task) } diff --git a/apps/backend/src/services/fulfillment/kuaishou-cloud/dispatch-role-sync.ts b/apps/backend/src/services/fulfillment/kuaishou-cloud/dispatch-role-sync.ts index 9dc6582f..9fc188d3 100644 --- a/apps/backend/src/services/fulfillment/kuaishou-cloud/dispatch-role-sync.ts +++ b/apps/backend/src/services/fulfillment/kuaishou-cloud/dispatch-role-sync.ts @@ -8,6 +8,7 @@ import { isClaimUidMatched, normalizeClaimUid, } from "../../claim/claim-identity.js"; +import { asJsonObject } from "../../../types/json.js"; import { normalizeKuaishouCloudFlow, normalizeKuaishouCloudRoleInfo, @@ -26,13 +27,9 @@ export async function syncKuaishouCloudRoleInfoBeforeDispatch( String(input.errorCodePrefix || "kuaishou_cloud").trim() || "kuaishou_cloud"; const cloudContext = - input.cloudContext && typeof input.cloudContext === "object" - ? input.cloudContext - : {}; + asJsonObject(input.cloudContext); const taskContext = - input.taskContext && typeof input.taskContext === "object" - ? input.taskContext - : {}; + asJsonObject(input.taskContext); const flow = normalizeKuaishouCloudFlow( input.flow || taskContext.kuaishouCloudFulfillment ); diff --git a/apps/backend/src/services/fulfillment/kuaishou-cloud/domain.ts b/apps/backend/src/services/fulfillment/kuaishou-cloud/domain.ts index 9bbfeffe..2ecb505f 100644 --- a/apps/backend/src/services/fulfillment/kuaishou-cloud/domain.ts +++ b/apps/backend/src/services/fulfillment/kuaishou-cloud/domain.ts @@ -1,10 +1,11 @@ import { resolveCloudtentaclesConfig } from '../../platforms/cloudtentacles/helpers.js' import { maskCode as maskCodeValue, maskPhone as maskPhoneValue } from '../../../utils/masking.js' +import { asJsonObject, type JsonObject } from '../../../types/json.js' + +export type { JsonObject } export const KUAISHOU_CLOUD_FIXED_VN_KEY = '1' -export type JsonObject = Record - const KUAISHOU_CLOUD_EXECUTOR_KEYS = new Set(['kuaishou_ct_assisted', 'kuaishou-industry']) export function isKuaishouCloudTask(task: unknown) { @@ -19,15 +20,15 @@ export function isIndustryEVoucherTask(task: unknown) { export function normalizeKuaishouCloudFlow(value: unknown) { const source: JsonObject = value && typeof value === 'object' ? (value as JsonObject) : {} - 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 : {} - const dispatch = source.dispatch && typeof source.dispatch === 'object' ? source.dispatch : {} + const binding = asJsonObject(source.binding) + const role = asJsonObject(source.role) + const purchase = asJsonObject(source.purchase) + const dispatch = asJsonObject(source.dispatch) const returnNumber = - source.returnNumber && typeof source.returnNumber === 'object' ? source.returnNumber : {} - const consume = source.consume && typeof source.consume === 'object' ? source.consume : {} - const ticket = source.ticket && typeof source.ticket === 'object' ? source.ticket : {} - const rebind = source.rebind && typeof source.rebind === 'object' ? source.rebind : {} + asJsonObject(source.returnNumber) + const consume = asJsonObject(source.consume) + const ticket = asJsonObject(source.ticket) + const rebind = asJsonObject(source.rebind) const deliveryItems = normalizeDeliveryItems(source, binding) const roleName = String(role.name || binding.roleName || '').trim() @@ -180,7 +181,7 @@ function normalizeRoleNameForCompare(value: unknown) { export function normalizeKuaishouCloudDeliveryItems(value: unknown) { const source: JsonObject = value && typeof value === 'object' ? (value as JsonObject) : {} - const binding = source.binding && typeof source.binding === 'object' ? source.binding : {} + const binding = asJsonObject(source.binding) return normalizeDeliveryItems(source, binding) } diff --git a/apps/backend/src/services/fulfillment/kuaishou-cloud/task-context.ts b/apps/backend/src/services/fulfillment/kuaishou-cloud/task-context.ts index bd091073..472a2ea5 100644 --- a/apps/backend/src/services/fulfillment/kuaishou-cloud/task-context.ts +++ b/apps/backend/src/services/fulfillment/kuaishou-cloud/task-context.ts @@ -1,7 +1,6 @@ import { parseTaskContext as parseTaskContextValue } from "../../../utils/task-json.js"; import type { TaskRow } from "../../../types/repository/rows.js"; - -type JsonObject = Record; +import type { JsonObject } from '../../../types/json.js' export function normalizeActor(actor: unknown) { if (!actor || typeof actor !== "object") { diff --git a/apps/backend/src/services/fulfillment/kuaishou-feifei/index.ts b/apps/backend/src/services/fulfillment/kuaishou-feifei/index.ts index 3bb5e4aa..26c61958 100644 --- a/apps/backend/src/services/fulfillment/kuaishou-feifei/index.ts +++ b/apps/backend/src/services/fulfillment/kuaishou-feifei/index.ts @@ -8,6 +8,7 @@ import { parseTaskContext } from '../../../utils/task-json.js' import { nowIso } from '../../../utils/time.js' import { TASK_STATUS } from '../../../domain/task-status.js' import { appendUidToUrl, getClaimIdentityFromContext } from '../../claim/claim-identity.js' +import type { JsonObject } from '../../../types/json.js' import { createKuaishouFeifeiOrder, queryKuaishouFeifeiOrder, @@ -17,8 +18,6 @@ import { buildKuaishouIndustryVoucherContext } from '../../platforms/kuaishou-in import { normalizeShortLinkPayload } from '../../short-links/short-link-service.js' import type { TaskRow } from '../../../types/repository/rows.js' -type JsonObject = Record - export function isKuaishouFeifeiTask(task: Partial | null | undefined) { return String(task?.executor_key || '').trim() === 'kuaishou_feifei' } diff --git a/apps/backend/src/services/notification/config-service.ts b/apps/backend/src/services/notification/config-service.ts index d6e6e216..7a6b9e06 100644 --- a/apps/backend/src/services/notification/config-service.ts +++ b/apps/backend/src/services/notification/config-service.ts @@ -1,4 +1,5 @@ import path from 'node:path' +import type { JsonObject } from '../../types/json.js' import { APP_CONFIG_KEYS } from '../../config/app-config-keys.js' import { PROJECT_ROOT } from '../../config/runtime.js' @@ -10,7 +11,6 @@ import { const NOTIFICATION_CONFIG_FILE_PATH = path.join(PROJECT_ROOT, 'data', 'notification-config.json') const DEFAULT_BARK_SERVER_URL = 'https://api.day.app' -type JsonObject = Record type NotificationRecipient = { id: string name: string diff --git a/apps/backend/src/services/notification/notification-service.ts b/apps/backend/src/services/notification/notification-service.ts index c4e667cd..32df0d96 100644 --- a/apps/backend/src/services/notification/notification-service.ts +++ b/apps/backend/src/services/notification/notification-service.ts @@ -1,4 +1,5 @@ import { logWarn } from '../../utils/logger.js' +import type { JsonObject } from '../../types/json.js' import { getNotificationConfig, listEnabledBarkRecipients, @@ -7,7 +8,6 @@ import { import { sendBarkNotification } from './bark-service.js' import { sendWpushNotification } from './wpush-service.js' -type JsonObject = Record type NotificationResult = ReturnType type NotificationInput = { title?: string diff --git a/apps/backend/src/services/open-91/crypto.ts b/apps/backend/src/services/open-91/crypto.ts index 6b4be93d..314cbc30 100644 --- a/apps/backend/src/services/open-91/crypto.ts +++ b/apps/backend/src/services/open-91/crypto.ts @@ -1,11 +1,10 @@ import crypto from 'node:crypto' +import type { JsonObject } from '../../types/json.js' import { createHttpError } from '../../utils/http.js' import { assertOpen91Config } from './config.js' import { normalizeOpen91String } from './payload.js' -type JsonObject = Record - export function buildOpen91SignSource(params: JsonObject = {}, { secret } = assertOpen91Config()) { const entries = Object.entries(params) .filter(([key]) => key !== 'sign') diff --git a/apps/backend/src/services/open-91/order-query-service.ts b/apps/backend/src/services/open-91/order-query-service.ts index 3fee453b..5f6df514 100644 --- a/apps/backend/src/services/open-91/order-query-service.ts +++ b/apps/backend/src/services/open-91/order-query-service.ts @@ -3,6 +3,7 @@ import { listKuaishouIndustryVouchersByOid } from '../../repositories/kuaishou-i import { listTasksByOrderId } from '../../repositories/task-repo.js' import { resolveTaskDeliveryLink } from '../fulfillment/delivery-link-service.js' import { logIntegration } from '../../utils/logger.js' +import { asJsonObject, type JsonObject } from '../../types/json.js' import { OPEN_91_MANUAL_FAILED_STATUS, OPEN_91_PENDING_CONFIG_STATUS, @@ -32,8 +33,6 @@ import { } from './response.js' import type { KuaishouIndustryVoucherRow, OrderRow, TaskRow } from '../../types/repository/rows.js' -type JsonObject = Record - export async function queryOpen91Order(payload: JsonObject = {}, { requestId = '' } = {}) { const config = assertOpen91Config() const normalized = normalizeOpen91QueryPayload(payload) @@ -245,7 +244,7 @@ function parseJsonObject(value: unknown): JsonObject { try { const parsed = JSON.parse(String(value || '{}')) - return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : {} + return asJsonObject(parsed) } catch { return {} } diff --git a/apps/backend/src/services/open-91/payload.ts b/apps/backend/src/services/open-91/payload.ts index 80edbf6e..3a064fd9 100644 --- a/apps/backend/src/services/open-91/payload.ts +++ b/apps/backend/src/services/open-91/payload.ts @@ -1,7 +1,6 @@ import { createHttpError } from '../../utils/http.js' import { assertOpen91Config } from './config.js' - -type JsonObject = Record +import type { JsonObject } from '../../types/json.js' export function normalizeOpen91String(value: unknown) { return String(value || '').trim() diff --git a/apps/backend/src/services/open-91/response.ts b/apps/backend/src/services/open-91/response.ts index 3d9f5ece..10ebe62c 100644 --- a/apps/backend/src/services/open-91/response.ts +++ b/apps/backend/src/services/open-91/response.ts @@ -1,3 +1,4 @@ +import type { JsonObject } from '../../types/json.js' import { OPEN_91_DEFAULT_FAIL_CODE, OPEN_91_SUCCESS_MESSAGE, @@ -5,8 +6,6 @@ import { import { isOpen91FailedTaskStatus } from '../../domain/task-status.js' import { normalizeOpen91String } from './payload.js' -type JsonObject = Record - export function buildOpen91SuccessResponse(data: unknown, message = OPEN_91_SUCCESS_MESSAGE) { return { code: 200, diff --git a/apps/backend/src/services/order/cloudtentacles-name-match-service.ts b/apps/backend/src/services/order/cloudtentacles-name-match-service.ts index ba57a025..7ce13bac 100644 --- a/apps/backend/src/services/order/cloudtentacles-name-match-service.ts +++ b/apps/backend/src/services/order/cloudtentacles-name-match-service.ts @@ -1,3 +1,4 @@ +import type { JsonObject } from '../../types/json.js' import { listCloudtentaclesSources, } from '../platforms/cloudtentacles/source-config-service.js' @@ -17,8 +18,6 @@ import { } from './cloudtentacles-override-rule-service.js' import { normalizeCloudtentaclesMatchName } from './cloudtentacles-match-utils.js' -type JsonObject = Record - type CloudtentaclesSource = { key?: string label?: string diff --git a/apps/backend/src/services/order/cloudtentacles-override-rule-service.ts b/apps/backend/src/services/order/cloudtentacles-override-rule-service.ts index d90fd754..725589e7 100644 --- a/apps/backend/src/services/order/cloudtentacles-override-rule-service.ts +++ b/apps/backend/src/services/order/cloudtentacles-override-rule-service.ts @@ -1,4 +1,5 @@ import path from 'node:path' +import type { JsonObject } from '../../types/json.js' import { APP_CONFIG_KEYS } from '../../config/app-config-keys.js' import { PROJECT_ROOT } from '../../config/runtime.js' @@ -14,8 +15,6 @@ const CLOUDTENTACLES_OVERRIDE_RULE_FILE_PATH = path.join( 'cloudtentacles-override-rules.json', ) -type JsonObject = Record - export type CloudtentaclesOverrideDeliveryItem = { cloudSkuId: number cloudSkuName: string diff --git a/apps/backend/src/services/order/delivery-task-service.test.ts b/apps/backend/src/services/order/delivery-task-service.test.ts index a45dd5c0..e30b0a8f 100644 --- a/apps/backend/src/services/order/delivery-task-service.test.ts +++ b/apps/backend/src/services/order/delivery-task-service.test.ts @@ -2,22 +2,111 @@ import assert from 'node:assert/strict' import test from 'node:test' import { syncDeliveryTasksForOrderWithDeps } from './delivery-task-service.js' +import type { OrderItemRow, OrderRow, TaskRow } from '../../types/repository/rows.js' +import type { JsonObject } from '../../types/json.js' + +type CreatedTaskInput = { + orderItemId?: number + contextJson?: string + taskStatus?: string + executorKey?: string + [key: string]: unknown +} + +function stubOrder(patch: Partial & Pick): OrderRow { + return { + id: patch.id, + order_id: patch.id, + provider: patch.provider || 'open_91', + platform: patch.platform || 'kuaishou', + shop_id: patch.shop_id || 'shop-1', + shop_name: patch.shop_name || '测试店铺', + platform_order_id: patch.platform_order_id, + order_status: patch.order_status || 'created', + pay_status: patch.pay_status || 'pending_payment', + buyer_id: '', + buyer_name: '', + receiver_contact: '', + total_amount: 0, + currency: 'CNY', + raw_payload_json: '{}', + paid_at: null, + created_at: '2026-05-29T00:00:00.000Z', + updated_at: '2026-05-29T00:00:00.000Z', + ...patch, + } as OrderRow +} + +function stubOrderItem( + patch: Partial & Pick, +): OrderItemRow { + return { + id: patch.id, + order_id: patch.order_id, + sku_code: patch.sku_code, + sku_name: patch.sku_name || patch.sku_code, + quantity: patch.quantity || 1, + spec_json: '{}', + item_snapshot_json: patch.item_snapshot_json || {}, + created_at: '2026-05-29T00:00:00.000Z', + updated_at: '2026-05-29T00:00:00.000Z', + ...patch, + } as OrderItemRow +} + +function stubCreatedTask(id: number, input: CreatedTaskInput): TaskRow { + return { + id, + order_id: 0, + order_item_id: Number(input.orderItemId || 0), + unit_index: 1, + platform_order_id: '', + profile_id: 0, + task_no: 'DT-test', + executor_key: String(input.executorKey || ''), + task_status: String(input.taskStatus || 'pending'), + delivery_status: 'pending', + result_code: '', + result_message: '', + automation_mode: 'manual', + requires_claim: true, + user_action_status: 'pending_claim', + attempt_count: 0, + runtime_session_id: '', + login_type: '', + nickname: '', + role_name: '', + role_id: '', + area: '', + partition_name: '', + claim_token: '', + primary_claim_token: '', + primary_claim_token_id: null, + primary_claim_token_status: '', + artifacts_json: '{}', + context_json: String(input.contextJson || '{}'), + screenshot_path: '', + last_error: '', + retry_count: 0, + created_at: '2026-05-29T00:00:00.000Z', + updated_at: '2026-05-29T00:00:00.000Z', + claimed_at: null, + role_confirmed_at: null, + redeemed_at: null, + } as TaskRow +} test('syncDeliveryTasksForOrder 多账号候选时不提前固定 cloudtentacles 账号', async () => { - const createdInputs: any[] = [] + const createdInputs: CreatedTaskInput[] = [] await syncDeliveryTasksForOrderWithDeps( - { + stubOrder({ id: 1, pay_status: 'pending_payment', - provider: 'open_91', - platform: 'kuaishou', - shop_id: 'shop-1', - shop_name: '测试店铺', platform_order_id: '2614900602069169', - } as any, + }), [ - { + stubOrderItem({ id: 10, order_id: 1, sku_code: 'SKU-1', @@ -37,9 +126,9 @@ test('syncDeliveryTasksForOrder 多账号候选时不提前固定 cloudtentacles }, ], }, - }, - }, - ] as any, + } as JsonObject, + }), + ], { listTasksByOrderId: async () => [], getFulfillmentProfileByKey: async () => ({ @@ -48,44 +137,33 @@ test('syncDeliveryTasksForOrder 多账号候选时不提前固定 cloudtentacles profile_name: 'kuaishou-lewan 履约', executor_key: 'kuaishou_ct_assisted', }), - createTask: async (input: any) => { - createdInputs.push(input) - return { - id: 100, - order_item_id: input.orderItemId, - context_json: input.contextJson, - task_status: input.taskStatus, - executor_key: input.executorKey, - } as any + createTask: async (input) => { + createdInputs.push(input as CreatedTaskInput) + return stubCreatedTask(100, input as CreatedTaskInput) }, nowIso: () => '2026-05-29T09:05:46.658Z', randomId: () => 'DT-test', }, ) - const context = JSON.parse(createdInputs[0].contextJson) - assert.deepEqual(context.kuaishouCloudFulfillment.binding.cloudSourceKeys, [ - 'account-a', - 'account-b', - ]) - assert.equal(context.kuaishouCloudFulfillment.binding.resolvedSourceKey, '') + const context = JSON.parse(String(createdInputs[0]?.contextJson || '{}')) as JsonObject + const fulfillment = context.kuaishouCloudFulfillment as JsonObject + const binding = fulfillment.binding as JsonObject + assert.deepEqual(binding.cloudSourceKeys, ['account-a', 'account-b']) + assert.equal(binding.resolvedSourceKey, '') }) test('syncDeliveryTasksForOrder 单账号候选时保留固定 cloudtentacles 账号', async () => { - const createdInputs: any[] = [] + const createdInputs: CreatedTaskInput[] = [] await syncDeliveryTasksForOrderWithDeps( - { + stubOrder({ id: 2, pay_status: 'pending_payment', - provider: 'open_91', - platform: 'kuaishou', - shop_id: 'shop-1', - shop_name: '测试店铺', platform_order_id: '2614900602069170', - } as any, + }), [ - { + stubOrderItem({ id: 11, order_id: 2, sku_code: 'SKU-1', @@ -97,9 +175,9 @@ test('syncDeliveryTasksForOrder 单账号候选时保留固定 cloudtentacles cloudSkuId: 74, cloudSkuName: '荣耀勋章礼包(30个)', }, - }, - }, - ] as any, + } as JsonObject, + }), + ], { listTasksByOrderId: async () => [], getFulfillmentProfileByKey: async () => ({ @@ -108,22 +186,18 @@ test('syncDeliveryTasksForOrder 单账号候选时保留固定 cloudtentacles profile_name: 'kuaishou-lewan 履约', executor_key: 'kuaishou_ct_assisted', }), - createTask: async (input: any) => { - createdInputs.push(input) - return { - id: 101, - order_item_id: input.orderItemId, - context_json: input.contextJson, - task_status: input.taskStatus, - executor_key: input.executorKey, - } as any + createTask: async (input) => { + createdInputs.push(input as CreatedTaskInput) + return stubCreatedTask(101, input as CreatedTaskInput) }, nowIso: () => '2026-05-29T09:05:46.658Z', randomId: () => 'DT-test', }, ) - const context = JSON.parse(createdInputs[0].contextJson) - assert.deepEqual(context.kuaishouCloudFulfillment.binding.cloudSourceKeys, ['account-a']) - assert.equal(context.kuaishouCloudFulfillment.binding.resolvedSourceKey, 'account-a') + const context = JSON.parse(String(createdInputs[0]?.contextJson || '{}')) as JsonObject + const fulfillment = context.kuaishouCloudFulfillment as JsonObject + const binding = fulfillment.binding as JsonObject + assert.deepEqual(binding.cloudSourceKeys, ['account-a']) + assert.equal(binding.resolvedSourceKey, 'account-a') }) diff --git a/apps/backend/src/services/platforms/cloudtentacles/catalog-service.ts b/apps/backend/src/services/platforms/cloudtentacles/catalog-service.ts index 9b86e2d0..c51433bc 100644 --- a/apps/backend/src/services/platforms/cloudtentacles/catalog-service.ts +++ b/apps/backend/src/services/platforms/cloudtentacles/catalog-service.ts @@ -1,8 +1,7 @@ import { createHttpError, type HttpErrorLike } from '../../../utils/http.js' import { cloudtentaclesRequest } from './http-client.js' import { resolveCloudtentaclesConfig } from './helpers.js' - -type JsonObject = Record +import type { JsonObject } from '../../../types/json.js' export async function getCloudtentaclesAsset(payload: JsonObject = {}) { const token = requireToken(payload.token, 'cloudtentacles 余额查询缺少 token', 'cloudtentacles_asset_missing_token') diff --git a/apps/backend/src/services/platforms/cloudtentacles/crypto-service.ts b/apps/backend/src/services/platforms/cloudtentacles/crypto-service.ts index 22148d77..7e6f2ba8 100644 --- a/apps/backend/src/services/platforms/cloudtentacles/crypto-service.ts +++ b/apps/backend/src/services/platforms/cloudtentacles/crypto-service.ts @@ -1,10 +1,9 @@ import { createHash, constants, publicEncrypt } from 'node:crypto' +import type { JsonObject } from '../../../types/json.js' import { createHttpError } from '../../../utils/http.js' import { resolveCloudtentaclesConfig } from './helpers.js' -type JsonObject = Record - export function md5CloudtentaclesPassword(password: unknown) { return createHash('md5').update(String(password || ''), 'utf8').digest('hex') } diff --git a/apps/backend/src/services/platforms/cloudtentacles/debug-flow-service.ts b/apps/backend/src/services/platforms/cloudtentacles/debug-flow-service.ts index 4f933f65..eb257c19 100644 --- a/apps/backend/src/services/platforms/cloudtentacles/debug-flow-service.ts +++ b/apps/backend/src/services/platforms/cloudtentacles/debug-flow-service.ts @@ -1,6 +1,7 @@ import { createHttpError } from '../../../utils/http.js' import { getCloudtentaclesAsset, listCloudtentaclesSku, buyCloudtentaclesSku } from './catalog-service.js' import { getCloudtentaclesKnapsack } from './knapsack-service.js' +import type { JsonObject } from '../../../types/json.js' import { appointCloudtentaclesVirtualNumber, fetchCloudtentaclesVirtualNumberCode, @@ -9,7 +10,6 @@ import { verifyCloudtentaclesLoginCode, } from './virtual-number-service.js' -type JsonObject = Record type FlowStepOptions = { retries?: number retryDelayMs?: number diff --git a/apps/backend/src/services/platforms/cloudtentacles/helpers.ts b/apps/backend/src/services/platforms/cloudtentacles/helpers.ts index a97ffcc9..df4e97d7 100644 --- a/apps/backend/src/services/platforms/cloudtentacles/helpers.ts +++ b/apps/backend/src/services/platforms/cloudtentacles/helpers.ts @@ -1,4 +1,5 @@ import { runtimeConfig } from '../../../config/runtime.js' +import type { JsonObject } from '../../../types/json.js' import type { RuntimeConfig } from '../../../types/runtime-config.js' import { @@ -9,8 +10,6 @@ import { } from './defaults.js' type CloudtentaclesRuntimeConfig = RuntimeConfig['platforms']['cloudtentacles'] -type JsonObject = Record - export function resolveCloudtentaclesConfig(overrides: Partial = {}) { const baseConfig = runtimeConfig.platforms?.cloudtentacles || { baseUrl: '', diff --git a/apps/backend/src/services/platforms/cloudtentacles/http-client.ts b/apps/backend/src/services/platforms/cloudtentacles/http-client.ts index f2aee1ce..8e1bc810 100644 --- a/apps/backend/src/services/platforms/cloudtentacles/http-client.ts +++ b/apps/backend/src/services/platforms/cloudtentacles/http-client.ts @@ -1,12 +1,12 @@ import http from 'node:http' import https from 'node:https' +import type { JsonObject } from '../../../types/json.js' import { createHttpError } from '../../../utils/http.js' import { logInfo, logWarn } from '../../../utils/logger.js' import { notifyCloudtentaclesAuthExpired } from '../../notification/domain-notifications.js' import { buildCloudtentaclesHeaders, buildCloudtentaclesUrl, resolveCloudtentaclesConfig } from './helpers.js' -type JsonObject = Record type HeaderAdapter = { get(name: unknown): string | null } diff --git a/apps/backend/src/services/platforms/cloudtentacles/knapsack-service.ts b/apps/backend/src/services/platforms/cloudtentacles/knapsack-service.ts index b36232c5..889cc770 100644 --- a/apps/backend/src/services/platforms/cloudtentacles/knapsack-service.ts +++ b/apps/backend/src/services/platforms/cloudtentacles/knapsack-service.ts @@ -1,8 +1,7 @@ import { createHttpError } from '../../../utils/http.js' import { cloudtentaclesRequest } from './http-client.js' import { resolveCloudtentaclesConfig } from './helpers.js' - -type JsonObject = Record +import type { JsonObject } from '../../../types/json.js' export async function getCloudtentaclesKnapsack(payload: JsonObject = {}) { const token = String(payload.token || '').trim() diff --git a/apps/backend/src/services/platforms/cloudtentacles/record-service.ts b/apps/backend/src/services/platforms/cloudtentacles/record-service.ts index e0b56429..ccc6f3fe 100644 --- a/apps/backend/src/services/platforms/cloudtentacles/record-service.ts +++ b/apps/backend/src/services/platforms/cloudtentacles/record-service.ts @@ -2,8 +2,7 @@ import { createHttpError } from '../../../utils/http.js' import { logInfo } from '../../../utils/logger.js' import { cloudtentaclesRequest } from './http-client.js' import { resolveCloudtentaclesConfig } from './helpers.js' - -type JsonObject = Record +import type { JsonObject } from '../../../types/json.js' export type CloudtentaclesDeliveryRecordQuery = JsonObject & { baseUrl?: string diff --git a/apps/backend/src/services/platforms/cloudtentacles/session-service.ts b/apps/backend/src/services/platforms/cloudtentacles/session-service.ts index 32742700..5eb76807 100644 --- a/apps/backend/src/services/platforms/cloudtentacles/session-service.ts +++ b/apps/backend/src/services/platforms/cloudtentacles/session-service.ts @@ -3,8 +3,7 @@ import { logInfo } from '../../../utils/logger.js' import { encryptCloudtentaclesPayload, md5CloudtentaclesPassword } from './crypto-service.js' import { cloudtentaclesRequest } from './http-client.js' import { resolveCloudtentaclesConfig } from './helpers.js' - -type JsonObject = Record +import type { JsonObject } from '../../../types/json.js' export async function sendCloudtentaclesSmsCode(payload: JsonObject = {}) { const username = String(payload.username || '').trim() diff --git a/apps/backend/src/services/platforms/cloudtentacles/session-state-service.ts b/apps/backend/src/services/platforms/cloudtentacles/session-state-service.ts index 496b30aa..1b724218 100644 --- a/apps/backend/src/services/platforms/cloudtentacles/session-state-service.ts +++ b/apps/backend/src/services/platforms/cloudtentacles/session-state-service.ts @@ -1,4 +1,5 @@ import path from 'node:path' +import type { JsonObject } from '../../../types/json.js' import { APP_CONFIG_KEYS } from '../../../config/app-config-keys.js' import { PROJECT_ROOT } from '../../../config/runtime.js' @@ -16,7 +17,6 @@ import { const CLOUDTENTACLES_SESSION_FILE_PATH = path.join(PROJECT_ROOT, 'data', 'cloudtentacles-session.json') -type JsonObject = Record type CloudtentaclesSessionState = ReturnType type CloudtentaclesSessionStatesFile = { sessions: Record diff --git a/apps/backend/src/services/platforms/cloudtentacles/source-config-service.ts b/apps/backend/src/services/platforms/cloudtentacles/source-config-service.ts index f2ee4f3b..4fe79f50 100644 --- a/apps/backend/src/services/platforms/cloudtentacles/source-config-service.ts +++ b/apps/backend/src/services/platforms/cloudtentacles/source-config-service.ts @@ -1,4 +1,5 @@ import path from 'node:path' +import type { JsonObject } from '../../../types/json.js' import { APP_CONFIG_KEYS } from '../../../config/app-config-keys.js' import { PROJECT_ROOT } from '../../../config/runtime.js' @@ -14,8 +15,6 @@ import { const CLOUDTENTACLES_SOURCES_FILE_PATH = path.join(PROJECT_ROOT, 'data', 'cloudtentacles-sources.json') -type JsonObject = Record - export function getCloudtentaclesSourcesFilePath() { return CLOUDTENTACLES_SOURCES_FILE_PATH } diff --git a/apps/backend/src/services/platforms/cloudtentacles/virtual-number-service.ts b/apps/backend/src/services/platforms/cloudtentacles/virtual-number-service.ts index d6f445be..c44e6d8a 100644 --- a/apps/backend/src/services/platforms/cloudtentacles/virtual-number-service.ts +++ b/apps/backend/src/services/platforms/cloudtentacles/virtual-number-service.ts @@ -1,5 +1,6 @@ import http from 'node:http' import https from 'node:https' +import type { JsonObject } from '../../../types/json.js' import { createHttpError } from '../../../utils/http.js' import { cloudtentaclesRequest } from './http-client.js' @@ -8,7 +9,6 @@ import { resolveCloudtentaclesConfig } from './helpers.js' const BIND_URL_PROBE_MAX_BODY_BYTES = 64 * 1024 const AMS_SIGNATURE_EXPIRED_CODE = '99998' -type JsonObject = Record type HeaderAdapter = { get(name: unknown): string | null } diff --git a/apps/backend/src/services/platforms/kuaishou-feifei/http-client.ts b/apps/backend/src/services/platforms/kuaishou-feifei/http-client.ts index 9ba0741f..23fbcd29 100644 --- a/apps/backend/src/services/platforms/kuaishou-feifei/http-client.ts +++ b/apps/backend/src/services/platforms/kuaishou-feifei/http-client.ts @@ -1,11 +1,10 @@ import crypto from 'node:crypto' +import { asJsonObject, type JsonObject } from '../../../types/json.js' import { createHttpError } from '../../../utils/http.js' import { createRequestId, logExternalHttpPacket } from '../../../utils/logger.js' import { assertKuaishouFeifeiConfig } from './config.js' -type JsonObject = Record - export async function kuaishouFeifeiRequest(pathname: string, payload: JsonObject = {}) { const config = assertKuaishouFeifeiConfig() const body = JSON.stringify(payload) @@ -189,7 +188,7 @@ export function isKuaishouFeifeiSuccessResponse(json: JsonObject) { function parseJsonObject(text: string): JsonObject { try { const parsed = JSON.parse(text || '{}') - return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : {} + return asJsonObject(parsed) } catch { return {} } diff --git a/apps/backend/src/services/platforms/kuaishou-feifei/notify-service.ts b/apps/backend/src/services/platforms/kuaishou-feifei/notify-service.ts index 5d4035e1..6af16b26 100644 --- a/apps/backend/src/services/platforms/kuaishou-feifei/notify-service.ts +++ b/apps/backend/src/services/platforms/kuaishou-feifei/notify-service.ts @@ -1,4 +1,5 @@ import crypto from 'node:crypto' +import type { JsonObject } from '../../../types/json.js' import { createTaskEvent } from '../../../repositories/task-event-repo.js' import { findKuaishouFeifeiTaskByOrder } from '../../../repositories/task-repo.js' @@ -9,7 +10,6 @@ import { assertKuaishouFeifeiConfig } from './config.js' import { signKuaishouFeifeiPayload } from './http-client.js' import { mapKuaishouFeifeiOrder } from './order-service.js' -type JsonObject = Record type NotifyHeaders = Record export async function handleKuaishouFeifeiNotify(input: { diff --git a/apps/backend/src/services/platforms/kuaishou-feifei/order-service.ts b/apps/backend/src/services/platforms/kuaishou-feifei/order-service.ts index 20561eff..afc93cf7 100644 --- a/apps/backend/src/services/platforms/kuaishou-feifei/order-service.ts +++ b/apps/backend/src/services/platforms/kuaishou-feifei/order-service.ts @@ -1,7 +1,6 @@ import { getKuaishouFeifeiConfig } from './config.js' import { kuaishouFeifeiRequest } from './http-client.js' - -type JsonObject = Record +import type { JsonObject } from '../../../types/json.js' export type KuaishouFeifeiProductListResult = ReturnType diff --git a/apps/backend/src/services/platforms/kuaishou-feifei/source-config-service.ts b/apps/backend/src/services/platforms/kuaishou-feifei/source-config-service.ts index 63f05e73..35b9231b 100644 --- a/apps/backend/src/services/platforms/kuaishou-feifei/source-config-service.ts +++ b/apps/backend/src/services/platforms/kuaishou-feifei/source-config-service.ts @@ -1,4 +1,5 @@ import path from 'node:path' +import type { JsonObject } from '../../../types/json.js' import { APP_CONFIG_KEYS } from '../../../config/app-config-keys.js' import { PROJECT_ROOT } from '../../../config/runtime.js' @@ -16,8 +17,6 @@ const KUAISHOU_FEIFEI_CONFIG_FILE_PATH = path.join( 'kuaishou-feifei-config.json', ) -type JsonObject = Record - export type KuaishouFeifeiSourceConfig = { enabled: boolean baseUrl: string diff --git a/apps/backend/src/services/platforms/kuaishou-industry/consume-code-service.ts b/apps/backend/src/services/platforms/kuaishou-industry/consume-code-service.ts index bf3b0c9b..b7c4c527 100644 --- a/apps/backend/src/services/platforms/kuaishou-industry/consume-code-service.ts +++ b/apps/backend/src/services/platforms/kuaishou-industry/consume-code-service.ts @@ -3,6 +3,7 @@ import { findKuaishouIndustryVoucherByCode } from '../../../repositories/kuaisho import { normalizeTimestampIso } from '../../../utils/time.js' import { logWarn } from '../../../utils/logger.js' import { TASK_STATUS } from '../../../domain/task-status.js' +import type { JsonObject } from '../../../types/json.js' import { getKuaishouIndustryConfig, assertMatchingAppKey, @@ -17,8 +18,6 @@ import { consumeCallback } from './consume-callback-service.js' import { consumeKuaishouIndustryVoucher } from './voucher-service.js' import { attachKuaishouIndustryVoucherToTask } from './voucher-binding-service.js' -type JsonObject = Record - export async function handleConsumeCode(rawBody: JsonObject = {}) { const config = getKuaishouIndustryConfig() diff --git a/apps/backend/src/services/platforms/kuaishou-industry/crypto.ts b/apps/backend/src/services/platforms/kuaishou-industry/crypto.ts index 0b23ee10..4ca8e603 100644 --- a/apps/backend/src/services/platforms/kuaishou-industry/crypto.ts +++ b/apps/backend/src/services/platforms/kuaishou-industry/crypto.ts @@ -1,11 +1,10 @@ import crypto from 'node:crypto' +import type { JsonObject } from '../../../types/json.js' import { createHttpError } from '../../../utils/http.js' import { logWarn } from '../../../utils/logger.js' import { assertKuaishouIndustryConfig } from './config.js' -type JsonObject = Record - export type SignMethod = 'MD5' | 'HMAC_SHA256' export function buildKuaishouIndustrySignSource(params: JsonObject = {}, { signSecret } = assertKuaishouIndustryConfig()) { diff --git a/apps/backend/src/services/platforms/kuaishou-industry/destroy-code-service.ts b/apps/backend/src/services/platforms/kuaishou-industry/destroy-code-service.ts index ffe8719e..20ea7937 100644 --- a/apps/backend/src/services/platforms/kuaishou-industry/destroy-code-service.ts +++ b/apps/backend/src/services/platforms/kuaishou-industry/destroy-code-service.ts @@ -1,4 +1,5 @@ import { updateTask } from '../../../repositories/task-repo.js' +import type { JsonObject } from '../../../types/json.js' import { listKuaishouIndustryVouchersByOid, updateKuaishouIndustryVoucherByCode, @@ -18,8 +19,6 @@ import { import { destroyCallback } from './destroy-callback-service.js' import { attachKuaishouIndustryVoucherToTask } from './voucher-binding-service.js' -type JsonObject = Record - export async function handleDestroyCode(rawBody: JsonObject = {}) { const config = getKuaishouIndustryConfig() diff --git a/apps/backend/src/services/platforms/kuaishou-industry/openapi-client.ts b/apps/backend/src/services/platforms/kuaishou-industry/openapi-client.ts index e360c800..f22281da 100644 --- a/apps/backend/src/services/platforms/kuaishou-industry/openapi-client.ts +++ b/apps/backend/src/services/platforms/kuaishou-industry/openapi-client.ts @@ -2,8 +2,8 @@ import { getKuaishouIndustryConfig } from './config.js' import { signKuaishouIndustryPayload, type SignMethod } from './crypto.js' import { ensureKuaishouIndustryAccessToken } from './token-service.js' import { logIntegration } from '../../../utils/logger.js' - -export type JsonObject = Record +import type { JsonObject } from '../../../types/json.js' +export type { JsonObject } type KuaishouIndustryOpenApiCallInput = { apiMethod: string diff --git a/apps/backend/src/services/platforms/kuaishou-industry/openapi-values.ts b/apps/backend/src/services/platforms/kuaishou-industry/openapi-values.ts index 15b25a29..69913708 100644 --- a/apps/backend/src/services/platforms/kuaishou-industry/openapi-values.ts +++ b/apps/backend/src/services/platforms/kuaishou-industry/openapi-values.ts @@ -1,5 +1,5 @@ -export type JsonObject = Record - +import type { JsonObject } from '../../../types/json.js' +export type { JsonObject } export function pickDefinedBizParams(input: JsonObject): JsonObject { const output: JsonObject = {} diff --git a/apps/backend/src/services/platforms/kuaishou-industry/payload.ts b/apps/backend/src/services/platforms/kuaishou-industry/payload.ts index 05a9cb87..5ec90d2c 100644 --- a/apps/backend/src/services/platforms/kuaishou-industry/payload.ts +++ b/apps/backend/src/services/platforms/kuaishou-industry/payload.ts @@ -1,7 +1,6 @@ import { createHttpError } from '../../../utils/http.js' import { assertKuaishouIndustryConfig } from './config.js' - -type JsonObject = Record +import { asJsonObject, type JsonObject } from '../../../types/json.js' export function normalizeIndustryString(value: unknown) { return String(value || '').trim() @@ -239,7 +238,7 @@ function parseParamField(raw: JsonObject) { try { const parsed = JSON.parse(normalizeIndustryString(raw.param || '{}')) - return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : {} + return asJsonObject(parsed) } catch { return {} } diff --git a/apps/backend/src/services/platforms/kuaishou-industry/query-code-service.ts b/apps/backend/src/services/platforms/kuaishou-industry/query-code-service.ts index f920c2e2..5c1e6227 100644 --- a/apps/backend/src/services/platforms/kuaishou-industry/query-code-service.ts +++ b/apps/backend/src/services/platforms/kuaishou-industry/query-code-service.ts @@ -1,3 +1,4 @@ +import type { JsonObject } from '../../../types/json.js' import { findKuaishouIndustryVoucherByCode, listKuaishouIndustryVouchersByOid, @@ -18,8 +19,6 @@ import { isKuaishouIndustryVoucherSendCallbackSuccess, } from './voucher-service.js' -type JsonObject = Record - export async function handleQueryCode(rawBody: JsonObject = {}) { const config = getKuaishouIndustryConfig() diff --git a/apps/backend/src/services/platforms/kuaishou-industry/response.ts b/apps/backend/src/services/platforms/kuaishou-industry/response.ts index a4be8e94..454b262c 100644 --- a/apps/backend/src/services/platforms/kuaishou-industry/response.ts +++ b/apps/backend/src/services/platforms/kuaishou-industry/response.ts @@ -1,5 +1,4 @@ -type JsonObject = Record - +import type { JsonObject } from '../../../types/json.js' export function buildIndustrySuccessResponse(data: unknown = null) { return { result: 1, diff --git a/apps/backend/src/services/platforms/kuaishou-industry/send-code-service.ts b/apps/backend/src/services/platforms/kuaishou-industry/send-code-service.ts index 33aaa059..f2292c6a 100644 --- a/apps/backend/src/services/platforms/kuaishou-industry/send-code-service.ts +++ b/apps/backend/src/services/platforms/kuaishou-industry/send-code-service.ts @@ -1,5 +1,6 @@ import { findLatestOrderByPlatformOrderId } from '../../../repositories/order-repo.js' import { listTasksByOrderId } from '../../../repositories/task-repo.js' +import { asJsonObject, type JsonObject } from '../../../types/json.js' import { findKuaishouIndustryVoucherByCode, listKuaishouIndustryVouchersByOid, @@ -33,8 +34,6 @@ import { bindKuaishouIndustryVouchersToOrderTasks } from './voucher-binding-serv import { parseAmountToFen } from '../../../utils/money.js' import type { KuaishouIndustryVoucherRow, OrderRow } from '../../../types/repository/rows.js' -type JsonObject = Record - type SendCodeCallbackParams = { oid: string sendType: string @@ -677,7 +676,7 @@ function parseJsonObject(value: unknown): JsonObject { try { const parsed = JSON.parse(String(value || '{}')) - return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : {} + return asJsonObject(parsed) } catch { return {} } diff --git a/apps/backend/src/services/platforms/kuaishou-industry/source-config-service.ts b/apps/backend/src/services/platforms/kuaishou-industry/source-config-service.ts index 863297e3..195c78c3 100644 --- a/apps/backend/src/services/platforms/kuaishou-industry/source-config-service.ts +++ b/apps/backend/src/services/platforms/kuaishou-industry/source-config-service.ts @@ -1,4 +1,5 @@ import path from 'node:path' +import type { JsonObject } from '../../../types/json.js' import { APP_CONFIG_KEYS } from '../../../config/app-config-keys.js' import { PROJECT_ROOT, runtimeConfig } from '../../../config/runtime.js' @@ -14,8 +15,6 @@ const LEGACY_DEFAULT_REDIRECT_URI = 'https://ks.khhao.com/oauth-callback' const DEFAULT_REDIRECT_URI = 'https://ks.khhao.com/admin/platform-shops?tab=kuaishouIndustry' const DEFAULT_SCOPES = 'merchant_item' -type JsonObject = Record - export type KuaishouIndustryShopConfig = { enabled: boolean sellerId: string diff --git a/apps/backend/src/services/platforms/kuaishou-industry/token-service.ts b/apps/backend/src/services/platforms/kuaishou-industry/token-service.ts index 6a6e7cbb..117458bd 100644 --- a/apps/backend/src/services/platforms/kuaishou-industry/token-service.ts +++ b/apps/backend/src/services/platforms/kuaishou-industry/token-service.ts @@ -1,5 +1,6 @@ import { createHttpError } from '../../../utils/http.js' import { logInfo, logWarn } from '../../../utils/logger.js' +import type { JsonObject } from '../../../types/json.js' import { findKuaishouIndustryShopConfig, getKuaishouIndustrySourceConfig, @@ -10,8 +11,6 @@ import { type KuaishouIndustrySourceConfig, } from './source-config-service.js' -type JsonObject = Record - const ACCESS_TOKEN_REFRESH_MARGIN_MS = 30 * 60 * 1000 const DEFAULT_ACCESS_TOKEN_TTL_MS = 47 * 60 * 60 * 1000 const DEFAULT_REFRESH_TOKEN_TTL_MS = 179 * 24 * 60 * 60 * 1000 diff --git a/apps/backend/src/services/platforms/kuaishou-industry/voucher-binding-service.ts b/apps/backend/src/services/platforms/kuaishou-industry/voucher-binding-service.ts index 52b12a6f..77284c39 100644 --- a/apps/backend/src/services/platforms/kuaishou-industry/voucher-binding-service.ts +++ b/apps/backend/src/services/platforms/kuaishou-industry/voucher-binding-service.ts @@ -1,4 +1,5 @@ import { updateTask } from '../../../repositories/task-repo.js' +import type { JsonObject } from '../../../types/json.js' import { listKuaishouIndustryVouchersByOid, updateKuaishouIndustryVoucherByCode, @@ -17,8 +18,6 @@ import type { TaskRow, } from '../../../types/repository/rows.js' -type JsonObject = Record - export async function bindKuaishouIndustryVouchersToOrderTasks( order: OrderRow | null | undefined, tasks: TaskRow[] = [], diff --git a/apps/backend/src/services/platforms/kuaishou-industry/voucher-service.ts b/apps/backend/src/services/platforms/kuaishou-industry/voucher-service.ts index 7f54bc3b..56aecc77 100644 --- a/apps/backend/src/services/platforms/kuaishou-industry/voucher-service.ts +++ b/apps/backend/src/services/platforms/kuaishou-industry/voucher-service.ts @@ -1,4 +1,5 @@ import { createTaskEvent } from '../../../repositories/task-event-repo.js' +import { asJsonObject, type JsonObject } from '../../../types/json.js' import { findKuaishouIndustryVoucherByCode, listKuaishouIndustryVouchersByTaskId, @@ -9,8 +10,6 @@ import { buildIndustryEticketItem } from './response.js' import { consumeCallback } from './consume-callback-service.js' import type { KuaishouIndustryVoucherRow, TaskRow } from '../../../types/repository/rows.js' -type JsonObject = Record - export const KUAISHOU_INDUSTRY_DEFAULT_VALID_DAYS = 3 export const KUAISHOU_INDUSTRY_SEND_CALLBACK_STATUS = { PENDING: 'pending', @@ -358,7 +357,7 @@ function parseJsonObject(value: unknown): JsonObject { try { const parsed = JSON.parse(String(value || '{}')) - return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : {} + return asJsonObject(parsed) } catch { return {} } diff --git a/apps/backend/src/services/platforms/ninetyone/order-service.ts b/apps/backend/src/services/platforms/ninetyone/order-service.ts index d3f4c735..713cc199 100644 --- a/apps/backend/src/services/platforms/ninetyone/order-service.ts +++ b/apps/backend/src/services/platforms/ninetyone/order-service.ts @@ -4,6 +4,7 @@ import { listOrderItemsByOrderId, replaceOrderItems } from '../../../repositorie import { listTasksByOrderId } from '../../../repositories/task-repo.js' import { upsertOrderFromSource } from '../../order/order-service.js' import { bindKuaishouIndustryVouchersToOrderTasks } from '../kuaishou-industry/voucher-binding-service.js' +import { asJsonObject, type JsonObject } from '../../../types/json.js' import { OPEN_91_PLATFORM, OPEN_91_PROVIDER, @@ -18,8 +19,6 @@ import type { OrderItemRow, OrderRow } from '../../../types/repository/rows.js' export const OPEN_91_PENDING_CONFIG_STATUS = 'pending_config' export const OPEN_91_MANUAL_FAILED_STATUS = 'manual_failed' -type JsonObject = Record - export function buildOpen91SourceEvent(payload: JsonObject = {}, config: JsonObject = {}) { const normalized = normalizeOpen91CreatePayload(payload) const productInfo = parseOpen91ProductNo(normalized.productNo) @@ -397,7 +396,7 @@ function parseJsonObject(value: unknown): JsonObject { try { const parsed = JSON.parse(String(value || '{}')) - return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : {} + return asJsonObject(parsed) } catch { return {} } diff --git a/apps/backend/src/services/scheduler/cloudtentacles-health-job.ts b/apps/backend/src/services/scheduler/cloudtentacles-health-job.ts index 05b9b9f0..2d79311d 100644 --- a/apps/backend/src/services/scheduler/cloudtentacles-health-job.ts +++ b/apps/backend/src/services/scheduler/cloudtentacles-health-job.ts @@ -1,4 +1,5 @@ import { getCloudtentaclesAsset } from '../platforms/cloudtentacles/catalog-service.js' +import type { JsonObject } from '../../types/json.js' import { normalizeCloudtentaclesDeviceId, normalizeCloudtentaclesDeviceType, @@ -13,7 +14,6 @@ import { notifyCloudtentaclesAuthExpired, } from '../notification/domain-notifications.js' -type JsonObject = Record type CloudtentaclesHealthAccountResult = { sourceKey: string label: string diff --git a/apps/backend/src/services/scheduler/config-service.ts b/apps/backend/src/services/scheduler/config-service.ts index 47daea99..80149a43 100644 --- a/apps/backend/src/services/scheduler/config-service.ts +++ b/apps/backend/src/services/scheduler/config-service.ts @@ -1,4 +1,5 @@ import path from 'node:path' +import type { JsonObject } from '../../types/json.js' import { APP_CONFIG_KEYS } from '../../config/app-config-keys.js' import { PROJECT_ROOT } from '../../config/runtime.js' @@ -10,8 +11,6 @@ import { const SCHEDULED_JOBS_FILE_PATH = path.join(PROJECT_ROOT, 'data', 'scheduled-jobs.json') const CLOUDTENTACLES_HEALTH_JOB_ID = 'cloudtentacles-health' -type JsonObject = Record - export function getScheduledJobsFilePath() { return SCHEDULED_JOBS_FILE_PATH } diff --git a/apps/backend/src/services/scheduler/scheduler-service.ts b/apps/backend/src/services/scheduler/scheduler-service.ts index bac8939b..11491750 100644 --- a/apps/backend/src/services/scheduler/scheduler-service.ts +++ b/apps/backend/src/services/scheduler/scheduler-service.ts @@ -1,13 +1,12 @@ import { logError, logInfo, logWarn } from '../../utils/logger.js' import { createHttpError } from '../../utils/http.js' +import type { JsonObject } from '../../types/json.js' import { getCloudtentaclesHealthJob, getScheduledJobsConfig, } from './config-service.js' import { runCloudtentaclesHealthJob } from './cloudtentacles-health-job.js' -type JsonObject = Record - const timers = new Map() const jobStates = new Map() diff --git a/apps/backend/src/types/admin/task-detail.ts b/apps/backend/src/types/admin/task-detail.ts new file mode 100644 index 00000000..ebea3a6b --- /dev/null +++ b/apps/backend/src/types/admin/task-detail.ts @@ -0,0 +1,34 @@ +import type { JsonObject } from '../json.js' + +/** + * 后台任务详情 API 返回形状(与前端 AdminTaskDetail 对齐的后端契约)。 + * 深层履约字段仍用 JsonObject,避免一次改完所有 mapper。 + */ +export type AdminClaimIdentitySummary = { + expectedUid: string + submittedAt: string | null + source: string + ready: boolean + boundUid: string + boundRoleName: string + uidMatched: boolean | null + compatibilityMode: 'uid' | 'legacy_no_uid' + note: string +} + +export type AdminTaskDetailView = { + task: JsonObject + order: JsonObject | null + orderItem: JsonObject | null + claimToken: JsonObject | null + artifacts: JsonObject + screenshotUrl: string + review: JsonObject + redeemResolution: JsonObject | null + claimIdentity: AdminClaimIdentitySummary + kuaishouCloudFulfillment: JsonObject | null + kuaishouIndustryVoucher: JsonObject | null + manualDispatch: JsonObject | null + events: JsonObject[] + operations: JsonObject +} diff --git a/apps/backend/src/types/json.ts b/apps/backend/src/types/json.ts index f3e9a9e6..3b37a5d2 100644 --- a/apps/backend/src/types/json.ts +++ b/apps/backend/src/types/json.ts @@ -1 +1,41 @@ -export type JsonRecord = Record +/** + * 项目统一的 JSON 结构类型(中心定义,避免各处复制 Record)。 + * + * 渐进策略: + * - JsonObject:当前仍用 any 值以兼容深层 context 访问(.a.b.c) + * - 新域模型请用 TaskContext / 明确 interface,少用 JsonObject + * - 后续可把热点路径迁到 StrictJsonObject(unknown 值) + */ + +/** 严格 JSON(推荐新代码) */ +export type JsonPrimitive = string | number | boolean | null +export type StrictJson = + | JsonPrimitive + | StrictJson[] + | { readonly [key: string]: StrictJson } +export type StrictJsonObject = { [key: string]: StrictJson } + +/** + * 宽松 JSON 对象袋(兼容既有履约 context / 平台响应)。 + * 值使用 any 仅为兼容深层属性链;新增代码优先 TaskContext 或 StrictJsonObject。 + */ +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export type JsonObject = Record + +/** @deprecated 使用 JsonObject */ +export type JsonRecord = JsonObject + +export function isPlainObject(value: unknown): value is JsonObject { + return Object.prototype.toString.call(value) === '[object Object]' +} + +export function asJsonObject(value: unknown): JsonObject { + return isPlainObject(value) ? value : {} +} + +export function getJsonProp(value: unknown, key: string): unknown { + if (!isPlainObject(value)) { + return undefined + } + return value[key] +} diff --git a/apps/backend/src/types/task-context.ts b/apps/backend/src/types/task-context.ts new file mode 100644 index 00000000..ad0ab223 --- /dev/null +++ b/apps/backend/src/types/task-context.ts @@ -0,0 +1,26 @@ +import type { JsonObject } from './json.js' + +/** + * 履约任务 context_json 的已知字段。 + * 未建模扩展仍可通过索引访问(JsonObject)。 + */ +export type ClaimIdentityContext = { + expectedUid?: string + submittedAt?: string | null + source?: string + uid?: string +} + +export type TaskContext = JsonObject & { + claimIdentity?: ClaimIdentityContext | JsonObject + kuaishouCloudFulfillment?: JsonObject + kuaishouFeifei?: JsonObject + kuaishouIndustryVoucher?: JsonObject + redeemResolution?: JsonObject + manualDispatch?: JsonObject + mock?: JsonObject + profileKey?: string + profileName?: string + skuCode?: string + skuName?: string +} diff --git a/apps/backend/src/utils/logger.ts b/apps/backend/src/utils/logger.ts index b0ceb911..86f1c55d 100644 --- a/apps/backend/src/utils/logger.ts +++ b/apps/backend/src/utils/logger.ts @@ -6,6 +6,7 @@ import util from 'node:util' import { PROJECT_ROOT, runtimeConfig } from '../config/runtime.js' import type { HttpErrorLike } from './http.js' import { maskSecret } from './masking.js' +import { asJsonObject } from '../types/json.js' type LogLevel = 'debug' | 'info' | 'warn' | 'error' type LogChannel = 'app' | 'integration' @@ -362,7 +363,7 @@ function isSensitiveLogKey(key: unknown): boolean { function normalizeExternalHttpPacketDetail( detail: ExternalHttpPacketDetail, ): ExternalHttpPacketDetail { - const source = detail && typeof detail === 'object' && !Array.isArray(detail) ? detail : {} + const source = asJsonObject(detail) const normalized: ExternalHttpPacketDetail = {} for (const [key, value] of Object.entries(source)) { diff --git a/apps/backend/src/utils/task-json.ts b/apps/backend/src/utils/task-json.ts index 8d1a45fe..d10e4bf9 100644 --- a/apps/backend/src/utils/task-json.ts +++ b/apps/backend/src/utils/task-json.ts @@ -1,26 +1,34 @@ -export type JsonRecord = Record +import type { JsonObject, JsonRecord } from '../types/json.js' +import { asJsonObject, isPlainObject } from '../types/json.js' +import type { TaskContext } from '../types/task-context.js' -export function parseJsonObject(value: unknown): JsonRecord { +export type { JsonRecord, JsonObject } + +export function parseJsonObject(value: unknown): JsonObject { if (!value) { return {} } - if (typeof value === 'object' && !Array.isArray(value)) { - return value as JsonRecord + if (isPlainObject(value)) { + return value } try { - const parsed = JSON.parse(String(value || '{}')) - return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : {} + const parsed = JSON.parse(String(value || '{}')) as unknown + return asJsonObject(parsed) } catch { return {} } } -export function parseTaskContext(task: { context_json?: unknown } | null | undefined): JsonRecord { - return parseJsonObject(task?.context_json) +export function parseTaskContext( + task: { context_json?: unknown } | null | undefined, +): TaskContext { + return parseJsonObject(task?.context_json) as TaskContext } -export function parseTaskState(task: { state_json?: unknown } | null | undefined): JsonRecord { +export function parseTaskState( + task: { state_json?: unknown } | null | undefined, +): JsonObject { return parseJsonObject(task?.state_json) } diff --git a/apps/backend/tsconfig.json b/apps/backend/tsconfig.json index 4a2c1a75..1ba6f1fb 100644 --- a/apps/backend/tsconfig.json +++ b/apps/backend/tsconfig.json @@ -3,8 +3,8 @@ "target": "ES2022", "module": "NodeNext", "moduleResolution": "NodeNext", - "allowJs": true, - "checkJs": true, + "allowJs": false, + "checkJs": false, "noEmit": true, "strict": true, "noUncheckedIndexedAccess": true, @@ -14,13 +14,11 @@ "lib": ["ES2022"] }, "include": [ - "src/**/*.js", "src/**/*.ts" ], "exclude": [ "node_modules", "data", - "src/**/*.test.js", "src/**/*.test.ts" ] } diff --git a/docs/履约配置/TypeScript类型收紧.md b/docs/履约配置/TypeScript类型收紧.md new file mode 100644 index 00000000..a1bff67d --- /dev/null +++ b/docs/履约配置/TypeScript类型收紧.md @@ -0,0 +1,26 @@ +# TypeScript 类型收紧 + +## 已完成 + +1. **源码仅 TS**:`allowJs: false`,不再 include `*.js` +2. **统一 JSON 类型中心**:`apps/backend/src/types/json.ts` + - `JsonObject`:中心定义(当前值仍为 any 以兼容深层 context 访问) + - `StrictJson` / `StrictJsonObject`:新代码可用的 unknown 严格形态 + - `asJsonObject` / `isPlainObject` 工具 +3. **去掉约 60 处本地** `type JsonObject = Record` +4. **TaskContext**:`types/task-context.ts`,`parseTaskContext` 返回 `TaskContext` +5. **Admin 任务详情契约**:`types/admin/task-detail.ts`,`getAdminTaskDetail(): Promise` +6. **测试 fixture**:`delivery-task-service.test.ts` 减少 `as any` + +## 未做完(后续) + +- 将 `JsonObject` 值从 `any` 迁到 `unknown`(需改大量 `.a.b` 访问) +- 前后端共享类型包 / OpenAPI +- 前端对齐 `noUncheckedIndexedAccess` +- 全量测试 fixture 工厂化 + +## 约定 + +- **新域逻辑**优先写 interface(如 `ClaimIdentity`、`AdminClaimIdentitySummary`) +- **平台原始响应**可暂用 `JsonObject`,但 normalize 出口要有明确类型 +- 禁止再新增本地 `type JsonObject = Record`