收紧后端 TypeScript:统一 Json 类型与 TaskContext 契约

关闭 allowJs;集中 JsonObject 定义并替换分散 any 别名;parseTaskContext 返回 TaskContext;任务详情 API 标注 AdminTaskDetailView;测试减少 as any。
This commit is contained in:
yml2213
2026-07-10 15:52:05 +08:00
parent e7244eaa5e
commit 8bcd9bbd81
79 changed files with 415 additions and 259 deletions
@@ -3,10 +3,9 @@ import { TASK_STATUS } from '../domain/task-status.js'
import { maskCode } from '../utils/masking.js' import { maskCode } from '../utils/masking.js'
import { parseTaskContext } from '../utils/task-json.js' import { parseTaskContext } from '../utils/task-json.js'
import { normalizeTimestampIso } from '../utils/time.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 QueryExecutor = (text: string, params?: unknown[]) => Promise<{ rows: unknown[] }>
type JsonRecord = Record<string, any>
export type KuaishouCloudSourceLoadStat = { export type KuaishouCloudSourceLoadStat = {
sourceKey: string sourceKey: string
activeCount: number activeCount: number
+2 -1
View File
@@ -1,9 +1,10 @@
import { Router } from 'express' import { Router } from 'express'
import type { JsonObject } from '../../types/json.js'
import { getAdminAuditLogs } from '../../services/admin/admin-audit-service.js' import { getAdminAuditLogs } from '../../services/admin/admin-audit-service.js'
import { createJsonHandler, requireAdminRoles } from './session.js' import { createJsonHandler, requireAdminRoles } from './session.js'
type AdminAuditLogRouteQuery = Record<string, any> type AdminAuditLogRouteQuery = JsonObject
const router = Router() const router = Router()
+3 -2
View File
@@ -1,5 +1,6 @@
import process from 'node:process' import process from 'node:process'
import { Router } from 'express' import { Router } from 'express'
import type { JsonObject } from '../types/json.js'
import { createRateLimitMiddleware } from '../middleware/rate-limit.js' import { createRateLimitMiddleware } from '../middleware/rate-limit.js'
import { handleSendCode } from '../services/platforms/kuaishou-industry/send-code-service.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<string, any> { function mergeRequestParams(req: any): JsonObject {
const query = req.query || {} const query = req.query || {}
const body = req.body || {} const body = req.body || {}
const params: Record<string, any> = { ...query } const params: JsonObject = { ...query }
for (const [key, value] of Object.entries(body)) { for (const [key, value] of Object.entries(body)) {
if (params[key] === undefined && key !== 'param') { if (params[key] === undefined && key !== 'param') {
@@ -1,8 +1,7 @@
import { createAdminAuditLog, listAdminAuditLogs } from '../../repositories/admin-audit-log-repo.js' import { createAdminAuditLog, listAdminAuditLogs } from '../../repositories/admin-audit-log-repo.js'
import { nowIso } from '../../utils/time.js' import { nowIso } from '../../utils/time.js'
import { normalizeDateQuery, normalizePage, normalizePageSize, safeParseJson } from './admin-query-utils.js' import { normalizeDateQuery, normalizePage, normalizePageSize, safeParseJson } from './admin-query-utils.js'
import type { JsonObject } from '../../types/json.js'
type JsonObject = Record<string, any>
export async function writeAdminAuditLog(session: JsonObject | null | undefined, payload: JsonObject = {}) { export async function writeAdminAuditLog(session: JsonObject | null | undefined, payload: JsonObject = {}) {
if (!session?.userId) { if (!session?.userId) {
@@ -1,4 +1,5 @@
import crypto from 'node:crypto' import crypto from 'node:crypto'
import type { JsonObject } from '../../types/json.js'
import { runtimeConfig } from '../../config/runtime.js' import { runtimeConfig } from '../../config/runtime.js'
import { import {
@@ -15,8 +16,6 @@ import { normalizePage, normalizePageSize } from './admin-query-utils.js'
type AdminUserRow = NonNullable<Awaited<ReturnType<typeof getAdminUserById>>> type AdminUserRow = NonNullable<Awaited<ReturnType<typeof getAdminUserById>>>
type JsonObject = Record<string, any>
export type AdminSession = { export type AdminSession = {
sessionId: string sessionId: string
userId: number userId: number
@@ -1,8 +1,7 @@
import { query } from '../../db/client.js' import { query } from '../../db/client.js'
import { TASK_STATUS } from '../../domain/task-status.js' import { TASK_STATUS } from '../../domain/task-status.js'
import { nowIso } from '../../utils/time.js' import { nowIso } from '../../utils/time.js'
import type { JsonObject } from '../../types/json.js'
type JsonObject = Record<string, any>
export async function getAdminDashboardSummary() { export async function getAdminDashboardSummary() {
const todayPrefix = nowIso().slice(0, 10) const todayPrefix = nowIso().slice(0, 10)
@@ -1,5 +1,4 @@
type JsonObject = Record<string, any> import type { JsonObject } from '../../types/json.js'
export function normalizePage(rawValue: unknown) { export function normalizePage(rawValue: unknown) {
const parsed = Number(rawValue) const parsed = Number(rawValue)
return Number.isFinite(parsed) && parsed > 0 ? Math.floor(parsed) : 1 return Number.isFinite(parsed) && parsed > 0 ? Math.floor(parsed) : 1
@@ -5,6 +5,7 @@ import { listOrderItemsByOrderId } from '../../repositories/order-item-repo.js'
import { getOrderById, listOrders } from '../../repositories/order-repo.js' import { getOrderById, listOrders } from '../../repositories/order-repo.js'
import { getTaskById, listTasks, listTasksByOrderId } from '../../repositories/task-repo.js' import { getTaskById, listTasks, listTasksByOrderId } from '../../repositories/task-repo.js'
import { listTaskEventsByTaskId } from '../../repositories/task-event-repo.js' import { listTaskEventsByTaskId } from '../../repositories/task-event-repo.js'
import type { JsonObject, JsonRecord } from '../../types/json.js'
import { import {
listKuaishouIndustryVouchersByOid, listKuaishouIndustryVouchersByOid,
listKuaishouIndustryVouchersByTaskId, listKuaishouIndustryVouchersByTaskId,
@@ -58,6 +59,7 @@ import type {
AdminOrderListResponse, AdminOrderListResponse,
AdminTaskListResponse, AdminTaskListResponse,
} from '../../types/admin/read-models.js' } from '../../types/admin/read-models.js'
import type { AdminTaskDetailView } from '../../types/admin/task-detail.js'
import type { import type {
AdminEntityIdInput, AdminEntityIdInput,
AdminOrderListQueryInput, AdminOrderListQueryInput,
@@ -69,8 +71,6 @@ import type {
TaskRow, TaskRow,
} from '../../types/repository/rows.js' } from '../../types/repository/rows.js'
type JsonRecord = Record<string, any>
export async function getAdminOrders( export async function getAdminOrders(
query: AdminOrderListQueryInput = {}, query: AdminOrderListQueryInput = {},
): Promise<AdminOrderListResponse> { ): Promise<AdminOrderListResponse> {
@@ -218,7 +218,7 @@ export async function getAdminTasks(
export async function getAdminTaskDetail( export async function getAdminTaskDetail(
taskId: AdminEntityIdInput, taskId: AdminEntityIdInput,
session: AdminViewerSessionInput | null = null, session: AdminViewerSessionInput | null = null,
): Promise<JsonRecord> { ): Promise<AdminTaskDetailView> {
const task = await getTaskById(Number(taskId)) const task = await getTaskById(Number(taskId))
if (!task) { if (!task) {
@@ -1,5 +1,6 @@
import { safeParseJson } from './admin-query-utils.js' import { safeParseJson } from './admin-query-utils.js'
import { normalizeAdminRole } from './admin-auth-service.js' import { normalizeAdminRole } from './admin-auth-service.js'
import { asJsonObject, type JsonObject, JsonRecord } from '../../types/json.js'
import { import {
parseTaskContext as parseTaskContextValue, parseTaskContext as parseTaskContextValue,
parseTaskState as parseTaskStateValue, parseTaskState as parseTaskStateValue,
@@ -9,7 +10,6 @@ import { canRegenerateClaimLinkStatus, isTaskFinalStatus } from '../../domain/ta
import type { AdminViewerSessionInput } from '../../types/admin/read-inputs.js' import type { AdminViewerSessionInput } from '../../types/admin/read-inputs.js'
import type { OrderItemRow, TaskRow } from '../../types/repository/rows.js' import type { OrderItemRow, TaskRow } from '../../types/repository/rows.js'
type JsonRecord = Record<string, any>
type CloudSourceLabelMap = Map<string, string> | Record<string, string> type CloudSourceLabelMap = Map<string, string> | Record<string, string>
type KuaishouCloudFulfillmentMapOptions = { type KuaishouCloudFulfillmentMapOptions = {
@@ -74,14 +74,14 @@ export function mapKuaishouCloudFulfillmentContext(
} }
const record = value as JsonRecord const record = value as JsonRecord
const ticket = record.ticket && typeof record.ticket === 'object' ? record.ticket : {} const ticket = asJsonObject(record.ticket)
const binding = record.binding && typeof record.binding === 'object' ? record.binding : {} const binding = asJsonObject(record.binding)
const role = record.role && typeof record.role === 'object' ? record.role : {} const role = asJsonObject(record.role)
const purchase = record.purchase && typeof record.purchase === 'object' ? record.purchase : {} const purchase = asJsonObject(record.purchase)
const dispatch = record.dispatch && typeof record.dispatch === 'object' ? record.dispatch : {} const dispatch = asJsonObject(record.dispatch)
const returnNumber = const returnNumber =
record.returnNumber && typeof record.returnNumber === 'object' ? record.returnNumber : {} asJsonObject(record.returnNumber)
const consume = record.consume && typeof record.consume === 'object' ? record.consume : {} const consume = asJsonObject(record.consume)
const cloudSourceKeys = Array.isArray(binding.cloudSourceKeys) const cloudSourceKeys = Array.isArray(binding.cloudSourceKeys)
? binding.cloudSourceKeys.map((value: unknown) => String(value || '').trim()).filter(Boolean) ? binding.cloudSourceKeys.map((value: unknown) => String(value || '').trim()).filter(Boolean)
: [] : []
@@ -1,6 +1,7 @@
import { getTaskById } from '../../repositories/task-repo.js' import { getTaskById } from '../../repositories/task-repo.js'
import { createHttpError } from '../../utils/http.js' import { createHttpError } from '../../utils/http.js'
import { safeParseJson } from './admin-query-utils.js' import { safeParseJson } from './admin-query-utils.js'
import { asJsonObject, type JsonObject, JsonRecord } from '../../types/json.js'
import { import {
TASK_STATUS, TASK_STATUS,
isTaskFulfillmentCompletedStatus, isTaskFulfillmentCompletedStatus,
@@ -22,8 +23,6 @@ import type {
} from '../../types/repository/rows.js' } from '../../types/repository/rows.js'
import type { AdminViewerContext } from './admin-read-shared-helpers.js' import type { AdminViewerContext } from './admin-read-shared-helpers.js'
type JsonRecord = Record<string, any>
type TaskFulfillmentState = { type TaskFulfillmentState = {
resourceStatus: string resourceStatus: string
customerStatus: string customerStatus: string
@@ -263,7 +262,7 @@ function isTaskFulfillmentCompleted(task: TaskRow | null | undefined): boolean {
} }
function normalizeRecord(value: unknown): JsonRecord { function normalizeRecord(value: unknown): JsonRecord {
return value && typeof value === 'object' && !Array.isArray(value) ? value : {} return asJsonObject(value)
} }
function getTaskRetryCount(task: TaskRow | null | undefined): number { function getTaskRetryCount(task: TaskRow | null | undefined): number {
@@ -1,3 +1,4 @@
import { asJsonObject, type JsonObject } from '../../types/json.js'
import { import {
findKuaishouIndustryVoucherByCode, findKuaishouIndustryVoucherByCode,
listKuaishouIndustryVouchersByOid, listKuaishouIndustryVouchersByOid,
@@ -27,8 +28,6 @@ import { reverseKuaishouIndustryCallback } from '../platforms/kuaishou-industry/
import type { KuaishouIndustryOpenApiCallResult } from '../platforms/kuaishou-industry/openapi-client.js' import type { KuaishouIndustryOpenApiCallResult } from '../platforms/kuaishou-industry/openapi-client.js'
import type { KuaishouIndustryVoucherRow, TaskRow } from '../../types/repository/rows.js' import type { KuaishouIndustryVoucherRow, TaskRow } from '../../types/repository/rows.js'
type JsonObject = Record<string, any>
export async function listAdminKuaishouIndustryVouchers( export async function listAdminKuaishouIndustryVouchers(
input: KuaishouIndustryVoucherAdminListQuery = {}, input: KuaishouIndustryVoucherAdminListQuery = {},
) { ) {
@@ -462,7 +461,7 @@ function parseJsonObject(value: unknown): JsonObject {
try { try {
const parsed = JSON.parse(String(value || '{}')) const parsed = JSON.parse(String(value || '{}'))
return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : {} return asJsonObject(parsed)
} catch { } catch {
return {} return {}
} }
@@ -1,5 +1,3 @@
type JsonObject = Record<string, any>
export function pickFirstNonEmpty(values: unknown[]) { export function pickFirstNonEmpty(values: unknown[]) {
for (const value of values) { for (const value of values) {
const normalized = String(value || "").trim(); 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 { getCloudtentaclesSourceByKey } from "../../../platforms/cloudtentacles/source-config-service.js";
import { getCloudtentaclesSessionStateByKey } from "../../../platforms/cloudtentacles/session-state-service.js"; import { getCloudtentaclesSessionStateByKey } from "../../../platforms/cloudtentacles/session-state-service.js";
import type { JsonObject } from '../../../../types/json.js'
import { import {
normalizeCloudtentaclesDeviceId, normalizeCloudtentaclesDeviceId,
normalizeCloudtentaclesDeviceType, normalizeCloudtentaclesDeviceType,
@@ -1,3 +1,4 @@
import type { JsonObject } from '../../../../types/json.js'
import { import {
getCloudtentaclesSourceByKey, getCloudtentaclesSourceByKey,
getCloudtentaclesSourcesFilePath, getCloudtentaclesSourcesFilePath,
@@ -79,8 +80,6 @@ import type {
AdminCloudtentaclesVirtualNumberInput, AdminCloudtentaclesVirtualNumberInput,
} from "../../../../types/admin/write-inputs.js"; } from "../../../../types/admin/write-inputs.js";
type JsonObject = Record<string, any>;
type LocalCloudtentaclesTask = { type LocalCloudtentaclesTask = {
taskId: number; taskId: number;
taskNo: string; taskNo: string;
@@ -1,3 +1,4 @@
import type { JsonObject } from '../../../../types/json.js'
import { import {
maskPhone as maskPhoneValue, maskPhone as maskPhoneValue,
maskSecret as maskSecretValue, maskSecret as maskSecretValue,
@@ -7,8 +8,6 @@ import {
normalizeCloudtentaclesDeviceType, normalizeCloudtentaclesDeviceType,
} from "../../../platforms/cloudtentacles/defaults.js"; } from "../../../platforms/cloudtentacles/defaults.js";
type JsonObject = Record<string, any>
export function maskSecret(value: unknown) { export function maskSecret(value: unknown) {
return maskSecretValue(value); return maskSecretValue(value);
} }
@@ -1,5 +1,6 @@
import { getCloudtentaclesSourceByKey } from "../../../platforms/cloudtentacles/source-config-service.js"; import { getCloudtentaclesSourceByKey } from "../../../platforms/cloudtentacles/source-config-service.js";
import { getCloudtentaclesSessionStateByKey } from "../../../platforms/cloudtentacles/session-state-service.js"; import { getCloudtentaclesSessionStateByKey } from "../../../platforms/cloudtentacles/session-state-service.js";
import type { JsonObject } from '../../../../types/json.js'
import { import {
normalizeCloudtentaclesDeviceId, normalizeCloudtentaclesDeviceId,
normalizeCloudtentaclesDeviceType, normalizeCloudtentaclesDeviceType,
@@ -12,8 +13,6 @@ import { maskPhone, maskSecret } from "./mappers.js";
const DEFAULT_CLOUDTENTACLES_BASE_URL = "https://123.207.217.176"; const DEFAULT_CLOUDTENTACLES_BASE_URL = "https://123.207.217.176";
type JsonObject = Record<string, any>
function _resolveSourceKey(payload: JsonObject = {}) { function _resolveSourceKey(payload: JsonObject = {}) {
return String(payload.sourceKey || "").trim() || "default"; return String(payload.sourceKey || "").trim() || "default";
} }
@@ -1,10 +1,9 @@
import type { JsonObject } from '../../../../types/json.js'
import { import {
normalizeCloudtentaclesDeviceId, normalizeCloudtentaclesDeviceId,
normalizeCloudtentaclesDeviceType, normalizeCloudtentaclesDeviceType,
} from "../../../platforms/cloudtentacles/defaults.js"; } from "../../../platforms/cloudtentacles/defaults.js";
type JsonObject = Record<string, any>;
export function normalizeAdminCloudtentaclesSourceConfigPayload( export function normalizeAdminCloudtentaclesSourceConfigPayload(
payload: JsonObject = {} payload: JsonObject = {}
) { ) {
@@ -1,4 +1,5 @@
import { getKuaishouFeifeiConfig } from '../../platforms/kuaishou-feifei/config.js' import { getKuaishouFeifeiConfig } from '../../platforms/kuaishou-feifei/config.js'
import type { JsonObject } from '../../../types/json.js'
import { import {
createKuaishouFeifeiOrder, createKuaishouFeifeiOrder,
listKuaishouFeifeiProducts, listKuaishouFeifeiProducts,
@@ -16,8 +17,6 @@ import {
import { normalizeCloudtentaclesMatchName } from '../../order/cloudtentacles-match-utils.js' import { normalizeCloudtentaclesMatchName } from '../../order/cloudtentacles-match-utils.js'
import { createHttpError } from '../../../utils/http.js' import { createHttpError } from '../../../utils/http.js'
type JsonObject = Record<string, any>
export function getAdminKuaishouFeifeiConfig() { export function getAdminKuaishouFeifeiConfig() {
const source = getAdminEditableKuaishouFeifeiConfig() const source = getAdminEditableKuaishouFeifeiConfig()
const effective = getKuaishouFeifeiConfig() const effective = getKuaishouFeifeiConfig()
@@ -1,5 +1,6 @@
import { maskSecret } from '../../../utils/masking.js' import { maskSecret } from '../../../utils/masking.js'
import { createHttpError } from '../../../utils/http.js' import { createHttpError } from '../../../utils/http.js'
import type { JsonObject } from '../../../types/json.js'
import { import {
findKuaishouIndustryShopConfig, findKuaishouIndustryShopConfig,
getKuaishouIndustrySourceConfig, getKuaishouIndustrySourceConfig,
@@ -13,8 +14,6 @@ import {
refreshKuaishouIndustryAccessToken, refreshKuaishouIndustryAccessToken,
} from '../../platforms/kuaishou-industry/token-service.js' } from '../../platforms/kuaishou-industry/token-service.js'
type JsonObject = Record<string, any>
const SECRET_FIELDS = [ const SECRET_FIELDS = [
'appSecret', 'appSecret',
'signSecret', 'signSecret',
@@ -1,11 +1,10 @@
import type { JsonObject } from '../../../types/json.js'
import { import {
failOpen91Order, failOpen91Order,
listOpen91Orders, listOpen91Orders,
retryOpen91Order, retryOpen91Order,
} from '../../platforms/ninetyone/order-service.js' } from '../../platforms/ninetyone/order-service.js'
type JsonObject = Record<string, any>
export async function getAdminNinetyoneOrders(query: JsonObject = {}) { export async function getAdminNinetyoneOrders(query: JsonObject = {}) {
return listOpen91Orders({ return listOpen91Orders({
page: Number(query.page || 1) || 1, page: Number(query.page || 1) || 1,
@@ -1,3 +1,4 @@
import type { JsonObject } from '../../../types/json.js'
import { import {
getNotificationConfig, getNotificationConfig,
getNotificationConfigFilePath, getNotificationConfigFilePath,
@@ -24,8 +25,6 @@ import type {
AdminScheduledJobsConfigInput, AdminScheduledJobsConfigInput,
} from '../../../types/admin/write-inputs.js' } from '../../../types/admin/write-inputs.js'
type JsonObject = Record<string, any>
export function getAdminNotificationConfig() { export function getAdminNotificationConfig() {
const config = getNotificationConfig() const config = getNotificationConfig()
@@ -1,6 +1,7 @@
import { getOrderById } from '../../../repositories/order-repo.js' import { getOrderById } from '../../../repositories/order-repo.js'
import { listTasksByOrderId, updateTask } from '../../../repositories/task-repo.js' import { listTasksByOrderId, updateTask } from '../../../repositories/task-repo.js'
import { createTaskEvent } from '../../../repositories/task-event-repo.js' import { createTaskEvent } from '../../../repositories/task-event-repo.js'
import type { JsonObject } from '../../../types/json.js'
import { import {
findKuaishouIndustryVoucherByCode, findKuaishouIndustryVoucherByCode,
listKuaishouIndustryVouchersByOid, listKuaishouIndustryVouchersByOid,
@@ -670,9 +671,9 @@ async function getRequiredIndustryVoucherForTask(task: TaskRow): Promise<Kuaisho
}) })
} }
function normalizeAdminTaskIndustryVoucherContext(value: unknown): Record<string, any> { function normalizeAdminTaskIndustryVoucherContext(value: unknown): JsonObject {
return value && typeof value === 'object' && !Array.isArray(value) return value && typeof value === 'object' && !Array.isArray(value)
? value as Record<string, any> ? value as JsonObject
: {} : {}
} }
@@ -1,8 +1,9 @@
import { createHttpError } from '../../../utils/http.js' import { createHttpError } from '../../../utils/http.js'
import type { HttpErrorLike } from '../../../utils/http.js' import type { HttpErrorLike } from '../../../utils/http.js'
import { import type { JsonObject } from '../../../types/json.js'
resolvePersistedCloudtentaclesContextBySourceKeys, import { resolvePersistedCloudtentaclesContextBySourceKeys } from '../../fulfillment/kuaishou-cloud/cloudtentacles-context.js'
} from '../../fulfillment/kuaishou-cloud/cloudtentacles-context.js'
export type { JsonObject }
export { export {
KUAISHOU_CLOUD_FIXED_VN_KEY, KUAISHOU_CLOUD_FIXED_VN_KEY,
@@ -10,7 +11,6 @@ export {
normalizeKuaishouCloudFlow, normalizeKuaishouCloudFlow,
normalizeKuaishouCloudRoleInfo, normalizeKuaishouCloudRoleInfo,
resolveKuaishouCloudBindUrlExpiresAt, resolveKuaishouCloudBindUrlExpiresAt,
type JsonObject,
} from '../../fulfillment/kuaishou-cloud/domain.js' } from '../../fulfillment/kuaishou-cloud/domain.js'
export { export {
prepareKuaishouCloudBindResourceWithFallback, prepareKuaishouCloudBindResourceWithFallback,
@@ -34,17 +34,17 @@ export type CloudSkuLikeItem = {
[key: string]: unknown [key: string]: unknown
} }
export type KuaishouCloudFlow = Record<string, any> & { export type KuaishouCloudFlow = JsonObject & {
configId: string configId: string
internalSkuCode: string internalSkuCode: string
internalSkuName: string internalSkuName: string
ticket: Record<string, any> ticket: JsonObject
binding: Record<string, any> binding: JsonObject
role: Record<string, any> role: JsonObject
purchase: Record<string, any> purchase: JsonObject
dispatch: Record<string, any> dispatch: JsonObject
returnNumber: Record<string, any> returnNumber: JsonObject
consume: Record<string, any> consume: JsonObject
} }
export type KuaishouCloudBindingResources = { export type KuaishouCloudBindingResources = {
@@ -1,3 +1,4 @@
import type { JsonObject } from '../../types/json.js'
import { import {
listFulfillmentProfileRequirements, listFulfillmentProfileRequirements,
replaceFulfillmentProfileRequirements, replaceFulfillmentProfileRequirements,
@@ -6,7 +7,6 @@ import {
} from '../../repositories/fulfillment-profile-repo.js' } from '../../repositories/fulfillment-profile-repo.js'
import { nowIso } from '../../utils/time.js' import { nowIso } from '../../utils/time.js'
type JsonObject = Record<string, any>
type CoreProfile = { type CoreProfile = {
profileKey: string profileKey: string
name: string name: string
@@ -2,6 +2,7 @@ import { createHttpError } from '../../utils/http.js'
import { parseTaskContext } from '../../utils/task-json.js' import { parseTaskContext } from '../../utils/task-json.js'
import { isTaskFinalStatus, normalizeTaskStatus, TASK_STATUS } from '../../domain/task-status.js' import { isTaskFinalStatus, normalizeTaskStatus, TASK_STATUS } from '../../domain/task-status.js'
import type { TaskRow } from '../../types/repository/rows.js' import type { TaskRow } from '../../types/repository/rows.js'
import type { JsonObject } from '../../types/json.js'
export type ClaimIdentity = { export type ClaimIdentity = {
expectedUid: string expectedUid: string
@@ -9,8 +10,6 @@ export type ClaimIdentity = {
source: string source: string
} }
type JsonObject = Record<string, any>
const CLAIM_UID_MAX_LENGTH = 64 const CLAIM_UID_MAX_LENGTH = 64
export function normalizeClaimUid(value: unknown): string { export function normalizeClaimUid(value: unknown): string {
@@ -131,10 +130,22 @@ export function assertLewanAutoFulfillmentUidReady(
return expectedUid 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( export function buildClaimIdentityAdminSummary(
context: unknown, context: unknown,
options: { flowLike?: unknown; taskRoleId?: unknown; taskRoleName?: unknown } = {}, options: { flowLike?: unknown; taskRoleId?: unknown; taskRoleName?: unknown } = {},
) { ): ClaimIdentityAdminSummary {
const identity = getClaimIdentityFromContext(context) const identity = getClaimIdentityFromContext(context)
const boundUid = resolveBoundRoleUid(options.flowLike) || normalizeClaimUid(options.taskRoleId) const boundUid = resolveBoundRoleUid(options.flowLike) || normalizeClaimUid(options.taskRoleId)
const flow = isPlainObject(options.flowLike) ? options.flowLike : {} const flow = isPlainObject(options.flowLike) ? options.flowLike : {}
@@ -11,9 +11,9 @@ import { buildClaimUrl } from './claim-service.js'
import { buildClaimIdentityPayload, getClaimIdentityFromContext } from './claim-identity.js' import { buildClaimIdentityPayload, getClaimIdentityFromContext } from './claim-identity.js'
import { resolveKuaishouFeifeiH5UrlWithUid } from '../fulfillment/kuaishou-feifei/index.js' import { resolveKuaishouFeifeiH5UrlWithUid } from '../fulfillment/kuaishou-feifei/index.js'
import type { ClaimTokenRow, OrderItemRow, OrderRow, TaskRow } from '../../types/repository/rows.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]) export const CLAIM_TERMINAL_STATUSES = new Set([TASK_STATUS.EXPIRED, TASK_STATUS.CLOSED])
type JsonObject = Record<string, any>
type ClaimContext = { type ClaimContext = {
claimToken: ClaimTokenRow claimToken: ClaimTokenRow
task: TaskRow task: TaskRow
@@ -343,13 +343,13 @@ export function mapClaimKuaishouCloudFulfillment(task: TaskRow, order: OrderRow)
return null return null
} }
const binding = source.binding && typeof source.binding === 'object' ? source.binding : {} const binding = asJsonObject(source.binding)
const role = source.role && typeof source.role === 'object' ? source.role : {} const role = asJsonObject(source.role)
const purchase = source.purchase && typeof source.purchase === 'object' ? source.purchase : {} const purchase = asJsonObject(source.purchase)
const ticket = source.ticket && typeof source.ticket === 'object' ? source.ticket : {} const ticket = asJsonObject(source.ticket)
const dispatch = source.dispatch && typeof source.dispatch === 'object' ? source.dispatch : {} const dispatch = asJsonObject(source.dispatch)
const returnNumber = source.returnNumber && typeof source.returnNumber === 'object' ? source.returnNumber : {} const returnNumber = asJsonObject(source.returnNumber)
const consume = source.consume && typeof source.consume === 'object' ? source.consume : {} const consume = asJsonObject(source.consume)
const roleName = String(role.name || binding.roleName || '').trim() const roleName = String(role.name || binding.roleName || '').trim()
const roleId = String(role.rid || binding.roleId || '').trim() const roleId = String(role.rid || binding.roleId || '').trim()
const defaultRoleName = String(role.defaultName || role.defaultRoleName || '').trim() const defaultRoleName = String(role.defaultName || role.defaultRoleName || '').trim()
@@ -3,6 +3,7 @@ import { getTaskById, updateTask, updateTaskStatusIfCurrent } from '../../reposi
import { createHttpError } from '../../utils/http.js' import { createHttpError } from '../../utils/http.js'
import { parseTaskContext as parseTaskContextValue } from '../../utils/task-json.js' import { parseTaskContext as parseTaskContextValue } from '../../utils/task-json.js'
import { addHours, nowIso } from '../../utils/time.js' import { addHours, nowIso } from '../../utils/time.js'
import { asJsonObject, type JsonObject } from '../../types/json.js'
import { import {
TASK_STATUS, TASK_STATUS,
canRedeemKuaishouCloudClaimStatus, canRedeemKuaishouCloudClaimStatus,
@@ -29,7 +30,6 @@ import { buildClaimDetailPayload, getClaimContext } from './kuaishou-cloud-claim
import { syncKuaishouCloudRoleInfo } from './kuaishou-cloud-sync-service.js' import { syncKuaishouCloudRoleInfo } from './kuaishou-cloud-sync-service.js'
import type { TaskRow } from '../../types/repository/rows.js' import type { TaskRow } from '../../types/repository/rows.js'
type JsonObject = Record<string, any>
type ClaimDetailPayload = ReturnType<typeof buildClaimDetailPayload> type ClaimDetailPayload = ReturnType<typeof buildClaimDetailPayload>
async function verifyIndustryVoucherTicket( async function verifyIndustryVoucherTicket(
@@ -537,9 +537,7 @@ function isKuaishouCloudMockTask(task: Partial<TaskRow> | null | undefined) {
function isKuaishouCloudMockContext(context: JsonObject = {}) { function isKuaishouCloudMockContext(context: JsonObject = {}) {
const flow = const flow =
context.kuaishouCloudFulfillment && typeof context.kuaishouCloudFulfillment === 'object' asJsonObject(context.kuaishouCloudFulfillment)
? context.kuaishouCloudFulfillment
: {}
const mock = flow.mock && typeof flow.mock === 'object' ? flow.mock : context.mock const mock = flow.mock && typeof flow.mock === 'object' ? flow.mock : context.mock
return Boolean(mock && typeof mock === 'object' && mock.enabled === true) return Boolean(mock && typeof mock === 'object' && mock.enabled === true)
@@ -558,7 +556,7 @@ function buildMockVerifiedKuaishouCloudFlow(value: unknown, timestamp: string) {
return { return {
...flow, ...flow,
mock: { mock: {
...(source.mock && typeof source.mock === 'object' ? source.mock : {}), ...(asJsonObject(source.mock)),
enabled: true, enabled: true,
}, },
binding: { binding: {
@@ -593,7 +591,7 @@ async function completeMockKuaishouCloudClaimTask(task: TaskRow, timestamp: stri
const nextFlow = { const nextFlow = {
...flow, ...flow,
mock: { mock: {
...(source.mock && typeof source.mock === 'object' ? source.mock : {}), ...(asJsonObject(source.mock)),
enabled: true, enabled: true,
}, },
dispatch: { dispatch: {
@@ -5,6 +5,7 @@ import {
refreshKuaishouCloudTaskRoleInfo, refreshKuaishouCloudTaskRoleInfo,
} from '../fulfillment/kuaishou-cloud/index.js' } from '../fulfillment/kuaishou-cloud/index.js'
import type { TaskRow } from '../../types/repository/rows.js' import type { TaskRow } from '../../types/repository/rows.js'
import { parseTaskContext as parseTaskContextValue } from '../../utils/task-json.js'
export async function syncKuaishouCloudRoleInfo(task: TaskRow) { export async function syncKuaishouCloudRoleInfo(task: TaskRow) {
const flow = normalizeKuaishouCloudFlow(parseTaskContext(task).kuaishouCloudFulfillment) const flow = normalizeKuaishouCloudFlow(parseTaskContext(task).kuaishouCloudFulfillment)
@@ -45,20 +46,6 @@ export async function syncKuaishouCloudRoleInfo(task: TaskRow) {
} }
} }
function parseTaskContext(task: Partial<TaskRow> | null | undefined): Record<string, any> { function parseTaskContext(task: Partial<TaskRow> | null | undefined) {
const rawValue = task?.context_json return parseTaskContextValue(task)
if (!rawValue) {
return {}
}
if (typeof rawValue === 'object') {
return rawValue
}
try {
return JSON.parse(String(rawValue || '{}'))
} catch {
return {}
}
} }
@@ -8,6 +8,7 @@ import {
isClaimUidMatched, isClaimUidMatched,
normalizeClaimUid, normalizeClaimUid,
} from "../../claim/claim-identity.js"; } from "../../claim/claim-identity.js";
import { asJsonObject } from "../../../types/json.js";
import { import {
normalizeKuaishouCloudFlow, normalizeKuaishouCloudFlow,
normalizeKuaishouCloudRoleInfo, normalizeKuaishouCloudRoleInfo,
@@ -26,13 +27,9 @@ export async function syncKuaishouCloudRoleInfoBeforeDispatch(
String(input.errorCodePrefix || "kuaishou_cloud").trim() || String(input.errorCodePrefix || "kuaishou_cloud").trim() ||
"kuaishou_cloud"; "kuaishou_cloud";
const cloudContext = const cloudContext =
input.cloudContext && typeof input.cloudContext === "object" asJsonObject(input.cloudContext);
? input.cloudContext
: {};
const taskContext = const taskContext =
input.taskContext && typeof input.taskContext === "object" asJsonObject(input.taskContext);
? input.taskContext
: {};
const flow = normalizeKuaishouCloudFlow( const flow = normalizeKuaishouCloudFlow(
input.flow || taskContext.kuaishouCloudFulfillment input.flow || taskContext.kuaishouCloudFulfillment
); );
@@ -1,10 +1,11 @@
import { resolveCloudtentaclesConfig } from '../../platforms/cloudtentacles/helpers.js' import { resolveCloudtentaclesConfig } from '../../platforms/cloudtentacles/helpers.js'
import { maskCode as maskCodeValue, maskPhone as maskPhoneValue } from '../../../utils/masking.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 const KUAISHOU_CLOUD_FIXED_VN_KEY = '1'
export type JsonObject = Record<string, any>
const KUAISHOU_CLOUD_EXECUTOR_KEYS = new Set(['kuaishou_ct_assisted', 'kuaishou-industry']) const KUAISHOU_CLOUD_EXECUTOR_KEYS = new Set(['kuaishou_ct_assisted', 'kuaishou-industry'])
export function isKuaishouCloudTask(task: unknown) { export function isKuaishouCloudTask(task: unknown) {
@@ -19,15 +20,15 @@ export function isIndustryEVoucherTask(task: unknown) {
export function normalizeKuaishouCloudFlow(value: unknown) { export function normalizeKuaishouCloudFlow(value: unknown) {
const source: JsonObject = value && typeof value === 'object' ? (value as JsonObject) : {} 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)
const role = source.role && typeof source.role === 'object' ? source.role : {} const role = asJsonObject(source.role)
const purchase = source.purchase && typeof source.purchase === 'object' ? source.purchase : {} const purchase = asJsonObject(source.purchase)
const dispatch = source.dispatch && typeof source.dispatch === 'object' ? source.dispatch : {} const dispatch = asJsonObject(source.dispatch)
const returnNumber = const returnNumber =
source.returnNumber && typeof source.returnNumber === 'object' ? source.returnNumber : {} asJsonObject(source.returnNumber)
const consume = source.consume && typeof source.consume === 'object' ? source.consume : {} const consume = asJsonObject(source.consume)
const ticket = source.ticket && typeof source.ticket === 'object' ? source.ticket : {} const ticket = asJsonObject(source.ticket)
const rebind = source.rebind && typeof source.rebind === 'object' ? source.rebind : {} const rebind = asJsonObject(source.rebind)
const deliveryItems = normalizeDeliveryItems(source, binding) const deliveryItems = normalizeDeliveryItems(source, binding)
const roleName = String(role.name || binding.roleName || '').trim() const roleName = String(role.name || binding.roleName || '').trim()
@@ -180,7 +181,7 @@ function normalizeRoleNameForCompare(value: unknown) {
export function normalizeKuaishouCloudDeliveryItems(value: unknown) { export function normalizeKuaishouCloudDeliveryItems(value: unknown) {
const source: JsonObject = value && typeof value === 'object' ? (value as JsonObject) : {} 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) return normalizeDeliveryItems(source, binding)
} }
@@ -1,7 +1,6 @@
import { parseTaskContext as parseTaskContextValue } from "../../../utils/task-json.js"; import { parseTaskContext as parseTaskContextValue } from "../../../utils/task-json.js";
import type { TaskRow } from "../../../types/repository/rows.js"; import type { TaskRow } from "../../../types/repository/rows.js";
import type { JsonObject } from '../../../types/json.js'
type JsonObject = Record<string, any>;
export function normalizeActor(actor: unknown) { export function normalizeActor(actor: unknown) {
if (!actor || typeof actor !== "object") { if (!actor || typeof actor !== "object") {
@@ -8,6 +8,7 @@ import { parseTaskContext } from '../../../utils/task-json.js'
import { nowIso } from '../../../utils/time.js' import { nowIso } from '../../../utils/time.js'
import { TASK_STATUS } from '../../../domain/task-status.js' import { TASK_STATUS } from '../../../domain/task-status.js'
import { appendUidToUrl, getClaimIdentityFromContext } from '../../claim/claim-identity.js' import { appendUidToUrl, getClaimIdentityFromContext } from '../../claim/claim-identity.js'
import type { JsonObject } from '../../../types/json.js'
import { import {
createKuaishouFeifeiOrder, createKuaishouFeifeiOrder,
queryKuaishouFeifeiOrder, queryKuaishouFeifeiOrder,
@@ -17,8 +18,6 @@ import { buildKuaishouIndustryVoucherContext } from '../../platforms/kuaishou-in
import { normalizeShortLinkPayload } from '../../short-links/short-link-service.js' import { normalizeShortLinkPayload } from '../../short-links/short-link-service.js'
import type { TaskRow } from '../../../types/repository/rows.js' import type { TaskRow } from '../../../types/repository/rows.js'
type JsonObject = Record<string, any>
export function isKuaishouFeifeiTask(task: Partial<TaskRow> | null | undefined) { export function isKuaishouFeifeiTask(task: Partial<TaskRow> | null | undefined) {
return String(task?.executor_key || '').trim() === 'kuaishou_feifei' return String(task?.executor_key || '').trim() === 'kuaishou_feifei'
} }
@@ -1,4 +1,5 @@
import path from 'node:path' import path from 'node:path'
import type { JsonObject } from '../../types/json.js'
import { APP_CONFIG_KEYS } from '../../config/app-config-keys.js' import { APP_CONFIG_KEYS } from '../../config/app-config-keys.js'
import { PROJECT_ROOT } from '../../config/runtime.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 NOTIFICATION_CONFIG_FILE_PATH = path.join(PROJECT_ROOT, 'data', 'notification-config.json')
const DEFAULT_BARK_SERVER_URL = 'https://api.day.app' const DEFAULT_BARK_SERVER_URL = 'https://api.day.app'
type JsonObject = Record<string, any>
type NotificationRecipient = { type NotificationRecipient = {
id: string id: string
name: string name: string
@@ -1,4 +1,5 @@
import { logWarn } from '../../utils/logger.js' import { logWarn } from '../../utils/logger.js'
import type { JsonObject } from '../../types/json.js'
import { import {
getNotificationConfig, getNotificationConfig,
listEnabledBarkRecipients, listEnabledBarkRecipients,
@@ -7,7 +8,6 @@ import {
import { sendBarkNotification } from './bark-service.js' import { sendBarkNotification } from './bark-service.js'
import { sendWpushNotification } from './wpush-service.js' import { sendWpushNotification } from './wpush-service.js'
type JsonObject = Record<string, any>
type NotificationResult = ReturnType<typeof mapNotificationResult> type NotificationResult = ReturnType<typeof mapNotificationResult>
type NotificationInput = { type NotificationInput = {
title?: string title?: string
+1 -2
View File
@@ -1,11 +1,10 @@
import crypto from 'node:crypto' import crypto from 'node:crypto'
import type { JsonObject } from '../../types/json.js'
import { createHttpError } from '../../utils/http.js' import { createHttpError } from '../../utils/http.js'
import { assertOpen91Config } from './config.js' import { assertOpen91Config } from './config.js'
import { normalizeOpen91String } from './payload.js' import { normalizeOpen91String } from './payload.js'
type JsonObject = Record<string, any>
export function buildOpen91SignSource(params: JsonObject = {}, { secret } = assertOpen91Config()) { export function buildOpen91SignSource(params: JsonObject = {}, { secret } = assertOpen91Config()) {
const entries = Object.entries(params) const entries = Object.entries(params)
.filter(([key]) => key !== 'sign') .filter(([key]) => key !== 'sign')
@@ -3,6 +3,7 @@ import { listKuaishouIndustryVouchersByOid } from '../../repositories/kuaishou-i
import { listTasksByOrderId } from '../../repositories/task-repo.js' import { listTasksByOrderId } from '../../repositories/task-repo.js'
import { resolveTaskDeliveryLink } from '../fulfillment/delivery-link-service.js' import { resolveTaskDeliveryLink } from '../fulfillment/delivery-link-service.js'
import { logIntegration } from '../../utils/logger.js' import { logIntegration } from '../../utils/logger.js'
import { asJsonObject, type JsonObject } from '../../types/json.js'
import { import {
OPEN_91_MANUAL_FAILED_STATUS, OPEN_91_MANUAL_FAILED_STATUS,
OPEN_91_PENDING_CONFIG_STATUS, OPEN_91_PENDING_CONFIG_STATUS,
@@ -32,8 +33,6 @@ import {
} from './response.js' } from './response.js'
import type { KuaishouIndustryVoucherRow, OrderRow, TaskRow } from '../../types/repository/rows.js' import type { KuaishouIndustryVoucherRow, OrderRow, TaskRow } from '../../types/repository/rows.js'
type JsonObject = Record<string, any>
export async function queryOpen91Order(payload: JsonObject = {}, { requestId = '' } = {}) { export async function queryOpen91Order(payload: JsonObject = {}, { requestId = '' } = {}) {
const config = assertOpen91Config() const config = assertOpen91Config()
const normalized = normalizeOpen91QueryPayload(payload) const normalized = normalizeOpen91QueryPayload(payload)
@@ -245,7 +244,7 @@ function parseJsonObject(value: unknown): JsonObject {
try { try {
const parsed = JSON.parse(String(value || '{}')) const parsed = JSON.parse(String(value || '{}'))
return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : {} return asJsonObject(parsed)
} catch { } catch {
return {} return {}
} }
+1 -2
View File
@@ -1,7 +1,6 @@
import { createHttpError } from '../../utils/http.js' import { createHttpError } from '../../utils/http.js'
import { assertOpen91Config } from './config.js' import { assertOpen91Config } from './config.js'
import type { JsonObject } from '../../types/json.js'
type JsonObject = Record<string, any>
export function normalizeOpen91String(value: unknown) { export function normalizeOpen91String(value: unknown) {
return String(value || '').trim() return String(value || '').trim()
@@ -1,3 +1,4 @@
import type { JsonObject } from '../../types/json.js'
import { import {
OPEN_91_DEFAULT_FAIL_CODE, OPEN_91_DEFAULT_FAIL_CODE,
OPEN_91_SUCCESS_MESSAGE, OPEN_91_SUCCESS_MESSAGE,
@@ -5,8 +6,6 @@ import {
import { isOpen91FailedTaskStatus } from '../../domain/task-status.js' import { isOpen91FailedTaskStatus } from '../../domain/task-status.js'
import { normalizeOpen91String } from './payload.js' import { normalizeOpen91String } from './payload.js'
type JsonObject = Record<string, any>
export function buildOpen91SuccessResponse(data: unknown, message = OPEN_91_SUCCESS_MESSAGE) { export function buildOpen91SuccessResponse(data: unknown, message = OPEN_91_SUCCESS_MESSAGE) {
return { return {
code: 200, code: 200,
@@ -1,3 +1,4 @@
import type { JsonObject } from '../../types/json.js'
import { import {
listCloudtentaclesSources, listCloudtentaclesSources,
} from '../platforms/cloudtentacles/source-config-service.js' } from '../platforms/cloudtentacles/source-config-service.js'
@@ -17,8 +18,6 @@ import {
} from './cloudtentacles-override-rule-service.js' } from './cloudtentacles-override-rule-service.js'
import { normalizeCloudtentaclesMatchName } from './cloudtentacles-match-utils.js' import { normalizeCloudtentaclesMatchName } from './cloudtentacles-match-utils.js'
type JsonObject = Record<string, any>
type CloudtentaclesSource = { type CloudtentaclesSource = {
key?: string key?: string
label?: string label?: string
@@ -1,4 +1,5 @@
import path from 'node:path' import path from 'node:path'
import type { JsonObject } from '../../types/json.js'
import { APP_CONFIG_KEYS } from '../../config/app-config-keys.js' import { APP_CONFIG_KEYS } from '../../config/app-config-keys.js'
import { PROJECT_ROOT } from '../../config/runtime.js' import { PROJECT_ROOT } from '../../config/runtime.js'
@@ -14,8 +15,6 @@ const CLOUDTENTACLES_OVERRIDE_RULE_FILE_PATH = path.join(
'cloudtentacles-override-rules.json', 'cloudtentacles-override-rules.json',
) )
type JsonObject = Record<string, any>
export type CloudtentaclesOverrideDeliveryItem = { export type CloudtentaclesOverrideDeliveryItem = {
cloudSkuId: number cloudSkuId: number
cloudSkuName: string cloudSkuName: string
@@ -2,22 +2,111 @@ import assert from 'node:assert/strict'
import test from 'node:test' import test from 'node:test'
import { syncDeliveryTasksForOrderWithDeps } from './delivery-task-service.js' 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<OrderRow> & Pick<OrderRow, 'id' | 'platform_order_id'>): 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<OrderItemRow> & Pick<OrderItemRow, 'id' | 'order_id' | 'sku_code'>,
): 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 () => { test('syncDeliveryTasksForOrder 多账号候选时不提前固定 cloudtentacles 账号', async () => {
const createdInputs: any[] = [] const createdInputs: CreatedTaskInput[] = []
await syncDeliveryTasksForOrderWithDeps( await syncDeliveryTasksForOrderWithDeps(
{ stubOrder({
id: 1, id: 1,
pay_status: 'pending_payment', pay_status: 'pending_payment',
provider: 'open_91',
platform: 'kuaishou',
shop_id: 'shop-1',
shop_name: '测试店铺',
platform_order_id: '2614900602069169', platform_order_id: '2614900602069169',
} as any, }),
[ [
{ stubOrderItem({
id: 10, id: 10,
order_id: 1, order_id: 1,
sku_code: 'SKU-1', sku_code: 'SKU-1',
@@ -37,9 +126,9 @@ test('syncDeliveryTasksForOrder 多账号候选时不提前固定 cloudtentacles
}, },
], ],
}, },
}, } as JsonObject,
}, }),
] as any, ],
{ {
listTasksByOrderId: async () => [], listTasksByOrderId: async () => [],
getFulfillmentProfileByKey: async () => ({ getFulfillmentProfileByKey: async () => ({
@@ -48,44 +137,33 @@ test('syncDeliveryTasksForOrder 多账号候选时不提前固定 cloudtentacles
profile_name: 'kuaishou-lewan 履约', profile_name: 'kuaishou-lewan 履约',
executor_key: 'kuaishou_ct_assisted', executor_key: 'kuaishou_ct_assisted',
}), }),
createTask: async (input: any) => { createTask: async (input) => {
createdInputs.push(input) createdInputs.push(input as CreatedTaskInput)
return { return stubCreatedTask(100, input as CreatedTaskInput)
id: 100,
order_item_id: input.orderItemId,
context_json: input.contextJson,
task_status: input.taskStatus,
executor_key: input.executorKey,
} as any
}, },
nowIso: () => '2026-05-29T09:05:46.658Z', nowIso: () => '2026-05-29T09:05:46.658Z',
randomId: () => 'DT-test', randomId: () => 'DT-test',
}, },
) )
const context = JSON.parse(createdInputs[0].contextJson) const context = JSON.parse(String(createdInputs[0]?.contextJson || '{}')) as JsonObject
assert.deepEqual(context.kuaishouCloudFulfillment.binding.cloudSourceKeys, [ const fulfillment = context.kuaishouCloudFulfillment as JsonObject
'account-a', const binding = fulfillment.binding as JsonObject
'account-b', assert.deepEqual(binding.cloudSourceKeys, ['account-a', 'account-b'])
]) assert.equal(binding.resolvedSourceKey, '')
assert.equal(context.kuaishouCloudFulfillment.binding.resolvedSourceKey, '')
}) })
test('syncDeliveryTasksForOrder 单账号候选时保留固定 cloudtentacles 账号', async () => { test('syncDeliveryTasksForOrder 单账号候选时保留固定 cloudtentacles 账号', async () => {
const createdInputs: any[] = [] const createdInputs: CreatedTaskInput[] = []
await syncDeliveryTasksForOrderWithDeps( await syncDeliveryTasksForOrderWithDeps(
{ stubOrder({
id: 2, id: 2,
pay_status: 'pending_payment', pay_status: 'pending_payment',
provider: 'open_91',
platform: 'kuaishou',
shop_id: 'shop-1',
shop_name: '测试店铺',
platform_order_id: '2614900602069170', platform_order_id: '2614900602069170',
} as any, }),
[ [
{ stubOrderItem({
id: 11, id: 11,
order_id: 2, order_id: 2,
sku_code: 'SKU-1', sku_code: 'SKU-1',
@@ -97,9 +175,9 @@ test('syncDeliveryTasksForOrder 单账号候选时保留固定 cloudtentacles
cloudSkuId: 74, cloudSkuId: 74,
cloudSkuName: '荣耀勋章礼包(30个)', cloudSkuName: '荣耀勋章礼包(30个)',
}, },
}, } as JsonObject,
}, }),
] as any, ],
{ {
listTasksByOrderId: async () => [], listTasksByOrderId: async () => [],
getFulfillmentProfileByKey: async () => ({ getFulfillmentProfileByKey: async () => ({
@@ -108,22 +186,18 @@ test('syncDeliveryTasksForOrder 单账号候选时保留固定 cloudtentacles
profile_name: 'kuaishou-lewan 履约', profile_name: 'kuaishou-lewan 履约',
executor_key: 'kuaishou_ct_assisted', executor_key: 'kuaishou_ct_assisted',
}), }),
createTask: async (input: any) => { createTask: async (input) => {
createdInputs.push(input) createdInputs.push(input as CreatedTaskInput)
return { return stubCreatedTask(101, input as CreatedTaskInput)
id: 101,
order_item_id: input.orderItemId,
context_json: input.contextJson,
task_status: input.taskStatus,
executor_key: input.executorKey,
} as any
}, },
nowIso: () => '2026-05-29T09:05:46.658Z', nowIso: () => '2026-05-29T09:05:46.658Z',
randomId: () => 'DT-test', randomId: () => 'DT-test',
}, },
) )
const context = JSON.parse(createdInputs[0].contextJson) const context = JSON.parse(String(createdInputs[0]?.contextJson || '{}')) as JsonObject
assert.deepEqual(context.kuaishouCloudFulfillment.binding.cloudSourceKeys, ['account-a']) const fulfillment = context.kuaishouCloudFulfillment as JsonObject
assert.equal(context.kuaishouCloudFulfillment.binding.resolvedSourceKey, 'account-a') const binding = fulfillment.binding as JsonObject
assert.deepEqual(binding.cloudSourceKeys, ['account-a'])
assert.equal(binding.resolvedSourceKey, 'account-a')
}) })
@@ -1,8 +1,7 @@
import { createHttpError, type HttpErrorLike } from '../../../utils/http.js' import { createHttpError, type HttpErrorLike } from '../../../utils/http.js'
import { cloudtentaclesRequest } from './http-client.js' import { cloudtentaclesRequest } from './http-client.js'
import { resolveCloudtentaclesConfig } from './helpers.js' import { resolveCloudtentaclesConfig } from './helpers.js'
import type { JsonObject } from '../../../types/json.js'
type JsonObject = Record<string, any>
export async function getCloudtentaclesAsset(payload: JsonObject = {}) { export async function getCloudtentaclesAsset(payload: JsonObject = {}) {
const token = requireToken(payload.token, 'cloudtentacles 余额查询缺少 token', 'cloudtentacles_asset_missing_token') const token = requireToken(payload.token, 'cloudtentacles 余额查询缺少 token', 'cloudtentacles_asset_missing_token')
@@ -1,10 +1,9 @@
import { createHash, constants, publicEncrypt } from 'node:crypto' import { createHash, constants, publicEncrypt } from 'node:crypto'
import type { JsonObject } from '../../../types/json.js'
import { createHttpError } from '../../../utils/http.js' import { createHttpError } from '../../../utils/http.js'
import { resolveCloudtentaclesConfig } from './helpers.js' import { resolveCloudtentaclesConfig } from './helpers.js'
type JsonObject = Record<string, any>
export function md5CloudtentaclesPassword(password: unknown) { export function md5CloudtentaclesPassword(password: unknown) {
return createHash('md5').update(String(password || ''), 'utf8').digest('hex') return createHash('md5').update(String(password || ''), 'utf8').digest('hex')
} }
@@ -1,6 +1,7 @@
import { createHttpError } from '../../../utils/http.js' import { createHttpError } from '../../../utils/http.js'
import { getCloudtentaclesAsset, listCloudtentaclesSku, buyCloudtentaclesSku } from './catalog-service.js' import { getCloudtentaclesAsset, listCloudtentaclesSku, buyCloudtentaclesSku } from './catalog-service.js'
import { getCloudtentaclesKnapsack } from './knapsack-service.js' import { getCloudtentaclesKnapsack } from './knapsack-service.js'
import type { JsonObject } from '../../../types/json.js'
import { import {
appointCloudtentaclesVirtualNumber, appointCloudtentaclesVirtualNumber,
fetchCloudtentaclesVirtualNumberCode, fetchCloudtentaclesVirtualNumberCode,
@@ -9,7 +10,6 @@ import {
verifyCloudtentaclesLoginCode, verifyCloudtentaclesLoginCode,
} from './virtual-number-service.js' } from './virtual-number-service.js'
type JsonObject = Record<string, any>
type FlowStepOptions = { type FlowStepOptions = {
retries?: number retries?: number
retryDelayMs?: number retryDelayMs?: number
@@ -1,4 +1,5 @@
import { runtimeConfig } from '../../../config/runtime.js' import { runtimeConfig } from '../../../config/runtime.js'
import type { JsonObject } from '../../../types/json.js'
import type { RuntimeConfig } from '../../../types/runtime-config.js' import type { RuntimeConfig } from '../../../types/runtime-config.js'
import { import {
@@ -9,8 +10,6 @@ import {
} from './defaults.js' } from './defaults.js'
type CloudtentaclesRuntimeConfig = RuntimeConfig['platforms']['cloudtentacles'] type CloudtentaclesRuntimeConfig = RuntimeConfig['platforms']['cloudtentacles']
type JsonObject = Record<string, any>
export function resolveCloudtentaclesConfig(overrides: Partial<CloudtentaclesRuntimeConfig> = {}) { export function resolveCloudtentaclesConfig(overrides: Partial<CloudtentaclesRuntimeConfig> = {}) {
const baseConfig = runtimeConfig.platforms?.cloudtentacles || { const baseConfig = runtimeConfig.platforms?.cloudtentacles || {
baseUrl: '', baseUrl: '',
@@ -1,12 +1,12 @@
import http from 'node:http' import http from 'node:http'
import https from 'node:https' import https from 'node:https'
import type { JsonObject } from '../../../types/json.js'
import { createHttpError } from '../../../utils/http.js' import { createHttpError } from '../../../utils/http.js'
import { logInfo, logWarn } from '../../../utils/logger.js' import { logInfo, logWarn } from '../../../utils/logger.js'
import { notifyCloudtentaclesAuthExpired } from '../../notification/domain-notifications.js' import { notifyCloudtentaclesAuthExpired } from '../../notification/domain-notifications.js'
import { buildCloudtentaclesHeaders, buildCloudtentaclesUrl, resolveCloudtentaclesConfig } from './helpers.js' import { buildCloudtentaclesHeaders, buildCloudtentaclesUrl, resolveCloudtentaclesConfig } from './helpers.js'
type JsonObject = Record<string, any>
type HeaderAdapter = { type HeaderAdapter = {
get(name: unknown): string | null get(name: unknown): string | null
} }
@@ -1,8 +1,7 @@
import { createHttpError } from '../../../utils/http.js' import { createHttpError } from '../../../utils/http.js'
import { cloudtentaclesRequest } from './http-client.js' import { cloudtentaclesRequest } from './http-client.js'
import { resolveCloudtentaclesConfig } from './helpers.js' import { resolveCloudtentaclesConfig } from './helpers.js'
import type { JsonObject } from '../../../types/json.js'
type JsonObject = Record<string, any>
export async function getCloudtentaclesKnapsack(payload: JsonObject = {}) { export async function getCloudtentaclesKnapsack(payload: JsonObject = {}) {
const token = String(payload.token || '').trim() const token = String(payload.token || '').trim()
@@ -2,8 +2,7 @@ import { createHttpError } from '../../../utils/http.js'
import { logInfo } from '../../../utils/logger.js' import { logInfo } from '../../../utils/logger.js'
import { cloudtentaclesRequest } from './http-client.js' import { cloudtentaclesRequest } from './http-client.js'
import { resolveCloudtentaclesConfig } from './helpers.js' import { resolveCloudtentaclesConfig } from './helpers.js'
import type { JsonObject } from '../../../types/json.js'
type JsonObject = Record<string, any>
export type CloudtentaclesDeliveryRecordQuery = JsonObject & { export type CloudtentaclesDeliveryRecordQuery = JsonObject & {
baseUrl?: string baseUrl?: string
@@ -3,8 +3,7 @@ import { logInfo } from '../../../utils/logger.js'
import { encryptCloudtentaclesPayload, md5CloudtentaclesPassword } from './crypto-service.js' import { encryptCloudtentaclesPayload, md5CloudtentaclesPassword } from './crypto-service.js'
import { cloudtentaclesRequest } from './http-client.js' import { cloudtentaclesRequest } from './http-client.js'
import { resolveCloudtentaclesConfig } from './helpers.js' import { resolveCloudtentaclesConfig } from './helpers.js'
import type { JsonObject } from '../../../types/json.js'
type JsonObject = Record<string, any>
export async function sendCloudtentaclesSmsCode(payload: JsonObject = {}) { export async function sendCloudtentaclesSmsCode(payload: JsonObject = {}) {
const username = String(payload.username || '').trim() const username = String(payload.username || '').trim()
@@ -1,4 +1,5 @@
import path from 'node:path' import path from 'node:path'
import type { JsonObject } from '../../../types/json.js'
import { APP_CONFIG_KEYS } from '../../../config/app-config-keys.js' import { APP_CONFIG_KEYS } from '../../../config/app-config-keys.js'
import { PROJECT_ROOT } from '../../../config/runtime.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') const CLOUDTENTACLES_SESSION_FILE_PATH = path.join(PROJECT_ROOT, 'data', 'cloudtentacles-session.json')
type JsonObject = Record<string, any>
type CloudtentaclesSessionState = ReturnType<typeof createDefaultCloudtentaclesSessionState> type CloudtentaclesSessionState = ReturnType<typeof createDefaultCloudtentaclesSessionState>
type CloudtentaclesSessionStatesFile = { type CloudtentaclesSessionStatesFile = {
sessions: Record<string, CloudtentaclesSessionState> sessions: Record<string, CloudtentaclesSessionState>
@@ -1,4 +1,5 @@
import path from 'node:path' import path from 'node:path'
import type { JsonObject } from '../../../types/json.js'
import { APP_CONFIG_KEYS } from '../../../config/app-config-keys.js' import { APP_CONFIG_KEYS } from '../../../config/app-config-keys.js'
import { PROJECT_ROOT } from '../../../config/runtime.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') const CLOUDTENTACLES_SOURCES_FILE_PATH = path.join(PROJECT_ROOT, 'data', 'cloudtentacles-sources.json')
type JsonObject = Record<string, any>
export function getCloudtentaclesSourcesFilePath() { export function getCloudtentaclesSourcesFilePath() {
return CLOUDTENTACLES_SOURCES_FILE_PATH return CLOUDTENTACLES_SOURCES_FILE_PATH
} }
@@ -1,5 +1,6 @@
import http from 'node:http' import http from 'node:http'
import https from 'node:https' import https from 'node:https'
import type { JsonObject } from '../../../types/json.js'
import { createHttpError } from '../../../utils/http.js' import { createHttpError } from '../../../utils/http.js'
import { cloudtentaclesRequest } from './http-client.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 BIND_URL_PROBE_MAX_BODY_BYTES = 64 * 1024
const AMS_SIGNATURE_EXPIRED_CODE = '99998' const AMS_SIGNATURE_EXPIRED_CODE = '99998'
type JsonObject = Record<string, any>
type HeaderAdapter = { type HeaderAdapter = {
get(name: unknown): string | null get(name: unknown): string | null
} }
@@ -1,11 +1,10 @@
import crypto from 'node:crypto' import crypto from 'node:crypto'
import { asJsonObject, type JsonObject } from '../../../types/json.js'
import { createHttpError } from '../../../utils/http.js' import { createHttpError } from '../../../utils/http.js'
import { createRequestId, logExternalHttpPacket } from '../../../utils/logger.js' import { createRequestId, logExternalHttpPacket } from '../../../utils/logger.js'
import { assertKuaishouFeifeiConfig } from './config.js' import { assertKuaishouFeifeiConfig } from './config.js'
type JsonObject = Record<string, any>
export async function kuaishouFeifeiRequest(pathname: string, payload: JsonObject = {}) { export async function kuaishouFeifeiRequest(pathname: string, payload: JsonObject = {}) {
const config = assertKuaishouFeifeiConfig() const config = assertKuaishouFeifeiConfig()
const body = JSON.stringify(payload) const body = JSON.stringify(payload)
@@ -189,7 +188,7 @@ export function isKuaishouFeifeiSuccessResponse(json: JsonObject) {
function parseJsonObject(text: string): JsonObject { function parseJsonObject(text: string): JsonObject {
try { try {
const parsed = JSON.parse(text || '{}') const parsed = JSON.parse(text || '{}')
return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : {} return asJsonObject(parsed)
} catch { } catch {
return {} return {}
} }
@@ -1,4 +1,5 @@
import crypto from 'node:crypto' import crypto from 'node:crypto'
import type { JsonObject } from '../../../types/json.js'
import { createTaskEvent } from '../../../repositories/task-event-repo.js' import { createTaskEvent } from '../../../repositories/task-event-repo.js'
import { findKuaishouFeifeiTaskByOrder } from '../../../repositories/task-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 { signKuaishouFeifeiPayload } from './http-client.js'
import { mapKuaishouFeifeiOrder } from './order-service.js' import { mapKuaishouFeifeiOrder } from './order-service.js'
type JsonObject = Record<string, any>
type NotifyHeaders = Record<string, string | string[] | undefined> type NotifyHeaders = Record<string, string | string[] | undefined>
export async function handleKuaishouFeifeiNotify(input: { export async function handleKuaishouFeifeiNotify(input: {
@@ -1,7 +1,6 @@
import { getKuaishouFeifeiConfig } from './config.js' import { getKuaishouFeifeiConfig } from './config.js'
import { kuaishouFeifeiRequest } from './http-client.js' import { kuaishouFeifeiRequest } from './http-client.js'
import type { JsonObject } from '../../../types/json.js'
type JsonObject = Record<string, any>
export type KuaishouFeifeiProductListResult = ReturnType<typeof mapKuaishouFeifeiProductList> export type KuaishouFeifeiProductListResult = ReturnType<typeof mapKuaishouFeifeiProductList>
@@ -1,4 +1,5 @@
import path from 'node:path' import path from 'node:path'
import type { JsonObject } from '../../../types/json.js'
import { APP_CONFIG_KEYS } from '../../../config/app-config-keys.js' import { APP_CONFIG_KEYS } from '../../../config/app-config-keys.js'
import { PROJECT_ROOT } from '../../../config/runtime.js' import { PROJECT_ROOT } from '../../../config/runtime.js'
@@ -16,8 +17,6 @@ const KUAISHOU_FEIFEI_CONFIG_FILE_PATH = path.join(
'kuaishou-feifei-config.json', 'kuaishou-feifei-config.json',
) )
type JsonObject = Record<string, any>
export type KuaishouFeifeiSourceConfig = { export type KuaishouFeifeiSourceConfig = {
enabled: boolean enabled: boolean
baseUrl: string baseUrl: string
@@ -3,6 +3,7 @@ import { findKuaishouIndustryVoucherByCode } from '../../../repositories/kuaisho
import { normalizeTimestampIso } from '../../../utils/time.js' import { normalizeTimestampIso } from '../../../utils/time.js'
import { logWarn } from '../../../utils/logger.js' import { logWarn } from '../../../utils/logger.js'
import { TASK_STATUS } from '../../../domain/task-status.js' import { TASK_STATUS } from '../../../domain/task-status.js'
import type { JsonObject } from '../../../types/json.js'
import { import {
getKuaishouIndustryConfig, getKuaishouIndustryConfig,
assertMatchingAppKey, assertMatchingAppKey,
@@ -17,8 +18,6 @@ import { consumeCallback } from './consume-callback-service.js'
import { consumeKuaishouIndustryVoucher } from './voucher-service.js' import { consumeKuaishouIndustryVoucher } from './voucher-service.js'
import { attachKuaishouIndustryVoucherToTask } from './voucher-binding-service.js' import { attachKuaishouIndustryVoucherToTask } from './voucher-binding-service.js'
type JsonObject = Record<string, any>
export async function handleConsumeCode(rawBody: JsonObject = {}) { export async function handleConsumeCode(rawBody: JsonObject = {}) {
const config = getKuaishouIndustryConfig() const config = getKuaishouIndustryConfig()
@@ -1,11 +1,10 @@
import crypto from 'node:crypto' import crypto from 'node:crypto'
import type { JsonObject } from '../../../types/json.js'
import { createHttpError } from '../../../utils/http.js' import { createHttpError } from '../../../utils/http.js'
import { logWarn } from '../../../utils/logger.js' import { logWarn } from '../../../utils/logger.js'
import { assertKuaishouIndustryConfig } from './config.js' import { assertKuaishouIndustryConfig } from './config.js'
type JsonObject = Record<string, any>
export type SignMethod = 'MD5' | 'HMAC_SHA256' export type SignMethod = 'MD5' | 'HMAC_SHA256'
export function buildKuaishouIndustrySignSource(params: JsonObject = {}, { signSecret } = assertKuaishouIndustryConfig()) { export function buildKuaishouIndustrySignSource(params: JsonObject = {}, { signSecret } = assertKuaishouIndustryConfig()) {
@@ -1,4 +1,5 @@
import { updateTask } from '../../../repositories/task-repo.js' import { updateTask } from '../../../repositories/task-repo.js'
import type { JsonObject } from '../../../types/json.js'
import { import {
listKuaishouIndustryVouchersByOid, listKuaishouIndustryVouchersByOid,
updateKuaishouIndustryVoucherByCode, updateKuaishouIndustryVoucherByCode,
@@ -18,8 +19,6 @@ import {
import { destroyCallback } from './destroy-callback-service.js' import { destroyCallback } from './destroy-callback-service.js'
import { attachKuaishouIndustryVoucherToTask } from './voucher-binding-service.js' import { attachKuaishouIndustryVoucherToTask } from './voucher-binding-service.js'
type JsonObject = Record<string, any>
export async function handleDestroyCode(rawBody: JsonObject = {}) { export async function handleDestroyCode(rawBody: JsonObject = {}) {
const config = getKuaishouIndustryConfig() const config = getKuaishouIndustryConfig()
@@ -2,8 +2,8 @@ import { getKuaishouIndustryConfig } from './config.js'
import { signKuaishouIndustryPayload, type SignMethod } from './crypto.js' import { signKuaishouIndustryPayload, type SignMethod } from './crypto.js'
import { ensureKuaishouIndustryAccessToken } from './token-service.js' import { ensureKuaishouIndustryAccessToken } from './token-service.js'
import { logIntegration } from '../../../utils/logger.js' import { logIntegration } from '../../../utils/logger.js'
import type { JsonObject } from '../../../types/json.js'
export type JsonObject = Record<string, any> export type { JsonObject }
type KuaishouIndustryOpenApiCallInput = { type KuaishouIndustryOpenApiCallInput = {
apiMethod: string apiMethod: string
@@ -1,5 +1,5 @@
export type JsonObject = Record<string, any> import type { JsonObject } from '../../../types/json.js'
export type { JsonObject }
export function pickDefinedBizParams(input: JsonObject): JsonObject { export function pickDefinedBizParams(input: JsonObject): JsonObject {
const output: JsonObject = {} const output: JsonObject = {}
@@ -1,7 +1,6 @@
import { createHttpError } from '../../../utils/http.js' import { createHttpError } from '../../../utils/http.js'
import { assertKuaishouIndustryConfig } from './config.js' import { assertKuaishouIndustryConfig } from './config.js'
import { asJsonObject, type JsonObject } from '../../../types/json.js'
type JsonObject = Record<string, any>
export function normalizeIndustryString(value: unknown) { export function normalizeIndustryString(value: unknown) {
return String(value || '').trim() return String(value || '').trim()
@@ -239,7 +238,7 @@ function parseParamField(raw: JsonObject) {
try { try {
const parsed = JSON.parse(normalizeIndustryString(raw.param || '{}')) const parsed = JSON.parse(normalizeIndustryString(raw.param || '{}'))
return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : {} return asJsonObject(parsed)
} catch { } catch {
return {} return {}
} }
@@ -1,3 +1,4 @@
import type { JsonObject } from '../../../types/json.js'
import { import {
findKuaishouIndustryVoucherByCode, findKuaishouIndustryVoucherByCode,
listKuaishouIndustryVouchersByOid, listKuaishouIndustryVouchersByOid,
@@ -18,8 +19,6 @@ import {
isKuaishouIndustryVoucherSendCallbackSuccess, isKuaishouIndustryVoucherSendCallbackSuccess,
} from './voucher-service.js' } from './voucher-service.js'
type JsonObject = Record<string, any>
export async function handleQueryCode(rawBody: JsonObject = {}) { export async function handleQueryCode(rawBody: JsonObject = {}) {
const config = getKuaishouIndustryConfig() const config = getKuaishouIndustryConfig()
@@ -1,5 +1,4 @@
type JsonObject = Record<string, any> import type { JsonObject } from '../../../types/json.js'
export function buildIndustrySuccessResponse(data: unknown = null) { export function buildIndustrySuccessResponse(data: unknown = null) {
return { return {
result: 1, result: 1,
@@ -1,5 +1,6 @@
import { findLatestOrderByPlatformOrderId } from '../../../repositories/order-repo.js' import { findLatestOrderByPlatformOrderId } from '../../../repositories/order-repo.js'
import { listTasksByOrderId } from '../../../repositories/task-repo.js' import { listTasksByOrderId } from '../../../repositories/task-repo.js'
import { asJsonObject, type JsonObject } from '../../../types/json.js'
import { import {
findKuaishouIndustryVoucherByCode, findKuaishouIndustryVoucherByCode,
listKuaishouIndustryVouchersByOid, listKuaishouIndustryVouchersByOid,
@@ -33,8 +34,6 @@ import { bindKuaishouIndustryVouchersToOrderTasks } from './voucher-binding-serv
import { parseAmountToFen } from '../../../utils/money.js' import { parseAmountToFen } from '../../../utils/money.js'
import type { KuaishouIndustryVoucherRow, OrderRow } from '../../../types/repository/rows.js' import type { KuaishouIndustryVoucherRow, OrderRow } from '../../../types/repository/rows.js'
type JsonObject = Record<string, any>
type SendCodeCallbackParams = { type SendCodeCallbackParams = {
oid: string oid: string
sendType: string sendType: string
@@ -677,7 +676,7 @@ function parseJsonObject(value: unknown): JsonObject {
try { try {
const parsed = JSON.parse(String(value || '{}')) const parsed = JSON.parse(String(value || '{}'))
return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : {} return asJsonObject(parsed)
} catch { } catch {
return {} return {}
} }
@@ -1,4 +1,5 @@
import path from 'node:path' import path from 'node:path'
import type { JsonObject } from '../../../types/json.js'
import { APP_CONFIG_KEYS } from '../../../config/app-config-keys.js' import { APP_CONFIG_KEYS } from '../../../config/app-config-keys.js'
import { PROJECT_ROOT, runtimeConfig } from '../../../config/runtime.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_REDIRECT_URI = 'https://ks.khhao.com/admin/platform-shops?tab=kuaishouIndustry'
const DEFAULT_SCOPES = 'merchant_item' const DEFAULT_SCOPES = 'merchant_item'
type JsonObject = Record<string, any>
export type KuaishouIndustryShopConfig = { export type KuaishouIndustryShopConfig = {
enabled: boolean enabled: boolean
sellerId: string sellerId: string
@@ -1,5 +1,6 @@
import { createHttpError } from '../../../utils/http.js' import { createHttpError } from '../../../utils/http.js'
import { logInfo, logWarn } from '../../../utils/logger.js' import { logInfo, logWarn } from '../../../utils/logger.js'
import type { JsonObject } from '../../../types/json.js'
import { import {
findKuaishouIndustryShopConfig, findKuaishouIndustryShopConfig,
getKuaishouIndustrySourceConfig, getKuaishouIndustrySourceConfig,
@@ -10,8 +11,6 @@ import {
type KuaishouIndustrySourceConfig, type KuaishouIndustrySourceConfig,
} from './source-config-service.js' } from './source-config-service.js'
type JsonObject = Record<string, any>
const ACCESS_TOKEN_REFRESH_MARGIN_MS = 30 * 60 * 1000 const ACCESS_TOKEN_REFRESH_MARGIN_MS = 30 * 60 * 1000
const DEFAULT_ACCESS_TOKEN_TTL_MS = 47 * 60 * 60 * 1000 const DEFAULT_ACCESS_TOKEN_TTL_MS = 47 * 60 * 60 * 1000
const DEFAULT_REFRESH_TOKEN_TTL_MS = 179 * 24 * 60 * 60 * 1000 const DEFAULT_REFRESH_TOKEN_TTL_MS = 179 * 24 * 60 * 60 * 1000
@@ -1,4 +1,5 @@
import { updateTask } from '../../../repositories/task-repo.js' import { updateTask } from '../../../repositories/task-repo.js'
import type { JsonObject } from '../../../types/json.js'
import { import {
listKuaishouIndustryVouchersByOid, listKuaishouIndustryVouchersByOid,
updateKuaishouIndustryVoucherByCode, updateKuaishouIndustryVoucherByCode,
@@ -17,8 +18,6 @@ import type {
TaskRow, TaskRow,
} from '../../../types/repository/rows.js' } from '../../../types/repository/rows.js'
type JsonObject = Record<string, any>
export async function bindKuaishouIndustryVouchersToOrderTasks( export async function bindKuaishouIndustryVouchersToOrderTasks(
order: OrderRow | null | undefined, order: OrderRow | null | undefined,
tasks: TaskRow[] = [], tasks: TaskRow[] = [],
@@ -1,4 +1,5 @@
import { createTaskEvent } from '../../../repositories/task-event-repo.js' import { createTaskEvent } from '../../../repositories/task-event-repo.js'
import { asJsonObject, type JsonObject } from '../../../types/json.js'
import { import {
findKuaishouIndustryVoucherByCode, findKuaishouIndustryVoucherByCode,
listKuaishouIndustryVouchersByTaskId, listKuaishouIndustryVouchersByTaskId,
@@ -9,8 +10,6 @@ import { buildIndustryEticketItem } from './response.js'
import { consumeCallback } from './consume-callback-service.js' import { consumeCallback } from './consume-callback-service.js'
import type { KuaishouIndustryVoucherRow, TaskRow } from '../../../types/repository/rows.js' import type { KuaishouIndustryVoucherRow, TaskRow } from '../../../types/repository/rows.js'
type JsonObject = Record<string, any>
export const KUAISHOU_INDUSTRY_DEFAULT_VALID_DAYS = 3 export const KUAISHOU_INDUSTRY_DEFAULT_VALID_DAYS = 3
export const KUAISHOU_INDUSTRY_SEND_CALLBACK_STATUS = { export const KUAISHOU_INDUSTRY_SEND_CALLBACK_STATUS = {
PENDING: 'pending', PENDING: 'pending',
@@ -358,7 +357,7 @@ function parseJsonObject(value: unknown): JsonObject {
try { try {
const parsed = JSON.parse(String(value || '{}')) const parsed = JSON.parse(String(value || '{}'))
return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : {} return asJsonObject(parsed)
} catch { } catch {
return {} return {}
} }
@@ -4,6 +4,7 @@ import { listOrderItemsByOrderId, replaceOrderItems } from '../../../repositorie
import { listTasksByOrderId } from '../../../repositories/task-repo.js' import { listTasksByOrderId } from '../../../repositories/task-repo.js'
import { upsertOrderFromSource } from '../../order/order-service.js' import { upsertOrderFromSource } from '../../order/order-service.js'
import { bindKuaishouIndustryVouchersToOrderTasks } from '../kuaishou-industry/voucher-binding-service.js' import { bindKuaishouIndustryVouchersToOrderTasks } from '../kuaishou-industry/voucher-binding-service.js'
import { asJsonObject, type JsonObject } from '../../../types/json.js'
import { import {
OPEN_91_PLATFORM, OPEN_91_PLATFORM,
OPEN_91_PROVIDER, 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_PENDING_CONFIG_STATUS = 'pending_config'
export const OPEN_91_MANUAL_FAILED_STATUS = 'manual_failed' export const OPEN_91_MANUAL_FAILED_STATUS = 'manual_failed'
type JsonObject = Record<string, any>
export function buildOpen91SourceEvent(payload: JsonObject = {}, config: JsonObject = {}) { export function buildOpen91SourceEvent(payload: JsonObject = {}, config: JsonObject = {}) {
const normalized = normalizeOpen91CreatePayload(payload) const normalized = normalizeOpen91CreatePayload(payload)
const productInfo = parseOpen91ProductNo(normalized.productNo) const productInfo = parseOpen91ProductNo(normalized.productNo)
@@ -397,7 +396,7 @@ function parseJsonObject(value: unknown): JsonObject {
try { try {
const parsed = JSON.parse(String(value || '{}')) const parsed = JSON.parse(String(value || '{}'))
return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : {} return asJsonObject(parsed)
} catch { } catch {
return {} return {}
} }
@@ -1,4 +1,5 @@
import { getCloudtentaclesAsset } from '../platforms/cloudtentacles/catalog-service.js' import { getCloudtentaclesAsset } from '../platforms/cloudtentacles/catalog-service.js'
import type { JsonObject } from '../../types/json.js'
import { import {
normalizeCloudtentaclesDeviceId, normalizeCloudtentaclesDeviceId,
normalizeCloudtentaclesDeviceType, normalizeCloudtentaclesDeviceType,
@@ -13,7 +14,6 @@ import {
notifyCloudtentaclesAuthExpired, notifyCloudtentaclesAuthExpired,
} from '../notification/domain-notifications.js' } from '../notification/domain-notifications.js'
type JsonObject = Record<string, any>
type CloudtentaclesHealthAccountResult = { type CloudtentaclesHealthAccountResult = {
sourceKey: string sourceKey: string
label: string label: string
@@ -1,4 +1,5 @@
import path from 'node:path' import path from 'node:path'
import type { JsonObject } from '../../types/json.js'
import { APP_CONFIG_KEYS } from '../../config/app-config-keys.js' import { APP_CONFIG_KEYS } from '../../config/app-config-keys.js'
import { PROJECT_ROOT } from '../../config/runtime.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 SCHEDULED_JOBS_FILE_PATH = path.join(PROJECT_ROOT, 'data', 'scheduled-jobs.json')
const CLOUDTENTACLES_HEALTH_JOB_ID = 'cloudtentacles-health' const CLOUDTENTACLES_HEALTH_JOB_ID = 'cloudtentacles-health'
type JsonObject = Record<string, any>
export function getScheduledJobsFilePath() { export function getScheduledJobsFilePath() {
return SCHEDULED_JOBS_FILE_PATH return SCHEDULED_JOBS_FILE_PATH
} }
@@ -1,13 +1,12 @@
import { logError, logInfo, logWarn } from '../../utils/logger.js' import { logError, logInfo, logWarn } from '../../utils/logger.js'
import { createHttpError } from '../../utils/http.js' import { createHttpError } from '../../utils/http.js'
import type { JsonObject } from '../../types/json.js'
import { import {
getCloudtentaclesHealthJob, getCloudtentaclesHealthJob,
getScheduledJobsConfig, getScheduledJobsConfig,
} from './config-service.js' } from './config-service.js'
import { runCloudtentaclesHealthJob } from './cloudtentacles-health-job.js' import { runCloudtentaclesHealthJob } from './cloudtentacles-health-job.js'
type JsonObject = Record<string, any>
const timers = new Map<string, NodeJS.Timeout>() const timers = new Map<string, NodeJS.Timeout>()
const jobStates = new Map<string, JsonObject>() const jobStates = new Map<string, JsonObject>()
@@ -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
}
+41 -1
View File
@@ -1 +1,41 @@
export type JsonRecord = Record<string, any> /**
* 项目统一的 JSON 结构类型(中心定义,避免各处复制 Record<string, any>)。
*
* 渐进策略:
* - JsonObject:当前仍用 any 值以兼容深层 context 访问(.a.b.c
* - 新域模型请用 TaskContext / 明确 interface,少用 JsonObject
* - 后续可把热点路径迁到 StrictJsonObjectunknown 值)
*/
/** 严格 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<string, any>
/** @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]
}
+26
View File
@@ -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
}
+2 -1
View File
@@ -6,6 +6,7 @@ import util from 'node:util'
import { PROJECT_ROOT, runtimeConfig } from '../config/runtime.js' import { PROJECT_ROOT, runtimeConfig } from '../config/runtime.js'
import type { HttpErrorLike } from './http.js' import type { HttpErrorLike } from './http.js'
import { maskSecret } from './masking.js' import { maskSecret } from './masking.js'
import { asJsonObject } from '../types/json.js'
type LogLevel = 'debug' | 'info' | 'warn' | 'error' type LogLevel = 'debug' | 'info' | 'warn' | 'error'
type LogChannel = 'app' | 'integration' type LogChannel = 'app' | 'integration'
@@ -362,7 +363,7 @@ function isSensitiveLogKey(key: unknown): boolean {
function normalizeExternalHttpPacketDetail( function normalizeExternalHttpPacketDetail(
detail: ExternalHttpPacketDetail, detail: ExternalHttpPacketDetail,
): ExternalHttpPacketDetail { ): ExternalHttpPacketDetail {
const source = detail && typeof detail === 'object' && !Array.isArray(detail) ? detail : {} const source = asJsonObject(detail)
const normalized: ExternalHttpPacketDetail = {} const normalized: ExternalHttpPacketDetail = {}
for (const [key, value] of Object.entries(source)) { for (const [key, value] of Object.entries(source)) {
+17 -9
View File
@@ -1,26 +1,34 @@
export type JsonRecord = Record<string, any> 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) { if (!value) {
return {} return {}
} }
if (typeof value === 'object' && !Array.isArray(value)) { if (isPlainObject(value)) {
return value as JsonRecord return value
} }
try { try {
const parsed = JSON.parse(String(value || '{}')) const parsed = JSON.parse(String(value || '{}')) as unknown
return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : {} return asJsonObject(parsed)
} catch { } catch {
return {} return {}
} }
} }
export function parseTaskContext(task: { context_json?: unknown } | null | undefined): JsonRecord { export function parseTaskContext(
return parseJsonObject(task?.context_json) 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) return parseJsonObject(task?.state_json)
} }
+2 -4
View File
@@ -3,8 +3,8 @@
"target": "ES2022", "target": "ES2022",
"module": "NodeNext", "module": "NodeNext",
"moduleResolution": "NodeNext", "moduleResolution": "NodeNext",
"allowJs": true, "allowJs": false,
"checkJs": true, "checkJs": false,
"noEmit": true, "noEmit": true,
"strict": true, "strict": true,
"noUncheckedIndexedAccess": true, "noUncheckedIndexedAccess": true,
@@ -14,13 +14,11 @@
"lib": ["ES2022"] "lib": ["ES2022"]
}, },
"include": [ "include": [
"src/**/*.js",
"src/**/*.ts" "src/**/*.ts"
], ],
"exclude": [ "exclude": [
"node_modules", "node_modules",
"data", "data",
"src/**/*.test.js",
"src/**/*.test.ts" "src/**/*.test.ts"
] ]
} }
@@ -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<string, any>`
4. **TaskContext**`types/task-context.ts``parseTaskContext` 返回 `TaskContext`
5. **Admin 任务详情契约**`types/admin/task-detail.ts``getAdminTaskDetail(): Promise<AdminTaskDetailView>`
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<string, any>`