继续收紧 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
}
export type KuaishouCloudFlow = JsonObject & {
configId: string
internalSkuCode: string
internalSkuName: string
ticket: JsonObject
binding: JsonObject
role: JsonObject
purchase: JsonObject
dispatch: JsonObject
returnNumber: JsonObject
consume: JsonObject
}
export type { KuaishouCloudFlow } from '../../fulfillment/kuaishou-cloud/domain.js'
export type KuaishouCloudBindingResources = {
skuId: number
@@ -1,14 +1,19 @@
import { createHttpError } from '../../utils/http.js'
import { parseTaskContext } from '../../utils/task-json.js'
import { isTaskFinalStatus, normalizeTaskStatus, TASK_STATUS } from '../../domain/task-status.js'
import type {
ClaimIdentity,
ClaimIdentityAdminSummary,
ClaimIdentityPayload,
} from '../../types/claim-identity.js'
import type { TaskRow } from '../../types/repository/rows.js'
import type { JsonObject } from '../../types/json.js'
export type ClaimIdentity = {
expectedUid: string
submittedAt: string | null
source: string
}
export type {
ClaimIdentity,
ClaimIdentityAdminSummary,
ClaimIdentityPayload,
} from '../../types/claim-identity.js'
const CLAIM_UID_MAX_LENGTH = 64
@@ -130,18 +135,6 @@ export function assertLewanAutoFulfillmentUidReady(
return expectedUid
}
export type ClaimIdentityAdminSummary = {
expectedUid: string
submittedAt: string | null
source: string
ready: boolean
boundUid: string
boundRoleName: string
uidMatched: boolean | null
compatibilityMode: 'uid' | 'legacy_no_uid'
note: string
}
export function buildClaimIdentityAdminSummary(
context: unknown,
options: { flowLike?: unknown; taskRoleId?: unknown; taskRoleName?: unknown } = {},
@@ -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)
return {
expectedUid: identity.expectedUid,
@@ -18,7 +18,103 @@ export function isIndustryEVoucherTask(task: unknown) {
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 binding = asJsonObject(source.binding)
const role = asJsonObject(source.role)
@@ -14,6 +14,10 @@ export {
isSameKuaishouCloudRoleIdentity,
resolveKuaishouCloudBindUrlExpiresAt,
} from './domain.js'
export type {
KuaishouCloudDeliveryItem,
KuaishouCloudFlow,
} from './domain.js'
export { resolvePersistedCloudtentaclesContextBySourceKeys } from './cloudtentacles-context.js'
export { ensureTaskClaimLink } from './ensure-claim-link.js'
export {
@@ -41,10 +41,13 @@ export async function probeKuaishouCloudTaskBindUrl(task: TaskRow, options: Json
const probeIntervalMs =
Number(resolveCloudtentaclesConfig().bindUrlProbeIntervalSeconds || 30) * 1000
const lastProbeAt = flow.binding.bindProbeAt
? Date.parse(String(flow.binding.bindProbeAt))
: NaN
if (
!options.force &&
flow.binding.bindProbeAt &&
Date.now() - Date.parse(flow.binding.bindProbeAt) < probeIntervalMs
Number.isFinite(lastProbeAt) &&
Date.now() - lastProbeAt < probeIntervalMs
) {
return {
task,
@@ -38,14 +38,13 @@ export async function rebindKuaishouCloudTaskRole(task: TaskRow, options: JsonOb
}
const normalizedStatus = normalizeTaskStatus(task.task_status)
if (
![
TASK_STATUS.WAITING_BINDING,
TASK_STATUS.ROLE_CONFIRMED,
TASK_STATUS.MANUAL_REVIEW,
TASK_STATUS.RETRY_PENDING,
].includes(normalizedStatus as any)
) {
const rebindAllowedStatuses = new Set<string>([
TASK_STATUS.WAITING_BINDING,
TASK_STATUS.ROLE_CONFIRMED,
TASK_STATUS.MANUAL_REVIEW,
TASK_STATUS.RETRY_PENDING,
])
if (!rebindAllowedStatuses.has(normalizedStatus)) {
throw createHttpError('当前任务状态不可换绑角色', {
statusCode: 409,
errorCode: 'kuaishou_cloud_rebind_not_allowed',
@@ -6,9 +6,9 @@ import { getCloudtentaclesBindInfo } from '../../platforms/cloudtentacles/virtua
import {
hasKuaishouCloudDefaultRoleSnapshot,
isSameKuaishouCloudRoleIdentity,
normalizeKuaishouCloudFlow,
normalizeKuaishouCloudRoleInfo,
type JsonObject,
type KuaishouCloudFlow,
} from './domain.js'
export type KuaishouCloudDefaultRoleSnapshot = {
@@ -79,7 +79,7 @@ export function buildRoleStateFromCurrentInfo({
now,
emptyMessage,
}: {
flow: ReturnType<typeof normalizeKuaishouCloudFlow>
flow: KuaishouCloudFlow
roleInfo: { name: string; rid: string; rawInfo?: unknown }
now: string
emptyMessage: string
@@ -207,7 +207,28 @@ export async function syncKuaishouFeifeiTaskStatus(task: TaskRow) {
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)
? value as JsonObject
: {}
@@ -270,7 +291,7 @@ export function resolveKuaishouFeifeiH5UrlWithTaskUid(
}
function mergeKuaishouFeifeiOrder(
flow: ReturnType<typeof normalizeKuaishouFeifeiFlow>,
flow: KuaishouFeifeiFlow,
order: Awaited<ReturnType<typeof createKuaishouFeifeiOrder>>,
patch: {
platformOrderNo?: string
@@ -299,7 +320,7 @@ function mergeKuaishouFeifeiOrder(
}
function resolveKuaishouFeifeiDirectClaimUrl(
flow: ReturnType<typeof normalizeKuaishouFeifeiFlow>,
flow: KuaishouFeifeiFlow,
) {
const h5ClaimUrl = flow.h5.rechargeUrl || flow.h5.entryUrl
if (h5ClaimUrl) {
@@ -1,4 +1,5 @@
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 {
FULFILLMENT_EXECUTOR_KEYS,
@@ -6,8 +7,6 @@ import {
isKuaishouFeifeiExecutor,
} from './executors/types.js'
type JsonObject = Record<string, unknown>
export type FulfillmentBindingLike = {
id: number
profile_id?: number
@@ -7,10 +7,9 @@ import {
saveAppConfigEntry,
} from '../config/app-config-store.js'
import { normalizeCloudtentaclesMatchName } from '../order/cloudtentacles-match-utils.js'
import type { JsonObject } from '../../types/json.js'
import { FULFILLMENT_EXECUTOR_KEYS } from './executors/types.js'
type JsonObject = Record<string, unknown>
export type FulfillmentRoutingRuleMatchType = 'exact' | 'contains'
export type FulfillmentRoutingExecutorConfig = {
@@ -1,8 +1,8 @@
import type { JsonObject } from '../../types/json.js'
import { createHttpError } from '../../utils/http.js'
const DEFAULT_TIMEOUT_MS = 10000
type JsonObject = Record<string, unknown>
type BarkSendInput = {
serverUrl?: string
recipient?: {
@@ -1,10 +1,10 @@
import type { JsonObject } from '../../types/json.js'
import { logWarn } from '../../utils/logger.js'
import { sendInternalNotification } from './notification-service.js'
const DEFAULT_COOLDOWN_MS = 10 * 60 * 1000
const notificationCooldownMap = new Map<string, number>()
type JsonObject = Record<string, unknown>
type InternalNotificationPayload = {
title: string
body?: string
@@ -1,12 +1,12 @@
import http from 'node:http'
import https from 'node:https'
import type { JsonObject } from '../../types/json.js'
import { createHttpError } from '../../utils/http.js'
const DEFAULT_WPUSH_ENDPOINT = 'https://api.wpush.cn/api/v1/send'
const DEFAULT_TIMEOUT_MS = 10000
type JsonObject = Record<string, unknown>
type NodeHttpResponse = {
ok: boolean
status: number
+2 -11
View File
@@ -1,20 +1,11 @@
import type { ClaimIdentityAdminSummary } from '../claim-identity.js'
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 AdminClaimIdentitySummary = ClaimIdentityAdminSummary
export type AdminTaskDetailView = {
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'
export type { ClaimIdentityContext }
/**
* 履约任务 context_json 的已知字段。
* 未建模扩展仍可通过索引访问(JsonObject)。
* kuaishouCloudFulfillment / kuaishouFeifei 在 normalize 出口有具名类型;
* 此处保留 JsonObject 以兼容未 normalize 的原始落库形态。
*/
export type ClaimIdentityContext = {
expectedUid?: string
submittedAt?: string | null
source?: string
uid?: string
}
export type TaskContext = JsonObject & {
claimIdentity?: ClaimIdentityContext | JsonObject
claimIdentity?: ClaimIdentityContext
kuaishouCloudFulfillment?: JsonObject
kuaishouFeifei?: JsonObject
kuaishouIndustryVoucher?: JsonObject
+1 -2
View File
@@ -1,9 +1,8 @@
import type { JsonObject } from '../types/json.js'
import { createHttpError } from './http.js'
const LARGE_INTEGER_PATTERN = /(:\s*)(-?\d{16,})(\s*[,}\]])/g
type JsonObject = Record<string, unknown>
type ParseJsonObjectOptions = {
preserveLargeIntegers?: boolean
throwOnError?: boolean
+14 -11
View File
@@ -58,6 +58,19 @@ export interface AdminTaskActionResponse {
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 {
task: AdminTaskListItem
order: null | {
@@ -105,17 +118,7 @@ export interface AdminTaskDetail {
resultMessage: string
}>
}
claimIdentity: null | {
expectedUid: string
submittedAt: string | null
source: string
ready: boolean
boundUid: string
boundRoleName: string
uidMatched: boolean | null
compatibilityMode: 'uid' | 'legacy_no_uid'
note: string
}
claimIdentity: AdminClaimIdentitySummary | null
kuaishouCloudFulfillment: null | {
flowType: string
configId: string
+1
View File
@@ -4,6 +4,7 @@ export type ClaimTokenStatus = 'active' | 'used' | 'expired' | 'revoked' | (stri
export type ClaimTaskStatus = KnownTaskStatus | (string & {})
/** 与后端 ClaimIdentityPayload 对齐 */
export interface ClaimIdentityInfo {
expectedUid: string
submittedAt: string | null
+7 -3
View File
@@ -7,10 +7,13 @@
- `JsonObject`:中心定义(当前值仍为 any 以兼容深层 context 访问)
- `StrictJson` / `StrictJsonObject`:新代码可用的 unknown 严格形态
- `asJsonObject` / `isPlainObject` 工具
3. **去掉约 60 处本地** `type JsonObject = Record<string, any>`
3. **去掉生产路径本地** `type JsonObject = …`(统一从 `types/json` 导入;scripts 可保留本地别名)
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`
5. **ClaimIdentity 契约**`types/claim-identity.ts``ClaimIdentity` / `ClaimIdentityPayload` / `ClaimIdentityAdminSummary`
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
- 前端对齐 `noUncheckedIndexedAccess`
- 全量测试 fixture 工厂化
- scripts 本地 JsonObject 别名可按需迁入中心类型
## 约定