From 17a615b8ded27e22cf709db252d9a99a2d0c48d2 Mon Sep 17 00:00:00 2001 From: yml Date: Thu, 21 May 2026 18:50:07 +0800 Subject: [PATCH] deduplicate task parsing and masking helpers --- .../admin/admin-read-shared-helpers.ts | 36 +- .../services/admin/platform-config/mappers.ts | 27 +- .../admin/write/kuaishou-cloud-helpers.ts | 383 ++---------------- .../src/services/admin/write/shared.ts | 26 +- .../src/services/claim/session/shared.ts | 49 +-- .../fulfillment/kuaishou-cloud/domain.ts | 26 +- .../kuaishou-cloud/task-context.ts | 18 +- apps/backend/src/utils/masking.ts | 51 +++ apps/backend/src/utils/task-json.ts | 26 ++ 9 files changed, 151 insertions(+), 491 deletions(-) create mode 100644 apps/backend/src/utils/masking.ts create mode 100644 apps/backend/src/utils/task-json.ts 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 2ccc34db..31b8ed09 100644 --- a/apps/backend/src/services/admin/admin-read-shared-helpers.ts +++ b/apps/backend/src/services/admin/admin-read-shared-helpers.ts @@ -1,6 +1,10 @@ import { getAgisoShopConfig } from '../platforms/agiso/shop-config-service.js' import { safeParseJson } from './admin-query-utils.js' import { normalizeAdminRole } from './admin-auth-service.js' +import { + parseTaskContext as parseTaskContextValue, + parseTaskState as parseTaskStateValue, +} from '../../utils/task-json.js' import type { AdminViewerSessionInput } from '../../types/admin-read-inputs.js' import type { OrderItemRow, TaskRow } from '../../types/repository-rows.js' @@ -227,39 +231,11 @@ export function resolveDisplayShopName(provider: unknown, shopId: unknown, shopN } export function parseTaskContext(task: TaskLike | null | undefined): JsonRecord { - const value = task?.context_json - - if (!value) { - return {} - } - - if (typeof value === 'object') { - return value - } - - try { - return JSON.parse(String(value || '{}')) - } catch { - return {} - } + return parseTaskContextValue(task) } export function parseTaskState(task: TaskLike | null | undefined): JsonRecord { - const value = task?.state_json - - if (!value) { - return {} - } - - if (typeof value === 'object') { - return value - } - - try { - return JSON.parse(String(value || '{}')) - } catch { - return {} - } + return parseTaskStateValue(task) } export function resolveOrderItemTitle(item: Partial | null | undefined): string { diff --git a/apps/backend/src/services/admin/platform-config/mappers.ts b/apps/backend/src/services/admin/platform-config/mappers.ts index d4fcda74..c07b1313 100644 --- a/apps/backend/src/services/admin/platform-config/mappers.ts +++ b/apps/backend/src/services/admin/platform-config/mappers.ts @@ -1,29 +1,16 @@ +import { + maskPhone as maskPhoneValue, + maskSecret as maskSecretValue, +} from "../../../utils/masking.js"; + type JsonObject = Record export function maskSecret(value: unknown) { - const normalized = String(value || "").trim(); - if (!normalized) { - return ""; - } - - if (normalized.length <= 10) { - return `${normalized.slice(0, 2)}****${normalized.slice(-2)}`; - } - - return `${normalized.slice(0, 6)}****${normalized.slice(-6)}`; + return maskSecretValue(value); } export function maskPhone(value: unknown) { - const normalized = String(value || "").trim(); - if (!normalized) { - return ""; - } - - if (normalized.length < 7) { - return normalized; - } - - return `${normalized.slice(0, 3)}****${normalized.slice(-4)}`; + return maskPhoneValue(value, { maskShort: false }); } export function mapAdminKuaishouEticketShopItem(item: 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 ea87ef94..1400f423 100644 --- a/apps/backend/src/services/admin/write/kuaishou-cloud-helpers.ts +++ b/apps/backend/src/services/admin/write/kuaishou-cloud-helpers.ts @@ -1,20 +1,22 @@ -import { normalizeProductName } from '../../order/product-match-service.js' -import { getCloudtentaclesSourceConfig } from '../../platforms/cloudtentacles/source-config-service.js' -import { getCloudtentaclesSessionState } from '../../platforms/cloudtentacles/session-state-service.js' -import { resolveCloudtentaclesConfig } from '../../platforms/cloudtentacles/shared.js' -import { - appointCloudtentaclesVirtualNumber, - backCloudtentaclesVirtualNumber, - fetchCloudtentaclesVirtualNumberCode, - generateCloudtentaclesLoginCode, - getCloudtentaclesBindUrl, - verifyCloudtentaclesLoginCode, -} from '../../platforms/cloudtentacles/virtual-number-service.js' import { createHttpError } from '../../../utils/http.js' +import type { HttpErrorLike } from '../../../utils/http.js' +import { + resolvePersistedCloudtentaclesContext as resolveFulfillmentPersistedCloudtentaclesContext, +} from '../../fulfillment/kuaishou-cloud/cloudtentacles-context.js' -export const KUAISHOU_CLOUD_FIXED_VN_KEY = '1' - -export type JsonObject = Record +export { + KUAISHOU_CLOUD_FIXED_VN_KEY, + isKuaishouCloudTask, + normalizeKuaishouCloudFlow, + normalizeKuaishouCloudRoleInfo, + resolveKuaishouCloudBindUrlExpiresAt, + type JsonObject, +} from '../../fulfillment/kuaishou-cloud/domain.js' +export { + prepareKuaishouCloudBindResourceWithFallback, + resolveKuaishouCloudBindingResources, + resolveKuaishouCloudVnKeyCandidates, +} from '../../fulfillment/kuaishou-cloud/binding-resources.js' export type CloudtentaclesContext = { baseUrl: string @@ -31,17 +33,17 @@ export type CloudSkuLikeItem = { [key: string]: unknown } -export type KuaishouCloudFlow = JsonObject & { +export type KuaishouCloudFlow = Record & { configId: string internalSkuCode: string internalSkuName: string - ticket: JsonObject - binding: JsonObject - role: JsonObject - purchase: JsonObject - dispatch: JsonObject - returnNumber: JsonObject - consume: JsonObject + ticket: Record + binding: Record + role: Record + purchase: Record + dispatch: Record + returnNumber: Record + consume: Record } export type KuaishouCloudBindingResources = { @@ -60,332 +62,23 @@ export type PreparedKuaishouCloudBindResource = { bindUrl: string } -export function isKuaishouCloudTask(task: unknown): boolean { - const record: JsonObject = task && typeof task === 'object' ? task : {} - return String(record.executor_key || '').trim() === 'kuaishou_ct_assisted' -} - export function resolvePersistedCloudtentaclesContext(): CloudtentaclesContext { - const source = getCloudtentaclesSourceConfig() - const session = getCloudtentaclesSessionState() - const token = String(session.token || '').trim() - - if (!token) { - throw createHttpError('当前 cloudtentacles 没有可用 token,请先到平台配置完成登录校验', { - statusCode: 409, - errorCode: 'admin_task_kuaishou_cloud_missing_cloud_token', - }) - } - - return { - baseUrl: String(session.baseUrl || source.baseUrl || '').trim() || 'https://123.207.217.176', - token, - deviceId: String(session.deviceId || source.deviceId || '-').trim() || '-', - deviceType: Number(session.deviceType ?? source.deviceType ?? 0), - } -} - -export function normalizeKuaishouCloudFlow(value: unknown): KuaishouCloudFlow { - const source: JsonObject = value && typeof value === 'object' ? value : {} - const binding = source.binding && typeof source.binding === 'object' ? source.binding : {} - const role = source.role && typeof source.role === 'object' ? source.role : {} - const purchase = source.purchase && typeof source.purchase === 'object' ? source.purchase : {} - 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 ticket = source.ticket && typeof source.ticket === 'object' ? source.ticket : {} - const bindPreparedAt = binding.bindPreparedAt || null - - return { - ...source, - configId: String(source.configId || '').trim(), - internalSkuCode: String(source.internalSkuCode || '').trim(), - internalSkuName: String(source.internalSkuName || '').trim(), - ticket: { - code: String(ticket.code || '').trim(), - status: String(ticket.status || 'pending').trim() || 'pending', - capturedAt: ticket.capturedAt || null, - capturedBy: ticket.capturedBy || null, - verifiedAt: ticket.verifiedAt || null, - oid: String(ticket.oid || '').trim(), - formToken: String(ticket.formToken || '').trim(), - leftCount: Number(ticket.leftCount || 0) || 0, - goodsTitle: String(ticket.goodsTitle || '').trim(), - }, - binding: { - prepareStatus: String(binding.prepareStatus || 'pending').trim() || 'pending', - cloudSourceKey: String(binding.cloudSourceKey || 'default').trim() || 'default', - skuId: Number(binding.skuId || 0) || 0, - skuName: String(binding.skuName || '').trim(), - vnKey: String(binding.vnKey || '').trim(), - vnId: Number(binding.vnId || 0) || 0, - vnPhone: String(binding.vnPhone || '').trim(), - bindUrl: String(binding.bindUrl || '').trim(), - bindPreparedAt, - bindExpiresAt: binding.bindExpiresAt || resolveKuaishouCloudBindUrlExpiresAt(bindPreparedAt), - bindProbeAt: binding.bindProbeAt || null, - bindProbeStatus: String(binding.bindProbeStatus || '').trim(), - bindProbeMessage: String(binding.bindProbeMessage || '').trim(), - roleName: String(binding.roleName || '').trim(), - roleId: String(binding.roleId || '').trim(), - }, - role: { - status: String(role.status || 'pending').trim() || 'pending', - name: String(role.name || '').trim(), - rid: String(role.rid || '').trim(), - refreshedAt: role.refreshedAt || null, - errorMessage: String(role.errorMessage || '').trim(), - rawInfo: role.rawInfo && typeof role.rawInfo === 'object' ? role.rawInfo : null, - }, - purchase: { - autoBuyEnabled: purchase.autoBuyEnabled !== false, - minAssetReserve: Number(purchase.minAssetReserve || 0) || 0, - usedKnapsack: purchase.usedKnapsack === true, - purchaseTriggered: purchase.purchaseTriggered === true, - assetBefore: Number(purchase.assetBefore || 0) || 0, - assetAfter: Number(purchase.assetAfter || 0) || 0, - purchaseAt: purchase.purchaseAt || null, - }, - dispatch: { - status: String(dispatch.status || 'pending').trim() || 'pending', - dispatchAt: dispatch.dispatchAt || null, - dispatchBy: dispatch.dispatchBy || null, - sendType: Number(dispatch.sendType || 0) || 0, - note: String(dispatch.note || '').trim(), - }, - returnNumber: { - status: String(returnNumber.status || 'pending').trim() || 'pending', - returnedAt: returnNumber.returnedAt || null, - returnedBy: returnNumber.returnedBy || null, - autoReturnEnabled: returnNumber.autoReturnEnabled === true, - }, - consume: { - status: String(consume.status || 'pending').trim() || 'pending', - shopId: String(consume.shopId || '').trim(), - shopName: String(consume.shopName || '').trim(), - autoConsumeEnabled: consume.autoConsumeEnabled === true, - consumedAt: consume.consumedAt || null, - errorMessage: String(consume.errorMessage || '').trim(), - }, - } -} - -export function normalizeKuaishouCloudRoleInfo(value: unknown): { name: string, rid: string, rawInfo: JsonObject | null } { - const rawInfo: JsonObject | null = value && typeof value === 'object' ? value : null - const nestedBindInfo = rawInfo?.sBindInfo && typeof rawInfo.sBindInfo === 'object' ? rawInfo.sBindInfo : null - const source = nestedBindInfo || rawInfo - - return { - name: String(source?.name || source?.roleName || source?.nickname || source?.sRoleName || '').trim(), - rid: String(source?.rid || source?.roleId || source?.uid || source?.sRoleId || source?.sUserId || '').trim(), - rawInfo, - } -} - -export function resolveKuaishouCloudBindUrlExpiresAt(preparedAt: unknown): string | null { - const preparedTime = Date.parse(String(preparedAt || '')) - if (!Number.isFinite(preparedTime)) { - return null - } - - const config = resolveCloudtentaclesConfig() - const ttlSeconds = Number(config.bindUrlTtlSeconds || 600) - return new Date(preparedTime + Math.max(1, ttlSeconds) * 1000).toISOString() -} - -export function resolveKuaishouCloudBindingResources( - flow: KuaishouCloudFlow, - { skuItems = [], knapsackItems = [] }: { skuItems?: unknown[], knapsackItems?: unknown[] } = {}, -): KuaishouCloudBindingResources { - const normalizedSkuItems = Array.isArray(skuItems) ? skuItems.filter(isCloudSkuLikeItem) : [] - const normalizedKnapsackItems = Array.isArray(knapsackItems) ? knapsackItems.filter(isCloudSkuLikeItem) : [] - const currentSkuId = Number(flow?.binding?.skuId || 0) || 0 - const currentSkuName = String(flow?.binding?.skuName || '').trim() - const nameCandidates = collectKuaishouCloudNameCandidates(flow) - - const skuItemById = currentSkuId > 0 - ? normalizedSkuItems.find((item) => Number(item.id || 0) === currentSkuId) || null - : null - const knapsackItemById = currentSkuId > 0 - ? normalizedKnapsackItems.find((item) => Number(item.id || 0) === currentSkuId) || null - : null - - if (skuItemById || knapsackItemById) { - const matchedItem = skuItemById || knapsackItemById - return { - skuId: Number(matchedItem?.id || 0) || 0, - skuName: String(currentSkuName || matchedItem?.name || '').trim(), - vnKey: KUAISHOU_CLOUD_FIXED_VN_KEY, - skuItem: skuItemById, - knapsackItem: knapsackItemById, - resolvedByName: false, + try { + return resolveFulfillmentPersistedCloudtentaclesContext() + } catch (error) { + if (isMissingCloudtentaclesTokenError(error)) { + throw createHttpError('当前 cloudtentacles 没有可用 token,请先到平台配置完成登录校验', { + statusCode: 409, + errorCode: 'admin_task_kuaishou_cloud_missing_cloud_token', + cause: error, + }) } - } - const matchedSkuItem = findCloudItemByNames(normalizedSkuItems, nameCandidates) - const matchedKnapsackItem = findCloudItemByNames( - normalizedKnapsackItems, - nameCandidates, - matchedSkuItem ? Number(matchedSkuItem.id || 0) : 0, - ) - const matchedItem = matchedSkuItem || matchedKnapsackItem - - return { - skuId: Number(matchedItem?.id || 0) || 0, - skuName: String(currentSkuName || matchedItem?.name || '').trim(), - vnKey: KUAISHOU_CLOUD_FIXED_VN_KEY, - skuItem: matchedSkuItem, - knapsackItem: matchedKnapsackItem, - resolvedByName: Boolean(matchedItem), + throw error } } -export function resolveKuaishouCloudVnKeyCandidates(input: { flow?: KuaishouCloudFlow, binding?: KuaishouCloudBindingResources } = {}): string[] { - return [KUAISHOU_CLOUD_FIXED_VN_KEY] -} - -export async function prepareKuaishouCloudBindResourceWithFallback(input: { - cloudContext?: CloudtentaclesContext - vnKeyCandidates?: string[] -} = {}): Promise { - const { cloudContext = {}, vnKeyCandidates = [] } = input - const candidates = Array.isArray(vnKeyCandidates) ? vnKeyCandidates : [] - let lastError: unknown = null - - for (const vnKey of candidates) { - let vnId = 0 - let vnPhone = '' - - try { - const appointed = await appointCloudtentaclesVirtualNumber({ - ...cloudContext, - key: vnKey, - }) - vnId = Number(appointed.item?.id || 0) - vnPhone = String(appointed.item?.phone || '').trim() - - if (!vnId || !vnPhone) { - throw createHttpError('申请虚拟号成功但返回数据不完整', { - statusCode: 502, - errorCode: 'admin_task_kuaishou_cloud_invalid_vn', - }) - } - - await generateCloudtentaclesLoginCode({ - ...cloudContext, - key: vnKey, - id: vnId, - }) - - const fetchedCode = await fetchCloudtentaclesVirtualNumberCode({ - ...cloudContext, - key: vnKey, - phone: vnPhone, - }) - - await verifyCloudtentaclesLoginCode({ - ...cloudContext, - key: vnKey, - id: vnId, - code: fetchedCode.code, - }) - - const bindUrlResult = await getCloudtentaclesBindUrl({ - ...cloudContext, - key: vnKey, - id: vnId, - }) - - return { - vnKey, - vnId, - vnPhone, - bindUrl: String(bindUrlResult.bindUrl || '').trim(), - } - } catch (error) { - lastError = error - - if (vnId > 0) { - try { - await backCloudtentaclesVirtualNumber({ - ...cloudContext, - key: vnKey, - id: vnId, - }) - } catch { - // 兜底退号失败时保留原始错误,避免吞掉主链路异常 - } - } - - if (!isRecoverableKuaishouCloudVnKeyError(error)) { - throw error - } - } - } - - throw lastError || createHttpError('没有找到可用的 VN Key', { - statusCode: 409, - errorCode: 'admin_task_kuaishou_cloud_missing_binding_config', - }) -} - -function collectKuaishouCloudNameCandidates(flow: KuaishouCloudFlow): string[] { - return Array.from(new Set([ - String(flow?.binding?.skuName || '').trim(), - String(flow?.internalSkuName || '').trim(), - String(flow?.internalSkuCode || '').trim(), - ].filter(Boolean))) -} - -function findCloudItemByNames(items: CloudSkuLikeItem[], nameCandidates: string[], preferredId = 0): CloudSkuLikeItem | null { - const normalizedItems = Array.isArray(items) ? items : [] - const normalizedNames = nameCandidates - .map((item) => ({ - raw: String(item || '').trim(), - normalized: normalizeProductName(item), - })) - .filter((item) => item.raw && item.normalized) - - if (normalizedNames.length === 0 || normalizedItems.length === 0) { - return preferredId > 0 - ? normalizedItems.find((item) => Number(item.id || 0) === preferredId) || null - : null - } - - if (preferredId > 0) { - const preferred = normalizedItems.find((item) => Number(item.id || 0) === preferredId) || null - if (preferred) { - return preferred - } - } - - const exactMatches = normalizedItems.filter((item) => { - const itemName = normalizeProductName(item.name) - return normalizedNames.some((candidate) => candidate.normalized === itemName) - }) - if (exactMatches.length > 0) { - return exactMatches[0] - } - - const partialMatches = normalizedItems.filter((item) => { - const itemName = normalizeProductName(item.name) - return normalizedNames.some((candidate) => itemName.includes(candidate.normalized) || candidate.normalized.includes(itemName)) - }) - if (partialMatches.length > 0) { - return partialMatches.sort((left, right) => String(left.name || '').length - String(right.name || '').length)[0] - } - - return null -} - -function isCloudSkuLikeItem(item: unknown): item is CloudSkuLikeItem { - const record: JsonObject = item && typeof item === 'object' ? item : {} - return Number(record.id || 0) > 0 -} - -function isRecoverableKuaishouCloudVnKeyError(error: any): boolean { - const errorCode = String(error?.errorCode || error?.code || '').trim() - const errorMessage = String(error?.message || '').trim() - return errorCode === 'cloudtentacles_vn_bind_url_failed' - && errorMessage.includes('不支持的游戏类型') +function isMissingCloudtentaclesTokenError(error: unknown): boolean { + const current = error as HttpErrorLike | null + return String(current?.errorCode || '').trim() === 'kuaishou_cloud_missing_cloud_token' } diff --git a/apps/backend/src/services/admin/write/shared.ts b/apps/backend/src/services/admin/write/shared.ts index 76d4f4da..281ee355 100644 --- a/apps/backend/src/services/admin/write/shared.ts +++ b/apps/backend/src/services/admin/write/shared.ts @@ -1,5 +1,9 @@ import { buildClaimUrl, createTaskClaimToken } from '../../claim/claim-service.js' import { createHttpError } from '../../../utils/http.js' +import { + maskCode as maskCodeValue, + maskPhone as maskPhoneValue, +} from '../../../utils/masking.js' import { canViewerConfirmAssistedRole, canViewerRedeemAssistedTask, @@ -24,29 +28,11 @@ type ErrorLike = { } export function maskPhone(value: unknown): string { - const text = String(value || '').trim() - if (!text) { - return '' - } - - if (text.length <= 7) { - return `${text.slice(0, 2)}***${text.slice(-2)}` - } - - return `${text.slice(0, 3)}****${text.slice(-4)}` + return maskPhoneValue(value) } export function maskCode(value: unknown): string { - const text = String(value || '').trim() - if (!text) { - return '' - } - - if (text.length <= 8) { - return `${text.slice(0, 2)}***${text.slice(-2)}` - } - - return `${text.slice(0, 4)}****${text.slice(-4)}` + return maskCodeValue(value) } export function resolveTaskInventoryGroupCodes(task: TaskRow): string[] | null { diff --git a/apps/backend/src/services/claim/session/shared.ts b/apps/backend/src/services/claim/session/shared.ts index 87bb5804..b71d661a 100644 --- a/apps/backend/src/services/claim/session/shared.ts +++ b/apps/backend/src/services/claim/session/shared.ts @@ -1,6 +1,11 @@ import { buildClaimUrl } from '../claim-service.js' import { createHttpError } from '../../../utils/http.js' +import { maskCode as maskCodeValue } from '../../../utils/masking.js' import { formatFenToAmount, normalizeFen } from '../../../utils/money.js' +import { + parseTaskContext as parseTaskContextValue, + parseTaskState as parseTaskStateValue, +} from '../../../utils/task-json.js' export const CLAIM_TERMINAL_STATUSES = new Set(['expired', 'closed']) export const REDEEM_REPLACEMENT_LIMIT = 10 @@ -178,21 +183,7 @@ export function mergeTaskContext(task, patch = {}) { } export function parseTaskState(task) { - const value = task?.state_json - - if (!value) { - return {} - } - - if (typeof value === 'object') { - return value - } - - try { - return JSON.parse(String(value || '{}')) - } catch { - return {} - } + return parseTaskStateValue(task) } export function isAssistedClaimTask(task) { @@ -200,21 +191,7 @@ export function isAssistedClaimTask(task) { } export function parseTaskContext(task) { - const value = task?.context_json - - if (!value) { - return {} - } - - if (typeof value === 'object') { - return value - } - - try { - return JSON.parse(String(value || '{}')) - } catch { - return {} - } + return parseTaskContextValue(task) } export function normalizeClaimLoginType(loginType) { @@ -227,15 +204,5 @@ export function isRecoverableSessionError(error) { } export function maskCode(value) { - const text = String(value || '').trim() - - if (!text) { - return '' - } - - if (text.length <= 8) { - return `${text.slice(0, 2)}****${text.slice(-2)}` - } - - return `${text.slice(0, 4)}****${text.slice(-4)}` + return maskCodeValue(value, { shortMask: '****' }) } diff --git a/apps/backend/src/services/fulfillment/kuaishou-cloud/domain.ts b/apps/backend/src/services/fulfillment/kuaishou-cloud/domain.ts index 5c16e519..57668f5e 100644 --- a/apps/backend/src/services/fulfillment/kuaishou-cloud/domain.ts +++ b/apps/backend/src/services/fulfillment/kuaishou-cloud/domain.ts @@ -1,4 +1,8 @@ import { resolveCloudtentaclesConfig } from "../../platforms/cloudtentacles/shared.js"; +import { + maskCode as maskCodeValue, + maskPhone as maskPhoneValue, +} from "../../../utils/masking.js"; export const KUAISHOU_CLOUD_FIXED_VN_KEY = "1"; @@ -154,29 +158,11 @@ export function normalizeKuaishouCloudRoleInfo(value) { } export function maskPhone(value) { - const text = String(value || "").trim(); - if (!text) { - return ""; - } - - if (text.length <= 7) { - return `${text.slice(0, 2)}***${text.slice(-2)}`; - } - - return `${text.slice(0, 3)}****${text.slice(-4)}`; + return maskPhoneValue(value); } export function maskCode(value) { - const text = String(value || "").trim(); - if (!text) { - return ""; - } - - if (text.length <= 8) { - return `${text.slice(0, 2)}***${text.slice(-2)}`; - } - - return `${text.slice(0, 4)}****${text.slice(-4)}`; + return maskCodeValue(value); } export function resolveKuaishouCloudBindUrlExpiresAt(preparedAt) { 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 af5f097b..dc28bada 100644 --- a/apps/backend/src/services/fulfillment/kuaishou-cloud/task-context.ts +++ b/apps/backend/src/services/fulfillment/kuaishou-cloud/task-context.ts @@ -1,3 +1,5 @@ +import { parseTaskContext as parseTaskContextValue } from "../../../utils/task-json.js"; + export function normalizeActor(actor) { if (!actor || typeof actor !== "object") { return null; @@ -21,21 +23,7 @@ export function normalizeActor(actor) { } export function parseTaskContext(task) { - const value = task?.context_json; - - if (!value) { - return {}; - } - - if (typeof value === "object") { - return value; - } - - try { - return JSON.parse(String(value || "{}")); - } catch { - return {}; - } + return parseTaskContextValue(task); } export function getTaskClaimExpiresAt(task) { diff --git a/apps/backend/src/utils/masking.ts b/apps/backend/src/utils/masking.ts new file mode 100644 index 00000000..c88c955c --- /dev/null +++ b/apps/backend/src/utils/masking.ts @@ -0,0 +1,51 @@ +type MaskPhoneOptions = { + maskShort?: boolean + shortMask?: string +} + +type MaskCodeOptions = { + shortMask?: string +} + +export function maskSecret(value: unknown): string { + const normalized = String(value || '').trim() + if (!normalized) { + return '' + } + + if (normalized.length <= 10) { + return `${normalized.slice(0, 2)}****${normalized.slice(-2)}` + } + + return `${normalized.slice(0, 6)}****${normalized.slice(-6)}` +} + +export function maskPhone(value: unknown, options: MaskPhoneOptions = {}): string { + const text = String(value || '').trim() + if (!text) { + return '' + } + + if (text.length <= 7) { + if (options.maskShort === false) { + return text + } + + return `${text.slice(0, 2)}${options.shortMask || '***'}${text.slice(-2)}` + } + + return `${text.slice(0, 3)}****${text.slice(-4)}` +} + +export function maskCode(value: unknown, options: MaskCodeOptions = {}): string { + const text = String(value || '').trim() + if (!text) { + return '' + } + + if (text.length <= 8) { + return `${text.slice(0, 2)}${options.shortMask || '***'}${text.slice(-2)}` + } + + return `${text.slice(0, 4)}****${text.slice(-4)}` +} diff --git a/apps/backend/src/utils/task-json.ts b/apps/backend/src/utils/task-json.ts new file mode 100644 index 00000000..8d1a45fe --- /dev/null +++ b/apps/backend/src/utils/task-json.ts @@ -0,0 +1,26 @@ +export type JsonRecord = Record + +export function parseJsonObject(value: unknown): JsonRecord { + if (!value) { + return {} + } + + if (typeof value === 'object' && !Array.isArray(value)) { + return value as JsonRecord + } + + try { + const parsed = JSON.parse(String(value || '{}')) + return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : {} + } catch { + return {} + } +} + +export function parseTaskContext(task: { context_json?: unknown } | null | undefined): JsonRecord { + return parseJsonObject(task?.context_json) +} + +export function parseTaskState(task: { state_json?: unknown } | null | undefined): JsonRecord { + return parseJsonObject(task?.state_json) +}