继续收紧 TypeScript:ClaimIdentity 契约与 flow 出口类型

- 新增 types/claim-identity,统一 ClaimIdentity / Payload / AdminSummary
- 导出 KuaishouCloudFlow、KuaishouFeifeiFlow 作为 normalize 出口
- 生产路径 JsonObject 统一从 types/json 导入,去掉 rebind as any
- 前后端 claimIdentity 类型字段对齐
This commit is contained in:
yml2213
2026-07-10 15:56:25 +08:00
parent 8bcd9bbd81
commit 042574dea3
20 changed files with 219 additions and 88 deletions
@@ -34,18 +34,7 @@ export type CloudSkuLikeItem = {
[key: string]: unknown [key: string]: unknown
} }
export type KuaishouCloudFlow = JsonObject & { export type { KuaishouCloudFlow } from '../../fulfillment/kuaishou-cloud/domain.js'
configId: string
internalSkuCode: string
internalSkuName: string
ticket: JsonObject
binding: JsonObject
role: JsonObject
purchase: JsonObject
dispatch: JsonObject
returnNumber: JsonObject
consume: JsonObject
}
export type KuaishouCloudBindingResources = { export type KuaishouCloudBindingResources = {
skuId: number skuId: number
@@ -1,14 +1,19 @@
import { createHttpError } from '../../utils/http.js' 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 {
ClaimIdentity,
ClaimIdentityAdminSummary,
ClaimIdentityPayload,
} from '../../types/claim-identity.js'
import type { TaskRow } from '../../types/repository/rows.js' import type { TaskRow } from '../../types/repository/rows.js'
import type { JsonObject } from '../../types/json.js' import type { JsonObject } from '../../types/json.js'
export type ClaimIdentity = { export type {
expectedUid: string ClaimIdentity,
submittedAt: string | null ClaimIdentityAdminSummary,
source: string ClaimIdentityPayload,
} } from '../../types/claim-identity.js'
const CLAIM_UID_MAX_LENGTH = 64 const CLAIM_UID_MAX_LENGTH = 64
@@ -130,18 +135,6 @@ 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 } = {},
@@ -261,7 +254,7 @@ export function appendUidToUrl(targetUrl: unknown, uid: unknown): string {
} }
} }
export function buildClaimIdentityPayload(value: unknown) { export function buildClaimIdentityPayload(value: unknown): ClaimIdentityPayload {
const identity = normalizeClaimIdentity(value) const identity = normalizeClaimIdentity(value)
return { return {
expectedUid: identity.expectedUid, expectedUid: identity.expectedUid,
@@ -18,7 +18,103 @@ export function isIndustryEVoucherTask(task: unknown) {
return String(source.executor_key || '').trim() === 'kuaishou-industry' return String(source.executor_key || '').trim() === 'kuaishou-industry'
} }
export function normalizeKuaishouCloudFlow(value: unknown) { export type KuaishouCloudDeliveryItem = {
cloudSkuId: number
cloudSkuName: string
quantity: number
}
/** normalizeKuaishouCloudFlow 出口类型(履约主路径应经此 normalize) */
export type KuaishouCloudFlow = {
configId: string
internalSkuCode: string
internalSkuName: string
deliveryItems: KuaishouCloudDeliveryItem[]
ticket: {
code: string
status: string
capturedAt: unknown
capturedBy: unknown
verifiedAt: unknown
oid: string
formToken: string
leftCount: number
goodsTitle: string
}
binding: {
prepareStatus: string
cloudSourceKeys: string[]
resolvedSourceKey: string
skuId: number
skuName: string
vnKey: string
vnId: number
vnPhone: string
bindUrl: string
bindPreparedAt: unknown
bindExpiresAt: unknown
bindProbeAt: unknown
bindProbeStatus: string
bindProbeMessage: string
roleName: string
roleId: string
}
role: {
status: string
name: string
rid: string
refreshedAt: unknown
errorMessage: string
rawInfo: unknown
defaultName: string
defaultRid: string
defaultCapturedAt: unknown
defaultCaptureStatus: string
defaultErrorMessage: string
isDefaultRole: boolean
}
purchase: {
autoBuyEnabled: boolean
minAssetReserve: number
usedKnapsack: boolean
purchaseTriggered: boolean
assetBefore: number
assetAfter: number
purchaseAt: unknown
items: unknown[]
}
dispatch: {
status: string
dispatchAt: unknown
dispatchBy: unknown
sendType: number
note: string
items: unknown[]
}
returnNumber: {
status: string
returnedAt: unknown
returnedBy: unknown
autoReturnEnabled: boolean
}
consume: {
status: string
shopId: string
shopName: string
autoConsumeEnabled: boolean
consumedAt: unknown
errorMessage: string
}
rebind: {
currentAttempt: number
history: unknown[]
}
notes: string
/** 未建模扩展(mock 等)仍可落库 */
[key: string]: unknown
}
export function normalizeKuaishouCloudFlow(value: unknown): KuaishouCloudFlow {
const source: JsonObject = value && typeof value === 'object' ? (value as JsonObject) : {} const source: JsonObject = value && typeof value === 'object' ? (value as JsonObject) : {}
const binding = asJsonObject(source.binding) const binding = asJsonObject(source.binding)
const role = asJsonObject(source.role) const role = asJsonObject(source.role)
@@ -14,6 +14,10 @@ export {
isSameKuaishouCloudRoleIdentity, isSameKuaishouCloudRoleIdentity,
resolveKuaishouCloudBindUrlExpiresAt, resolveKuaishouCloudBindUrlExpiresAt,
} from './domain.js' } from './domain.js'
export type {
KuaishouCloudDeliveryItem,
KuaishouCloudFlow,
} from './domain.js'
export { resolvePersistedCloudtentaclesContextBySourceKeys } from './cloudtentacles-context.js' export { resolvePersistedCloudtentaclesContextBySourceKeys } from './cloudtentacles-context.js'
export { ensureTaskClaimLink } from './ensure-claim-link.js' export { ensureTaskClaimLink } from './ensure-claim-link.js'
export { export {
@@ -41,10 +41,13 @@ export async function probeKuaishouCloudTaskBindUrl(task: TaskRow, options: Json
const probeIntervalMs = const probeIntervalMs =
Number(resolveCloudtentaclesConfig().bindUrlProbeIntervalSeconds || 30) * 1000 Number(resolveCloudtentaclesConfig().bindUrlProbeIntervalSeconds || 30) * 1000
const lastProbeAt = flow.binding.bindProbeAt
? Date.parse(String(flow.binding.bindProbeAt))
: NaN
if ( if (
!options.force && !options.force &&
flow.binding.bindProbeAt && Number.isFinite(lastProbeAt) &&
Date.now() - Date.parse(flow.binding.bindProbeAt) < probeIntervalMs Date.now() - lastProbeAt < probeIntervalMs
) { ) {
return { return {
task, task,
@@ -38,14 +38,13 @@ export async function rebindKuaishouCloudTaskRole(task: TaskRow, options: JsonOb
} }
const normalizedStatus = normalizeTaskStatus(task.task_status) const normalizedStatus = normalizeTaskStatus(task.task_status)
if ( const rebindAllowedStatuses = new Set<string>([
![ TASK_STATUS.WAITING_BINDING,
TASK_STATUS.WAITING_BINDING, TASK_STATUS.ROLE_CONFIRMED,
TASK_STATUS.ROLE_CONFIRMED, TASK_STATUS.MANUAL_REVIEW,
TASK_STATUS.MANUAL_REVIEW, TASK_STATUS.RETRY_PENDING,
TASK_STATUS.RETRY_PENDING, ])
].includes(normalizedStatus as any) if (!rebindAllowedStatuses.has(normalizedStatus)) {
) {
throw createHttpError('当前任务状态不可换绑角色', { throw createHttpError('当前任务状态不可换绑角色', {
statusCode: 409, statusCode: 409,
errorCode: 'kuaishou_cloud_rebind_not_allowed', errorCode: 'kuaishou_cloud_rebind_not_allowed',
@@ -6,9 +6,9 @@ import { getCloudtentaclesBindInfo } from '../../platforms/cloudtentacles/virtua
import { import {
hasKuaishouCloudDefaultRoleSnapshot, hasKuaishouCloudDefaultRoleSnapshot,
isSameKuaishouCloudRoleIdentity, isSameKuaishouCloudRoleIdentity,
normalizeKuaishouCloudFlow,
normalizeKuaishouCloudRoleInfo, normalizeKuaishouCloudRoleInfo,
type JsonObject, type JsonObject,
type KuaishouCloudFlow,
} from './domain.js' } from './domain.js'
export type KuaishouCloudDefaultRoleSnapshot = { export type KuaishouCloudDefaultRoleSnapshot = {
@@ -79,7 +79,7 @@ export function buildRoleStateFromCurrentInfo({
now, now,
emptyMessage, emptyMessage,
}: { }: {
flow: ReturnType<typeof normalizeKuaishouCloudFlow> flow: KuaishouCloudFlow
roleInfo: { name: string; rid: string; rawInfo?: unknown } roleInfo: { name: string; rid: string; rawInfo?: unknown }
now: string now: string
emptyMessage: string emptyMessage: string
@@ -207,7 +207,28 @@ export async function syncKuaishouFeifeiTaskStatus(task: TaskRow) {
return updatedTask || task return updatedTask || task
} }
export function normalizeKuaishouFeifeiFlow(value: unknown) { /** normalizeKuaishouFeifeiFlow 出口类型 */
export type KuaishouFeifeiFlow = {
flowType: 'kuaishou_feifei'
productCode: string
productName: string
platformOrderNo: string
orderNo: string
rechargeStatus: number
rechargeStatusLabel: string
rechargeResultMessage: string
claimUrl: string
consumeStatus: string
shortLink: ReturnType<typeof normalizeShortLinkPayload>
h5: {
entryUrl: string
rechargeUrl: string
}
lastSyncedAt: unknown
raw: unknown
}
export function normalizeKuaishouFeifeiFlow(value: unknown): KuaishouFeifeiFlow {
const source = value && typeof value === 'object' && !Array.isArray(value) const source = value && typeof value === 'object' && !Array.isArray(value)
? value as JsonObject ? value as JsonObject
: {} : {}
@@ -270,7 +291,7 @@ export function resolveKuaishouFeifeiH5UrlWithTaskUid(
} }
function mergeKuaishouFeifeiOrder( function mergeKuaishouFeifeiOrder(
flow: ReturnType<typeof normalizeKuaishouFeifeiFlow>, flow: KuaishouFeifeiFlow,
order: Awaited<ReturnType<typeof createKuaishouFeifeiOrder>>, order: Awaited<ReturnType<typeof createKuaishouFeifeiOrder>>,
patch: { patch: {
platformOrderNo?: string platformOrderNo?: string
@@ -299,7 +320,7 @@ function mergeKuaishouFeifeiOrder(
} }
function resolveKuaishouFeifeiDirectClaimUrl( function resolveKuaishouFeifeiDirectClaimUrl(
flow: ReturnType<typeof normalizeKuaishouFeifeiFlow>, flow: KuaishouFeifeiFlow,
) { ) {
const h5ClaimUrl = flow.h5.rechargeUrl || flow.h5.entryUrl const h5ClaimUrl = flow.h5.rechargeUrl || flow.h5.entryUrl
if (h5ClaimUrl) { if (h5ClaimUrl) {
@@ -1,4 +1,5 @@
import { getFulfillmentProfileByKey } from '../../repositories/fulfillment-profile-repo.js' import { getFulfillmentProfileByKey } from '../../repositories/fulfillment-profile-repo.js'
import type { JsonObject } from '../../types/json.js'
import type { OrderItemRow, OrderRow } from '../../types/repository/rows.js' import type { OrderItemRow, OrderRow } from '../../types/repository/rows.js'
import { import {
FULFILLMENT_EXECUTOR_KEYS, FULFILLMENT_EXECUTOR_KEYS,
@@ -6,8 +7,6 @@ import {
isKuaishouFeifeiExecutor, isKuaishouFeifeiExecutor,
} from './executors/types.js' } from './executors/types.js'
type JsonObject = Record<string, unknown>
export type FulfillmentBindingLike = { export type FulfillmentBindingLike = {
id: number id: number
profile_id?: number profile_id?: number
@@ -7,10 +7,9 @@ import {
saveAppConfigEntry, saveAppConfigEntry,
} from '../config/app-config-store.js' } from '../config/app-config-store.js'
import { normalizeCloudtentaclesMatchName } from '../order/cloudtentacles-match-utils.js' import { normalizeCloudtentaclesMatchName } from '../order/cloudtentacles-match-utils.js'
import type { JsonObject } from '../../types/json.js'
import { FULFILLMENT_EXECUTOR_KEYS } from './executors/types.js' import { FULFILLMENT_EXECUTOR_KEYS } from './executors/types.js'
type JsonObject = Record<string, unknown>
export type FulfillmentRoutingRuleMatchType = 'exact' | 'contains' export type FulfillmentRoutingRuleMatchType = 'exact' | 'contains'
export type FulfillmentRoutingExecutorConfig = { export type FulfillmentRoutingExecutorConfig = {
@@ -1,8 +1,8 @@
import type { JsonObject } from '../../types/json.js'
import { createHttpError } from '../../utils/http.js' import { createHttpError } from '../../utils/http.js'
const DEFAULT_TIMEOUT_MS = 10000 const DEFAULT_TIMEOUT_MS = 10000
type JsonObject = Record<string, unknown>
type BarkSendInput = { type BarkSendInput = {
serverUrl?: string serverUrl?: string
recipient?: { recipient?: {
@@ -1,10 +1,10 @@
import type { JsonObject } from '../../types/json.js'
import { logWarn } from '../../utils/logger.js' import { logWarn } from '../../utils/logger.js'
import { sendInternalNotification } from './notification-service.js' import { sendInternalNotification } from './notification-service.js'
const DEFAULT_COOLDOWN_MS = 10 * 60 * 1000 const DEFAULT_COOLDOWN_MS = 10 * 60 * 1000
const notificationCooldownMap = new Map<string, number>() const notificationCooldownMap = new Map<string, number>()
type JsonObject = Record<string, unknown>
type InternalNotificationPayload = { type InternalNotificationPayload = {
title: string title: string
body?: string body?: string
@@ -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'
const DEFAULT_WPUSH_ENDPOINT = 'https://api.wpush.cn/api/v1/send' const DEFAULT_WPUSH_ENDPOINT = 'https://api.wpush.cn/api/v1/send'
const DEFAULT_TIMEOUT_MS = 10000 const DEFAULT_TIMEOUT_MS = 10000
type JsonObject = Record<string, unknown>
type NodeHttpResponse = { type NodeHttpResponse = {
ok: boolean ok: boolean
status: number status: number
+2 -11
View File
@@ -1,20 +1,11 @@
import type { ClaimIdentityAdminSummary } from '../claim-identity.js'
import type { JsonObject } from '../json.js' import type { JsonObject } from '../json.js'
/** /**
* 后台任务详情 API 返回形状(与前端 AdminTaskDetail 对齐的后端契约)。 * 后台任务详情 API 返回形状(与前端 AdminTaskDetail 对齐的后端契约)。
* 深层履约字段仍用 JsonObject,避免一次改完所有 mapper。 * 深层履约字段仍用 JsonObject,避免一次改完所有 mapper。
*/ */
export type AdminClaimIdentitySummary = { export type AdminClaimIdentitySummary = 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 type AdminTaskDetailView = { export type AdminTaskDetailView = {
task: JsonObject task: JsonObject
+32
View File
@@ -0,0 +1,32 @@
/**
* 领取页 UID 身份与后台摘要的共享类型(纯数据契约,无运行时依赖)。
*/
export type ClaimIdentity = {
expectedUid: string
submittedAt: string | null
source: string
}
/** 领取页 / claim API 返回的 UID 身份(含 ready */
export type ClaimIdentityPayload = ClaimIdentity & {
ready: boolean
}
/** 后台任务详情中的 UID 兼容摘要 */
export type ClaimIdentityAdminSummary = ClaimIdentityPayload & {
boundUid: string
boundRoleName: string
uidMatched: boolean | null
compatibilityMode: 'uid' | 'legacy_no_uid'
note: string
}
/** context_json 中 claimIdentity 的宽松写入形态(兼容历史 uid 字段) */
export type ClaimIdentityContext = {
expectedUid?: string
submittedAt?: string | null
source?: string
/** @deprecated 历史字段,normalize 时映射到 expectedUid */
uid?: string
}
+6 -8
View File
@@ -1,18 +1,16 @@
import type { ClaimIdentityContext } from './claim-identity.js'
import type { JsonObject } from './json.js' import type { JsonObject } from './json.js'
export type { ClaimIdentityContext }
/** /**
* 履约任务 context_json 的已知字段。 * 履约任务 context_json 的已知字段。
* 未建模扩展仍可通过索引访问(JsonObject)。 * 未建模扩展仍可通过索引访问(JsonObject)。
* kuaishouCloudFulfillment / kuaishouFeifei 在 normalize 出口有具名类型;
* 此处保留 JsonObject 以兼容未 normalize 的原始落库形态。
*/ */
export type ClaimIdentityContext = {
expectedUid?: string
submittedAt?: string | null
source?: string
uid?: string
}
export type TaskContext = JsonObject & { export type TaskContext = JsonObject & {
claimIdentity?: ClaimIdentityContext | JsonObject claimIdentity?: ClaimIdentityContext
kuaishouCloudFulfillment?: JsonObject kuaishouCloudFulfillment?: JsonObject
kuaishouFeifei?: JsonObject kuaishouFeifei?: JsonObject
kuaishouIndustryVoucher?: JsonObject kuaishouIndustryVoucher?: JsonObject
+1 -2
View File
@@ -1,9 +1,8 @@
import type { JsonObject } from '../types/json.js'
import { createHttpError } from './http.js' import { createHttpError } from './http.js'
const LARGE_INTEGER_PATTERN = /(:\s*)(-?\d{16,})(\s*[,}\]])/g const LARGE_INTEGER_PATTERN = /(:\s*)(-?\d{16,})(\s*[,}\]])/g
type JsonObject = Record<string, unknown>
type ParseJsonObjectOptions = { type ParseJsonObjectOptions = {
preserveLargeIntegers?: boolean preserveLargeIntegers?: boolean
throwOnError?: boolean throwOnError?: boolean
+14 -11
View File
@@ -58,6 +58,19 @@ export interface AdminTaskActionResponse {
token?: string token?: string
} }
/** 与后端 ClaimIdentityAdminSummary / AdminClaimIdentitySummary 对齐 */
export interface 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 interface AdminTaskDetail { export interface AdminTaskDetail {
task: AdminTaskListItem task: AdminTaskListItem
order: null | { order: null | {
@@ -105,17 +118,7 @@ export interface AdminTaskDetail {
resultMessage: string resultMessage: string
}> }>
} }
claimIdentity: null | { claimIdentity: AdminClaimIdentitySummary | null
expectedUid: string
submittedAt: string | null
source: string
ready: boolean
boundUid: string
boundRoleName: string
uidMatched: boolean | null
compatibilityMode: 'uid' | 'legacy_no_uid'
note: string
}
kuaishouCloudFulfillment: null | { kuaishouCloudFulfillment: null | {
flowType: string flowType: string
configId: string configId: string
+1
View File
@@ -4,6 +4,7 @@ export type ClaimTokenStatus = 'active' | 'used' | 'expired' | 'revoked' | (stri
export type ClaimTaskStatus = KnownTaskStatus | (string & {}) export type ClaimTaskStatus = KnownTaskStatus | (string & {})
/** 与后端 ClaimIdentityPayload 对齐 */
export interface ClaimIdentityInfo { export interface ClaimIdentityInfo {
expectedUid: string expectedUid: string
submittedAt: string | null submittedAt: string | null
+7 -3
View File
@@ -7,10 +7,13 @@
- `JsonObject`:中心定义(当前值仍为 any 以兼容深层 context 访问) - `JsonObject`:中心定义(当前值仍为 any 以兼容深层 context 访问)
- `StrictJson` / `StrictJsonObject`:新代码可用的 unknown 严格形态 - `StrictJson` / `StrictJsonObject`:新代码可用的 unknown 严格形态
- `asJsonObject` / `isPlainObject` 工具 - `asJsonObject` / `isPlainObject` 工具
3. **去掉约 60 处本地** `type JsonObject = Record<string, any>` 3. **去掉生产路径本地** `type JsonObject = …`(统一从 `types/json` 导入;scripts 可保留本地别名)
4. **TaskContext**`types/task-context.ts``parseTaskContext` 返回 `TaskContext` 4. **TaskContext**`types/task-context.ts``parseTaskContext` 返回 `TaskContext`
5. **Admin 任务详情契约**`types/admin/task-detail.ts``getAdminTaskDetail(): Promise<AdminTaskDetailView>` 5. **ClaimIdentity 契约**`types/claim-identity.ts``ClaimIdentity` / `ClaimIdentityPayload` / `ClaimIdentityAdminSummary`
6. **测试 fixture**`delivery-task-service.test.ts` 减少 `as any` 6. **Admin 任务详情契约**`types/admin/task-detail.ts``getAdminTaskDetail(): Promise<AdminTaskDetailView>`
7. **normalize 出口具名类型**`KuaishouCloudFlow``KuaishouFeifeiFlow`
8. **前后端 claimIdentity 字段对齐**:前端 `ClaimIdentityInfo` / `AdminClaimIdentitySummary`
9. **测试 fixture**`delivery-task-service.test.ts` 减少 `as any`;生产路径去掉 `as any` 状态列表
## 未做完(后续) ## 未做完(后续)
@@ -18,6 +21,7 @@
- 前后端共享类型包 / OpenAPI - 前后端共享类型包 / OpenAPI
- 前端对齐 `noUncheckedIndexedAccess` - 前端对齐 `noUncheckedIndexedAccess`
- 全量测试 fixture 工厂化 - 全量测试 fixture 工厂化
- scripts 本地 JsonObject 别名可按需迁入中心类型
## 约定 ## 约定