添加快手 Cloud 换绑角色功能
This commit is contained in:
@@ -17,7 +17,7 @@ export const TASK_STATUS = {
|
||||
CLOSED: 'closed',
|
||||
} as const
|
||||
|
||||
export type KnownTaskStatus = typeof TASK_STATUS[keyof typeof TASK_STATUS]
|
||||
export type KnownTaskStatus = (typeof TASK_STATUS)[keyof typeof TASK_STATUS]
|
||||
export type TaskStatus = KnownTaskStatus | (string & {})
|
||||
|
||||
const TASK_TRANSITIONS: Record<KnownTaskStatus, readonly KnownTaskStatus[]> = {
|
||||
@@ -66,6 +66,7 @@ const TASK_TRANSITIONS: Record<KnownTaskStatus, readonly KnownTaskStatus[]> = {
|
||||
TASK_STATUS.CLOSED,
|
||||
],
|
||||
[TASK_STATUS.ROLE_CONFIRMED]: [
|
||||
TASK_STATUS.WAITING_BINDING,
|
||||
TASK_STATUS.REDEEMING,
|
||||
TASK_STATUS.RETRY_PENDING,
|
||||
TASK_STATUS.MANUAL_REVIEW,
|
||||
@@ -102,11 +103,7 @@ const TASK_TRANSITIONS: Record<KnownTaskStatus, readonly KnownTaskStatus[]> = {
|
||||
TASK_STATUS.REDEEMED,
|
||||
TASK_STATUS.CLOSED,
|
||||
],
|
||||
[TASK_STATUS.FAILED]: [
|
||||
TASK_STATUS.RETRY_PENDING,
|
||||
TASK_STATUS.MANUAL_REVIEW,
|
||||
TASK_STATUS.CLOSED,
|
||||
],
|
||||
[TASK_STATUS.FAILED]: [TASK_STATUS.RETRY_PENDING, TASK_STATUS.MANUAL_REVIEW, TASK_STATUS.CLOSED],
|
||||
[TASK_STATUS.COMPLETED]: [],
|
||||
[TASK_STATUS.REDEEMED]: [],
|
||||
[TASK_STATUS.EXPIRED]: [],
|
||||
@@ -206,6 +203,13 @@ const KUAISHOU_CLOUD_PREPARABLE_STATUSES = new Set<TaskStatus>([
|
||||
TASK_STATUS.FAILED,
|
||||
])
|
||||
|
||||
const KUAISHOU_CLOUD_REBINDABLE_STATUSES = new Set<TaskStatus>([
|
||||
TASK_STATUS.WAITING_BINDING,
|
||||
TASK_STATUS.ROLE_CONFIRMED,
|
||||
TASK_STATUS.MANUAL_REVIEW,
|
||||
TASK_STATUS.RETRY_PENDING,
|
||||
])
|
||||
|
||||
export function normalizeTaskStatus(value: unknown): TaskStatus {
|
||||
return String(value || '').trim() as TaskStatus
|
||||
}
|
||||
@@ -285,6 +289,10 @@ export function canPrepareKuaishouCloudFulfillmentStatus(status: unknown): boole
|
||||
return KUAISHOU_CLOUD_PREPARABLE_STATUSES.has(normalizeTaskStatus(status))
|
||||
}
|
||||
|
||||
export function canRebindKuaishouCloudRoleStatus(status: unknown): boolean {
|
||||
return KUAISHOU_CLOUD_REBINDABLE_STATUSES.has(normalizeTaskStatus(status))
|
||||
}
|
||||
|
||||
export function canDispatchKuaishouCloudFulfillmentStatus(status: unknown): boolean {
|
||||
return normalizeTaskStatus(status) === TASK_STATUS.WAITING_BINDING
|
||||
}
|
||||
@@ -293,10 +301,15 @@ export function canReturnKuaishouCloudFulfillmentStatus(status: unknown): boolea
|
||||
return normalizeTaskStatus(status) === TASK_STATUS.DISPATCHED_PENDING_RETURN
|
||||
}
|
||||
|
||||
export function resolveInitialPaidTaskStatus(profile: {
|
||||
requires_claim?: boolean
|
||||
executor_key?: unknown
|
||||
} | null | undefined): TaskStatus {
|
||||
export function resolveInitialPaidTaskStatus(
|
||||
profile:
|
||||
| {
|
||||
requires_claim?: boolean
|
||||
executor_key?: unknown
|
||||
}
|
||||
| null
|
||||
| undefined,
|
||||
): TaskStatus {
|
||||
if (Boolean(profile?.requires_claim)) {
|
||||
return TASK_STATUS.PAID
|
||||
}
|
||||
|
||||
@@ -1,177 +1,166 @@
|
||||
import type { Request } from "express";
|
||||
import { Router } from "express";
|
||||
import type { Request } from 'express'
|
||||
import { Router } from 'express'
|
||||
|
||||
import {
|
||||
getAdminTaskDetail,
|
||||
getAdminTasks,
|
||||
} from "../../services/admin/admin-read-service.js";
|
||||
import { closeAdminTask } from "../../services/admin/write/task-actions.js";
|
||||
import { getAdminTaskDetail, getAdminTasks } from '../../services/admin/admin-read-service.js'
|
||||
import { closeAdminTask } from '../../services/admin/write/task-actions.js'
|
||||
import {
|
||||
dispatchAdminTaskKuaishouCloudFulfillment,
|
||||
prepareAdminTaskKuaishouCloudFulfillment,
|
||||
rebindAdminTaskKuaishouCloudRole,
|
||||
refreshAdminTaskKuaishouCloudRoleInfo,
|
||||
returnNumberAdminTaskKuaishouCloudFulfillment,
|
||||
} from "../../services/admin/write/kuaishou-cloud-actions.js";
|
||||
import {
|
||||
createJsonHandler,
|
||||
requireAdminRoles,
|
||||
} from "./session.js";
|
||||
} from '../../services/admin/write/kuaishou-cloud-actions.js'
|
||||
import { createJsonHandler, requireAdminRoles } from './session.js'
|
||||
import type {
|
||||
AdminTaskKuaishouCloudDispatchRouteBody,
|
||||
AdminTaskRouteParams,
|
||||
AdminTaskRouteQuery,
|
||||
} from "../../types/admin/route-inputs.js";
|
||||
import type { AdminTaskActionResponse } from "../../types/admin/write-models.js";
|
||||
} from '../../types/admin/route-inputs.js'
|
||||
import type { AdminTaskActionResponse } from '../../types/admin/write-models.js'
|
||||
|
||||
const router = Router();
|
||||
const router = Router()
|
||||
|
||||
function getTaskId(req: Request): string {
|
||||
return String((req.params as AdminTaskRouteParams).taskId || "");
|
||||
return String((req.params as AdminTaskRouteParams).taskId || '')
|
||||
}
|
||||
|
||||
router.get(
|
||||
"/tasks",
|
||||
'/tasks',
|
||||
createJsonHandler(
|
||||
(req) =>
|
||||
getAdminTasks(req.query as AdminTaskRouteQuery, req.adminSession || null),
|
||||
(req) => getAdminTasks(req.query as AdminTaskRouteQuery, req.adminSession || null),
|
||||
{
|
||||
successMessage: "ok",
|
||||
errorMessage: "读取任务列表失败",
|
||||
scope: "[admin/tasks]",
|
||||
}
|
||||
)
|
||||
);
|
||||
successMessage: 'ok',
|
||||
errorMessage: '读取任务列表失败',
|
||||
scope: '[admin/tasks]',
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
router.get(
|
||||
"/tasks/:taskId",
|
||||
createJsonHandler(
|
||||
(req) => getAdminTaskDetail(getTaskId(req), req.adminSession || null),
|
||||
{
|
||||
successMessage: "ok",
|
||||
errorMessage: "读取任务详情失败",
|
||||
scope: "[admin/tasks/:taskId]",
|
||||
}
|
||||
)
|
||||
);
|
||||
'/tasks/:taskId',
|
||||
createJsonHandler((req) => getAdminTaskDetail(getTaskId(req), req.adminSession || null), {
|
||||
successMessage: 'ok',
|
||||
errorMessage: '读取任务详情失败',
|
||||
scope: '[admin/tasks/:taskId]',
|
||||
}),
|
||||
)
|
||||
|
||||
router.post(
|
||||
"/tasks/:taskId/close",
|
||||
requireAdminRoles(["admin", "operator", "support"]),
|
||||
createJsonHandler(
|
||||
(req) => closeAdminTask(getTaskId(req), req.adminSession || null),
|
||||
{
|
||||
successMessage: "任务已关闭",
|
||||
errorMessage: "关闭任务失败",
|
||||
scope: "[admin/tasks/:taskId/close]",
|
||||
audit: (req, data) => buildTaskAudit("task_closed", req, data),
|
||||
}
|
||||
)
|
||||
);
|
||||
'/tasks/:taskId/close',
|
||||
requireAdminRoles(['admin', 'operator', 'support']),
|
||||
createJsonHandler((req) => closeAdminTask(getTaskId(req), req.adminSession || null), {
|
||||
successMessage: '任务已关闭',
|
||||
errorMessage: '关闭任务失败',
|
||||
scope: '[admin/tasks/:taskId/close]',
|
||||
audit: (req, data) => buildTaskAudit('task_closed', req, data),
|
||||
}),
|
||||
)
|
||||
|
||||
router.post(
|
||||
"/tasks/:taskId/kuaishou-cloud/prepare",
|
||||
requireAdminRoles(["admin", "operator"]),
|
||||
'/tasks/:taskId/kuaishou-cloud/prepare',
|
||||
requireAdminRoles(['admin', 'operator']),
|
||||
createJsonHandler(
|
||||
(req) =>
|
||||
prepareAdminTaskKuaishouCloudFulfillment(
|
||||
getTaskId(req),
|
||||
req.adminSession || null
|
||||
),
|
||||
(req) => prepareAdminTaskKuaishouCloudFulfillment(getTaskId(req), req.adminSession || null),
|
||||
{
|
||||
successMessage: "绑定资源已准备完成",
|
||||
errorMessage: "准备绑定资源失败",
|
||||
scope: "[admin/tasks/:taskId/kuaishou-cloud/prepare]",
|
||||
audit: (req, data) => buildTaskAudit("task_kuaishou_cloud_prepare", req, data),
|
||||
}
|
||||
)
|
||||
);
|
||||
successMessage: '绑定资源已准备完成',
|
||||
errorMessage: '准备绑定资源失败',
|
||||
scope: '[admin/tasks/:taskId/kuaishou-cloud/prepare]',
|
||||
audit: (req, data) => buildTaskAudit('task_kuaishou_cloud_prepare', req, data),
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
router.post(
|
||||
"/tasks/:taskId/kuaishou-cloud/refresh-role-info",
|
||||
requireAdminRoles(["admin", "operator", "support"]),
|
||||
'/tasks/:taskId/kuaishou-cloud/refresh-role-info',
|
||||
requireAdminRoles(['admin', 'operator', 'support']),
|
||||
createJsonHandler(
|
||||
(req) =>
|
||||
refreshAdminTaskKuaishouCloudRoleInfo(
|
||||
getTaskId(req),
|
||||
req.adminSession || null
|
||||
),
|
||||
(req) => refreshAdminTaskKuaishouCloudRoleInfo(getTaskId(req), req.adminSession || null),
|
||||
{
|
||||
successMessage: "角色信息已刷新",
|
||||
errorMessage: "刷新角色信息失败",
|
||||
scope: "[admin/tasks/:taskId/kuaishou-cloud/refresh-role-info]",
|
||||
audit: (req, data) =>
|
||||
buildTaskAudit("task_kuaishou_cloud_role_info_refreshed", req, data),
|
||||
}
|
||||
)
|
||||
);
|
||||
successMessage: '角色信息已刷新',
|
||||
errorMessage: '刷新角色信息失败',
|
||||
scope: '[admin/tasks/:taskId/kuaishou-cloud/refresh-role-info]',
|
||||
audit: (req, data) => buildTaskAudit('task_kuaishou_cloud_role_info_refreshed', req, data),
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
router.post(
|
||||
"/tasks/:taskId/kuaishou-cloud/dispatch",
|
||||
requireAdminRoles(["admin", "operator"]),
|
||||
'/tasks/:taskId/kuaishou-cloud/rebind-role',
|
||||
requireAdminRoles(['admin', 'operator']),
|
||||
createJsonHandler(
|
||||
(req) => rebindAdminTaskKuaishouCloudRole(getTaskId(req), req.adminSession || null),
|
||||
{
|
||||
successMessage: '角色换绑资源已准备完成',
|
||||
errorMessage: '换绑角色失败',
|
||||
scope: '[admin/tasks/:taskId/kuaishou-cloud/rebind-role]',
|
||||
audit: (req, data) => buildTaskAudit('task_kuaishou_cloud_rebind_role', req, data),
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
router.post(
|
||||
'/tasks/:taskId/kuaishou-cloud/dispatch',
|
||||
requireAdminRoles(['admin', 'operator']),
|
||||
createJsonHandler(
|
||||
(req) =>
|
||||
dispatchAdminTaskKuaishouCloudFulfillment(
|
||||
getTaskId(req),
|
||||
req.body as AdminTaskKuaishouCloudDispatchRouteBody,
|
||||
req.adminSession || null
|
||||
req.adminSession || null,
|
||||
),
|
||||
{
|
||||
successMessage: "已完成绑定确认并发货",
|
||||
errorMessage: "执行发货失败",
|
||||
scope: "[admin/tasks/:taskId/kuaishou-cloud/dispatch]",
|
||||
successMessage: '已完成绑定确认并发货',
|
||||
errorMessage: '执行发货失败',
|
||||
scope: '[admin/tasks/:taskId/kuaishou-cloud/dispatch]',
|
||||
audit: (req, data) => {
|
||||
const result = data as AdminTaskActionResponse;
|
||||
const body = req.body as AdminTaskKuaishouCloudDispatchRouteBody;
|
||||
const result = data as AdminTaskActionResponse
|
||||
const body = req.body as AdminTaskKuaishouCloudDispatchRouteBody
|
||||
|
||||
return {
|
||||
action: "task_kuaishou_cloud_dispatch",
|
||||
targetType: "task",
|
||||
action: 'task_kuaishou_cloud_dispatch',
|
||||
targetType: 'task',
|
||||
targetId: String(getTaskId(req)),
|
||||
data: {
|
||||
taskId: result.task.taskId,
|
||||
taskNo: result.task.taskNo,
|
||||
status: result.task.status,
|
||||
deliveryStatus: result.task.deliveryStatus,
|
||||
ticketCodeProvided: Boolean(String(body.ticketCode || "").trim()),
|
||||
ticketCodeProvided: Boolean(String(body.ticketCode || '').trim()),
|
||||
},
|
||||
};
|
||||
}
|
||||
},
|
||||
}
|
||||
)
|
||||
);
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
router.post(
|
||||
"/tasks/:taskId/kuaishou-cloud/return-number",
|
||||
requireAdminRoles(["admin", "operator"]),
|
||||
'/tasks/:taskId/kuaishou-cloud/return-number',
|
||||
requireAdminRoles(['admin', 'operator']),
|
||||
createJsonHandler(
|
||||
(req) =>
|
||||
returnNumberAdminTaskKuaishouCloudFulfillment(
|
||||
getTaskId(req),
|
||||
req.adminSession || null
|
||||
),
|
||||
returnNumberAdminTaskKuaishouCloudFulfillment(getTaskId(req), req.adminSession || null),
|
||||
{
|
||||
successMessage: "号码已退还",
|
||||
errorMessage: "退还号码失败",
|
||||
scope: "[admin/tasks/:taskId/kuaishou-cloud/return-number]",
|
||||
audit: (req, data) =>
|
||||
buildTaskAudit("task_kuaishou_cloud_return_number", req, data),
|
||||
}
|
||||
)
|
||||
);
|
||||
successMessage: '号码已退还',
|
||||
errorMessage: '退还号码失败',
|
||||
scope: '[admin/tasks/:taskId/kuaishou-cloud/return-number]',
|
||||
audit: (req, data) => buildTaskAudit('task_kuaishou_cloud_return_number', req, data),
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
function buildTaskAudit(action: string, req: Request, data: unknown) {
|
||||
const result = data as AdminTaskActionResponse;
|
||||
const result = data as AdminTaskActionResponse
|
||||
|
||||
return {
|
||||
action,
|
||||
targetType: "task",
|
||||
targetType: 'task',
|
||||
targetId: String(getTaskId(req)),
|
||||
data: {
|
||||
taskId: result.task.taskId,
|
||||
taskNo: result.task.taskNo,
|
||||
status: result.task.status,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export default router;
|
||||
export default router
|
||||
|
||||
@@ -1,88 +1,89 @@
|
||||
import { Router } from "express";
|
||||
import { Router } from 'express'
|
||||
|
||||
import { createRateLimitMiddleware, getParamRateLimitKey } from "../middleware/rate-limit.js";
|
||||
import { createRateLimitMiddleware, getParamRateLimitKey } from '../middleware/rate-limit.js'
|
||||
import {
|
||||
confirmKuaishouCloudClaimRole,
|
||||
getKuaishouCloudClaimDetail,
|
||||
getKuaishouCloudClaimGuideAssetPath,
|
||||
rebindKuaishouCloudClaimRole,
|
||||
redeemKuaishouCloudClaim,
|
||||
verifyKuaishouCloudClaimTicket,
|
||||
} from "../services/claim/kuaishou-cloud-claim-service.js";
|
||||
import {
|
||||
buildNotFoundPayload,
|
||||
createRouteFileHandler,
|
||||
createRouteHandler,
|
||||
} from "../utils/http.js";
|
||||
} from '../services/claim/kuaishou-cloud-claim-service.js'
|
||||
import { buildNotFoundPayload, createRouteFileHandler, createRouteHandler } from '../utils/http.js'
|
||||
|
||||
const router = Router();
|
||||
const router = Router()
|
||||
const claimReadRateLimit = createRateLimitMiddleware({
|
||||
scope: "claim:read",
|
||||
scope: 'claim:read',
|
||||
windowMs: 60_000,
|
||||
max: 120,
|
||||
key: getParamRateLimitKey("token"),
|
||||
});
|
||||
key: getParamRateLimitKey('token'),
|
||||
})
|
||||
const claimWriteRateLimit = createRateLimitMiddleware({
|
||||
scope: "claim:write",
|
||||
scope: 'claim:write',
|
||||
windowMs: 60_000,
|
||||
max: 30,
|
||||
key: getParamRateLimitKey("token"),
|
||||
});
|
||||
key: getParamRateLimitKey('token'),
|
||||
})
|
||||
|
||||
router.get(
|
||||
"/:token",
|
||||
'/:token',
|
||||
claimReadRateLimit,
|
||||
createRouteHandler((req) => getKuaishouCloudClaimDetail(req.params.token), {
|
||||
errorMessage: "查询快手领取详情失败",
|
||||
scope: "[claims/:token]",
|
||||
})
|
||||
);
|
||||
errorMessage: '查询快手领取详情失败',
|
||||
scope: '[claims/:token]',
|
||||
}),
|
||||
)
|
||||
|
||||
router.post(
|
||||
"/:token/kuaishou-cloud/verify-ticket",
|
||||
'/:token/kuaishou-cloud/verify-ticket',
|
||||
claimWriteRateLimit,
|
||||
createRouteHandler(
|
||||
(req) => verifyKuaishouCloudClaimTicket(req.params.token, req.body),
|
||||
{
|
||||
successMessage: "核销码校验成功",
|
||||
errorMessage: "校验核销码失败",
|
||||
scope: "[claims/:token/kuaishou-cloud/verify-ticket]",
|
||||
}
|
||||
)
|
||||
);
|
||||
createRouteHandler((req) => verifyKuaishouCloudClaimTicket(req.params.token, req.body), {
|
||||
successMessage: '核销码校验成功',
|
||||
errorMessage: '校验核销码失败',
|
||||
scope: '[claims/:token/kuaishou-cloud/verify-ticket]',
|
||||
}),
|
||||
)
|
||||
|
||||
router.post(
|
||||
"/:token/kuaishou-cloud/confirm-role",
|
||||
'/:token/kuaishou-cloud/confirm-role',
|
||||
claimWriteRateLimit,
|
||||
createRouteHandler((req) => confirmKuaishouCloudClaimRole(req.params.token), {
|
||||
successMessage: "角色已确认",
|
||||
errorMessage: "确认角色失败",
|
||||
scope: "[claims/:token/kuaishou-cloud/confirm-role]",
|
||||
})
|
||||
);
|
||||
successMessage: '角色已确认',
|
||||
errorMessage: '确认角色失败',
|
||||
scope: '[claims/:token/kuaishou-cloud/confirm-role]',
|
||||
}),
|
||||
)
|
||||
|
||||
router.post(
|
||||
"/:token/kuaishou-cloud/redeem",
|
||||
'/:token/kuaishou-cloud/rebind-role',
|
||||
claimWriteRateLimit,
|
||||
createRouteHandler((req) => rebindKuaishouCloudClaimRole(req.params.token), {
|
||||
successMessage: '角色换绑资源已准备完成',
|
||||
errorMessage: '换绑角色失败',
|
||||
scope: '[claims/:token/kuaishou-cloud/rebind-role]',
|
||||
}),
|
||||
)
|
||||
|
||||
router.post(
|
||||
'/:token/kuaishou-cloud/redeem',
|
||||
claimWriteRateLimit,
|
||||
createRouteHandler((req) => redeemKuaishouCloudClaim(req.params.token), {
|
||||
successMessage: "兑换请求已提交",
|
||||
errorMessage: "兑换失败",
|
||||
scope: "[claims/:token/kuaishou-cloud/redeem]",
|
||||
})
|
||||
);
|
||||
successMessage: '兑换请求已提交',
|
||||
errorMessage: '兑换失败',
|
||||
scope: '[claims/:token/kuaishou-cloud/redeem]',
|
||||
}),
|
||||
)
|
||||
|
||||
router.get(
|
||||
"/assets/kuaishou-cloud/:filename",
|
||||
createRouteFileHandler(
|
||||
(req) => getKuaishouCloudClaimGuideAssetPath(req.params.filename),
|
||||
{
|
||||
errorMessage: "读取指引图片失败",
|
||||
scope: "[claims/assets/kuaishou-cloud/:filename]",
|
||||
}
|
||||
)
|
||||
);
|
||||
'/assets/kuaishou-cloud/:filename',
|
||||
createRouteFileHandler((req) => getKuaishouCloudClaimGuideAssetPath(req.params.filename), {
|
||||
errorMessage: '读取指引图片失败',
|
||||
scope: '[claims/assets/kuaishou-cloud/:filename]',
|
||||
}),
|
||||
)
|
||||
|
||||
router.use((req, res) => {
|
||||
res.status(404).json(buildNotFoundPayload(req));
|
||||
});
|
||||
res.status(404).json(buildNotFoundPayload(req))
|
||||
})
|
||||
|
||||
export default router;
|
||||
export default router
|
||||
|
||||
@@ -7,12 +7,18 @@ import { listTaskEventsByTaskId } from '../../repositories/task-event-repo.js'
|
||||
import { listCloudtentaclesSources } from '../platforms/cloudtentacles/source-config-service.js'
|
||||
import { createHttpError } from '../../utils/http.js'
|
||||
import { formatFenToAmount, normalizeFen } from '../../utils/money.js'
|
||||
import { normalizeDateQuery, normalizePage, normalizePageSize, safeParseJson } from './admin-query-utils.js'
|
||||
import {
|
||||
normalizeDateQuery,
|
||||
normalizePage,
|
||||
normalizePageSize,
|
||||
safeParseJson,
|
||||
} from './admin-query-utils.js'
|
||||
import {
|
||||
canCompleteManualDispatchTaskStatus,
|
||||
canDispatchKuaishouCloudFulfillmentStatus,
|
||||
canMarkManualReviewTaskStatus,
|
||||
canPrepareKuaishouCloudFulfillmentStatus,
|
||||
canRebindKuaishouCloudRoleStatus,
|
||||
canReturnKuaishouCloudFulfillmentStatus,
|
||||
canRetryTaskStatus,
|
||||
} from '../../domain/task-status.js'
|
||||
@@ -55,7 +61,9 @@ import type {
|
||||
|
||||
type JsonRecord = Record<string, any>
|
||||
|
||||
export async function getAdminOrders(query: AdminOrderListQueryInput = {}): Promise<AdminOrderListResponse> {
|
||||
export async function getAdminOrders(
|
||||
query: AdminOrderListQueryInput = {},
|
||||
): Promise<AdminOrderListResponse> {
|
||||
const page = normalizePage(query.page)
|
||||
const pageSize = normalizePageSize(query.pageSize)
|
||||
const { items, total } = await listOrders({
|
||||
@@ -110,9 +118,10 @@ export async function getAdminOrderDetail(orderId: AdminEntityIdInput): Promise<
|
||||
createdAt: order.created_at,
|
||||
updatedAt: order.updated_at,
|
||||
itemSummary,
|
||||
rawPayload: order.raw_payload_json && typeof order.raw_payload_json === 'object'
|
||||
? order.raw_payload_json
|
||||
: JSON.parse(String(order.raw_payload_json || '{}')),
|
||||
rawPayload:
|
||||
order.raw_payload_json && typeof order.raw_payload_json === 'object'
|
||||
? order.raw_payload_json
|
||||
: JSON.parse(String(order.raw_payload_json || '{}')),
|
||||
fulfillmentProgress: buildOrderFulfillmentProgress(tasks),
|
||||
},
|
||||
items: items.map((item) => ({
|
||||
@@ -122,9 +131,10 @@ export async function getAdminOrderDetail(orderId: AdminEntityIdInput): Promise<
|
||||
itemTitle: resolveOrderItemTitle(item),
|
||||
quantity: item.quantity,
|
||||
deliveryMode: resolveOrderItemDeliveryMode(tasks, item.id),
|
||||
spec: item.spec_json && typeof item.spec_json === 'object'
|
||||
? item.spec_json
|
||||
: JSON.parse(String(item.spec_json || '{}')),
|
||||
spec:
|
||||
item.spec_json && typeof item.spec_json === 'object'
|
||||
? item.spec_json
|
||||
: JSON.parse(String(item.spec_json || '{}')),
|
||||
})),
|
||||
tasks: tasks.map((task) => mapAdminTaskSummary(task)),
|
||||
}
|
||||
@@ -182,46 +192,49 @@ export async function getAdminTaskDetail(
|
||||
const claimUrl = claimToken ? buildClaimUrl(claimToken.token) : ''
|
||||
const screenshotUrl = await resolveAdminTaskScreenshotUrl(task, viewerContext)
|
||||
const cloudSourceLabelMap = buildCloudSourceLabelMap()
|
||||
const kuaishouCloudFulfillment = mapKuaishouCloudFulfillmentContext(
|
||||
taskContext.kuaishouCloudFulfillment,
|
||||
{ cloudSourceLabelMap },
|
||||
) || mapKuaishouCloudTaskStateProjection(task, { cloudSourceLabelMap })
|
||||
const kuaishouCloudFulfillment =
|
||||
mapKuaishouCloudFulfillmentContext(taskContext.kuaishouCloudFulfillment, {
|
||||
cloudSourceLabelMap,
|
||||
}) || mapKuaishouCloudTaskStateProjection(task, { cloudSourceLabelMap })
|
||||
|
||||
return {
|
||||
task: mapAdminTaskListItem({
|
||||
...task,
|
||||
sku_code: orderItem?.sku_code || '',
|
||||
sku_name: orderItem?.sku_name || '',
|
||||
claim_token: claimToken?.token || '',
|
||||
}, viewerContext),
|
||||
task: mapAdminTaskListItem(
|
||||
{
|
||||
...task,
|
||||
sku_code: orderItem?.sku_code || '',
|
||||
sku_name: orderItem?.sku_name || '',
|
||||
claim_token: claimToken?.token || '',
|
||||
},
|
||||
viewerContext,
|
||||
),
|
||||
order: order
|
||||
? {
|
||||
orderId: order.id,
|
||||
provider: order.provider || '',
|
||||
platform: order.platform,
|
||||
shopId: order.shop_id || '',
|
||||
shopName: order.shop_name || '',
|
||||
platformOrderId: order.platform_order_id,
|
||||
payStatus: order.pay_status,
|
||||
orderStatus: order.order_status,
|
||||
}
|
||||
orderId: order.id,
|
||||
provider: order.provider || '',
|
||||
platform: order.platform,
|
||||
shopId: order.shop_id || '',
|
||||
shopName: order.shop_name || '',
|
||||
platformOrderId: order.platform_order_id,
|
||||
payStatus: order.pay_status,
|
||||
orderStatus: order.order_status,
|
||||
}
|
||||
: null,
|
||||
orderItem: orderItem
|
||||
? {
|
||||
orderItemId: orderItem.id,
|
||||
skuCode: orderItem.sku_code,
|
||||
skuName: orderItem.sku_name,
|
||||
quantity: orderItem.quantity,
|
||||
}
|
||||
orderItemId: orderItem.id,
|
||||
skuCode: orderItem.sku_code,
|
||||
skuName: orderItem.sku_name,
|
||||
quantity: orderItem.quantity,
|
||||
}
|
||||
: null,
|
||||
claimToken: claimToken
|
||||
? {
|
||||
primaryClaimTokenId: claimToken.id,
|
||||
token: viewerContext.canViewSensitiveTaskData ? claimToken.token : '',
|
||||
status: claimToken.status,
|
||||
expiredAt: claimToken.expired_at,
|
||||
claimUrl,
|
||||
}
|
||||
primaryClaimTokenId: claimToken.id,
|
||||
token: viewerContext.canViewSensitiveTaskData ? claimToken.token : '',
|
||||
status: claimToken.status,
|
||||
expiredAt: claimToken.expired_at,
|
||||
claimUrl,
|
||||
}
|
||||
: null,
|
||||
artifacts: viewerContext.canViewSensitiveTaskData ? safeParseJson(task.artifacts_json) : {},
|
||||
screenshotUrl,
|
||||
@@ -236,27 +249,42 @@ export async function getAdminTaskDetail(
|
||||
manualDispatch: mapManualDispatchContext(taskContext.manualDispatch, viewerContext),
|
||||
events: taskEvents.map(mapAdminTaskEvent),
|
||||
operations: {
|
||||
canRetry: viewerContext.canManageTaskLifecycle
|
||||
&& !isManualDispatchTask(task)
|
||||
&& canRetryTaskStatus(task.task_status),
|
||||
canRetry:
|
||||
viewerContext.canManageTaskLifecycle &&
|
||||
!isManualDispatchTask(task) &&
|
||||
canRetryTaskStatus(task.task_status),
|
||||
canRegenerateClaimLink: canRegenerateClaimLinkForViewer(task, viewerContext),
|
||||
canClose: canViewerCloseTask(task, viewerContext),
|
||||
canMarkManualReview: viewerContext.canManageTaskLifecycle && canMarkManualReviewTaskStatus(task.task_status),
|
||||
canCompleteManualDispatch: viewerContext.canManageTaskLifecycle && isManualDispatchTask(task) && canCompleteManualDispatchTaskStatus(task.task_status),
|
||||
canPrepareKuaishouCloudFulfillment: viewerContext.canManageTaskLifecycle
|
||||
&& String(task.executor_key || '').trim() === 'kuaishou_ct_assisted'
|
||||
&& String(kuaishouCloudFulfillment?.dispatch.status || 'pending').trim() === 'pending'
|
||||
&& String(kuaishouCloudFulfillment?.returnNumber.status || 'pending').trim() === 'pending'
|
||||
&& canPrepareKuaishouCloudFulfillmentStatus(task.task_status),
|
||||
canRefreshKuaishouCloudRoleInfo: viewerContext.canOperateAssistedTask
|
||||
&& String(task.executor_key || '').trim() === 'kuaishou_ct_assisted'
|
||||
&& Number(kuaishouCloudFulfillment?.binding.vnId || 0) > 0,
|
||||
canDispatchKuaishouCloudFulfillment: viewerContext.canManageTaskLifecycle
|
||||
&& String(task.executor_key || '').trim() === 'kuaishou_ct_assisted'
|
||||
&& canDispatchKuaishouCloudFulfillmentStatus(task.task_status),
|
||||
canReturnKuaishouCloudFulfillment: viewerContext.canManageTaskLifecycle
|
||||
&& String(task.executor_key || '').trim() === 'kuaishou_ct_assisted'
|
||||
&& canReturnKuaishouCloudFulfillmentStatus(task.task_status),
|
||||
canMarkManualReview:
|
||||
viewerContext.canManageTaskLifecycle && canMarkManualReviewTaskStatus(task.task_status),
|
||||
canCompleteManualDispatch:
|
||||
viewerContext.canManageTaskLifecycle &&
|
||||
isManualDispatchTask(task) &&
|
||||
canCompleteManualDispatchTaskStatus(task.task_status),
|
||||
canPrepareKuaishouCloudFulfillment:
|
||||
viewerContext.canManageTaskLifecycle &&
|
||||
String(task.executor_key || '').trim() === 'kuaishou_ct_assisted' &&
|
||||
String(kuaishouCloudFulfillment?.dispatch.status || 'pending').trim() === 'pending' &&
|
||||
String(kuaishouCloudFulfillment?.returnNumber.status || 'pending').trim() === 'pending' &&
|
||||
canPrepareKuaishouCloudFulfillmentStatus(task.task_status),
|
||||
canRefreshKuaishouCloudRoleInfo:
|
||||
viewerContext.canOperateAssistedTask &&
|
||||
String(task.executor_key || '').trim() === 'kuaishou_ct_assisted' &&
|
||||
Number(kuaishouCloudFulfillment?.binding.vnId || 0) > 0,
|
||||
canRebindKuaishouCloudRole:
|
||||
viewerContext.canManageTaskLifecycle &&
|
||||
String(task.executor_key || '').trim() === 'kuaishou_ct_assisted' &&
|
||||
canRebindKuaishouCloudRoleStatus(task.task_status) &&
|
||||
String(kuaishouCloudFulfillment?.dispatch.status || 'pending').trim() !== 'success' &&
|
||||
Number(kuaishouCloudFulfillment?.binding.vnId || 0) > 0,
|
||||
canDispatchKuaishouCloudFulfillment:
|
||||
viewerContext.canManageTaskLifecycle &&
|
||||
String(task.executor_key || '').trim() === 'kuaishou_ct_assisted' &&
|
||||
canDispatchKuaishouCloudFulfillmentStatus(task.task_status),
|
||||
canReturnKuaishouCloudFulfillment:
|
||||
viewerContext.canManageTaskLifecycle &&
|
||||
String(task.executor_key || '').trim() === 'kuaishou_ct_assisted' &&
|
||||
canReturnKuaishouCloudFulfillmentStatus(task.task_status),
|
||||
canViewSensitiveTaskData: viewerContext.canViewSensitiveTaskData,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -4,10 +4,7 @@ import {
|
||||
parseTaskContext as parseTaskContextValue,
|
||||
parseTaskState as parseTaskStateValue,
|
||||
} from '../../utils/task-json.js'
|
||||
import {
|
||||
canRegenerateClaimLinkStatus,
|
||||
isTaskFinalStatus,
|
||||
} from '../../domain/task-status.js'
|
||||
import { canRegenerateClaimLinkStatus, isTaskFinalStatus } from '../../domain/task-status.js'
|
||||
|
||||
import type { AdminViewerSessionInput } from '../../types/admin/read-inputs.js'
|
||||
import type { OrderItemRow, TaskRow } from '../../types/repository/rows.js'
|
||||
@@ -51,16 +48,19 @@ export function mapManualDispatchContext(
|
||||
return {
|
||||
outcome: String(record.outcome || '').trim(),
|
||||
deliveryReference: String(record.deliveryReference || '').trim(),
|
||||
deliveredCredential: viewerContext.canViewSensitiveTaskData ? String(record.deliveredCredential || '').trim() : '',
|
||||
deliveredCredential: viewerContext.canViewSensitiveTaskData
|
||||
? String(record.deliveredCredential || '').trim()
|
||||
: '',
|
||||
resultMessage: String(record.resultMessage || '').trim(),
|
||||
completedAt: record.completedAt || null,
|
||||
completedBy: record.completedBy && typeof record.completedBy === 'object'
|
||||
? {
|
||||
userId: Number(record.completedBy.userId || 0) || 0,
|
||||
username: String(record.completedBy.username || '').trim(),
|
||||
role: String(record.completedBy.role || '').trim(),
|
||||
}
|
||||
: null,
|
||||
completedBy:
|
||||
record.completedBy && typeof record.completedBy === 'object'
|
||||
? {
|
||||
userId: Number(record.completedBy.userId || 0) || 0,
|
||||
username: String(record.completedBy.username || '').trim(),
|
||||
role: String(record.completedBy.role || '').trim(),
|
||||
}
|
||||
: null,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -78,7 +78,8 @@ export function mapKuaishouCloudFulfillmentContext(
|
||||
const role = record.role && typeof record.role === 'object' ? record.role : {}
|
||||
const purchase = record.purchase && typeof record.purchase === 'object' ? record.purchase : {}
|
||||
const dispatch = record.dispatch && typeof record.dispatch === 'object' ? record.dispatch : {}
|
||||
const returnNumber = record.returnNumber && typeof record.returnNumber === 'object' ? record.returnNumber : {}
|
||||
const returnNumber =
|
||||
record.returnNumber && typeof record.returnNumber === 'object' ? record.returnNumber : {}
|
||||
const consume = record.consume && typeof record.consume === 'object' ? record.consume : {}
|
||||
const cloudSourceKeys = Array.isArray(binding.cloudSourceKeys)
|
||||
? binding.cloudSourceKeys.map((value: unknown) => String(value || '').trim()).filter(Boolean)
|
||||
@@ -156,6 +157,7 @@ export function mapKuaishouCloudFulfillmentContext(
|
||||
consumedAt: consume.consumedAt || null,
|
||||
errorMessage: String(consume.errorMessage || '').trim(),
|
||||
},
|
||||
rebind: mapKuaishouCloudRebindContext(record.rebind),
|
||||
notes: String(record.notes || '').trim(),
|
||||
}
|
||||
}
|
||||
@@ -204,9 +206,13 @@ export function mapKuaishouCloudTaskStateProjection(
|
||||
binding: {
|
||||
prepareStatus: task.kuaishou_bind_url ? 'ready' : 'pending',
|
||||
cloudSourceKeys: sourceKey ? [sourceKey] : [],
|
||||
cloudSourceLabels: sourceKey ? [resolveCloudSourceLabel(sourceKey, options.cloudSourceLabelMap)] : [],
|
||||
cloudSourceLabels: sourceKey
|
||||
? [resolveCloudSourceLabel(sourceKey, options.cloudSourceLabelMap)]
|
||||
: [],
|
||||
resolvedSourceKey: sourceKey,
|
||||
resolvedSourceLabel: sourceKey ? resolveCloudSourceLabel(sourceKey, options.cloudSourceLabelMap) : '',
|
||||
resolvedSourceLabel: sourceKey
|
||||
? resolveCloudSourceLabel(sourceKey, options.cloudSourceLabelMap)
|
||||
: '',
|
||||
skuId: 0,
|
||||
skuName: '',
|
||||
vnKey: '',
|
||||
@@ -255,6 +261,10 @@ export function mapKuaishouCloudTaskStateProjection(
|
||||
consumedAt: null,
|
||||
errorMessage: '',
|
||||
},
|
||||
rebind: {
|
||||
currentAttempt: 0,
|
||||
history: [],
|
||||
},
|
||||
notes: '',
|
||||
}
|
||||
}
|
||||
@@ -263,13 +273,67 @@ function resolveCloudSourceLabel(sourceKey: string, labelMap: CloudSourceLabelMa
|
||||
const key = String(sourceKey || '').trim()
|
||||
if (!key) return ''
|
||||
|
||||
const label = labelMap instanceof Map
|
||||
? labelMap.get(key)
|
||||
: labelMap?.[key]
|
||||
const label = labelMap instanceof Map ? labelMap.get(key) : labelMap?.[key]
|
||||
|
||||
return String(label || key).trim() || key
|
||||
}
|
||||
|
||||
function mapKuaishouCloudRebindContext(value: unknown): JsonRecord {
|
||||
const record = value && typeof value === 'object' ? (value as JsonRecord) : {}
|
||||
const history = Array.isArray(record.history) ? record.history : []
|
||||
|
||||
return {
|
||||
currentAttempt: Math.max(0, Number(record.currentAttempt || 0) || 0),
|
||||
history: history.map((item) => mapKuaishouCloudRebindHistoryItem(item)).filter(Boolean),
|
||||
}
|
||||
}
|
||||
|
||||
function mapKuaishouCloudRebindHistoryItem(value: unknown): JsonRecord | null {
|
||||
if (!value || typeof value !== 'object') {
|
||||
return null
|
||||
}
|
||||
|
||||
const record = value as JsonRecord
|
||||
const oldBinding =
|
||||
record.oldBinding && typeof record.oldBinding === 'object'
|
||||
? (record.oldBinding as JsonRecord)
|
||||
: {}
|
||||
const newBinding =
|
||||
record.newBinding && typeof record.newBinding === 'object'
|
||||
? (record.newBinding as JsonRecord)
|
||||
: null
|
||||
|
||||
return {
|
||||
attempt: Math.max(1, Number(record.attempt || 1) || 1),
|
||||
source: String(record.source || '').trim(),
|
||||
requestedAt: record.requestedAt || null,
|
||||
requestedBy:
|
||||
record.requestedBy && typeof record.requestedBy === 'object' ? record.requestedBy : null,
|
||||
status: String(record.status || '').trim(),
|
||||
errorMessage: String(record.errorMessage || '').trim(),
|
||||
oldBinding: {
|
||||
vnKey: String(oldBinding.vnKey || '').trim(),
|
||||
vnId: Number(oldBinding.vnId || 0) || 0,
|
||||
vnPhone: String(oldBinding.vnPhone || '').trim(),
|
||||
bindUrl: String(oldBinding.bindUrl || '').trim(),
|
||||
bindPreparedAt: oldBinding.bindPreparedAt || null,
|
||||
bindExpiresAt: oldBinding.bindExpiresAt || null,
|
||||
roleName: String(oldBinding.roleName || '').trim(),
|
||||
roleId: String(oldBinding.roleId || '').trim(),
|
||||
},
|
||||
newBinding: newBinding
|
||||
? {
|
||||
vnKey: String(newBinding.vnKey || '').trim(),
|
||||
vnId: Number(newBinding.vnId || 0) || 0,
|
||||
vnPhone: String(newBinding.vnPhone || '').trim(),
|
||||
bindUrl: String(newBinding.bindUrl || '').trim(),
|
||||
bindPreparedAt: newBinding.bindPreparedAt || null,
|
||||
bindExpiresAt: newBinding.bindExpiresAt || null,
|
||||
}
|
||||
: null,
|
||||
}
|
||||
}
|
||||
|
||||
export function mapRedeemResolutionContext(value: unknown): JsonRecord | null {
|
||||
if (!value || typeof value !== 'object') {
|
||||
return null
|
||||
@@ -278,8 +342,8 @@ export function mapRedeemResolutionContext(value: unknown): JsonRecord | null {
|
||||
const record = value as JsonRecord
|
||||
const attempts = Array.isArray(record.attempts)
|
||||
? record.attempts
|
||||
.map((attempt) => mapRedeemResolutionAttempt(attempt))
|
||||
.filter((attempt): attempt is RedeemResolutionAttempt => Boolean(attempt))
|
||||
.map((attempt) => mapRedeemResolutionAttempt(attempt))
|
||||
.filter((attempt): attempt is RedeemResolutionAttempt => Boolean(attempt))
|
||||
: []
|
||||
|
||||
return {
|
||||
@@ -296,7 +360,9 @@ export function getTaskPrimaryClaimTokenId(task: TaskLike | null | undefined): n
|
||||
return Number.isFinite(value) && value > 0 ? value : null
|
||||
}
|
||||
|
||||
export function createAdminViewerContext(session: AdminViewerSessionInput | null = null): AdminViewerContext {
|
||||
export function createAdminViewerContext(
|
||||
session: AdminViewerSessionInput | null = null,
|
||||
): AdminViewerContext {
|
||||
const role = normalizeAdminRole(session?.role)
|
||||
|
||||
return {
|
||||
@@ -326,7 +392,11 @@ export function canViewerCloseTask(task: TaskLike, viewerContext: AdminViewerCon
|
||||
return !isTaskFinalStatus(task?.task_status)
|
||||
}
|
||||
|
||||
export function resolveDisplayShopName(provider: unknown, shopId: unknown, shopName: unknown): string {
|
||||
export function resolveDisplayShopName(
|
||||
provider: unknown,
|
||||
shopId: unknown,
|
||||
shopName: unknown,
|
||||
): string {
|
||||
const normalizedShopName = String(shopName || '').trim()
|
||||
if (normalizedShopName) {
|
||||
return normalizedShopName
|
||||
@@ -367,8 +437,13 @@ export function resolveOrderItemTitle(item: Partial<OrderItemRow> | null | undef
|
||||
])
|
||||
}
|
||||
|
||||
export function resolveOrderItemDeliveryMode(tasks: TaskLike[] | null | undefined, orderItemId: number | string): string {
|
||||
const task = (Array.isArray(tasks) ? tasks : []).find((item) => item.order_item_id === orderItemId)
|
||||
export function resolveOrderItemDeliveryMode(
|
||||
tasks: TaskLike[] | null | undefined,
|
||||
orderItemId: number | string,
|
||||
): string {
|
||||
const task = (Array.isArray(tasks) ? tasks : []).find(
|
||||
(item) => item.order_item_id === orderItemId,
|
||||
)
|
||||
|
||||
if (!task) {
|
||||
return ''
|
||||
@@ -382,7 +457,12 @@ export function resolveOrderItemDeliveryMode(tasks: TaskLike[] | null | undefine
|
||||
return 'kuaishou_cloud'
|
||||
}
|
||||
|
||||
if (task.requires_claim || getTaskPrimaryClaimTokenId(task) || task.primary_claim_token || task.claim_token) {
|
||||
if (
|
||||
task.requires_claim ||
|
||||
getTaskPrimaryClaimTokenId(task) ||
|
||||
task.primary_claim_token ||
|
||||
task.claim_token
|
||||
) {
|
||||
return 'claim_link'
|
||||
}
|
||||
|
||||
@@ -393,7 +473,10 @@ export function isManualDispatchTask(task: TaskLike | null | undefined): boolean
|
||||
return String(task?.executor_key || '').trim() === 'manual_dispatch'
|
||||
}
|
||||
|
||||
export function canRegenerateClaimLinkForViewer(task: TaskLike, viewerContext: AdminViewerContext): boolean {
|
||||
export function canRegenerateClaimLinkForViewer(
|
||||
task: TaskLike,
|
||||
viewerContext: AdminViewerContext,
|
||||
): boolean {
|
||||
if (isManualDispatchTask(task)) {
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -21,20 +21,15 @@ import {
|
||||
import { createHttpError } from '../../../utils/http.js'
|
||||
import { nowIso } from '../../../utils/time.js'
|
||||
import { TASK_STATUS } from '../../../domain/task-status.js'
|
||||
import {
|
||||
createAdminViewerContext,
|
||||
parseTaskContext,
|
||||
} from '../admin-read-shared-helpers.js'
|
||||
import {
|
||||
getRequiredTask,
|
||||
mapTaskActionPayload,
|
||||
} from '../admin-task-read-helpers.js'
|
||||
import { createAdminViewerContext, parseTaskContext } from '../admin-read-shared-helpers.js'
|
||||
import { getRequiredTask, mapTaskActionPayload } from '../admin-task-read-helpers.js'
|
||||
import {
|
||||
ensureTaskClaimLink,
|
||||
getTaskClaimExpiresAt,
|
||||
maskCode,
|
||||
maskPhone,
|
||||
} from '../write-helpers.js'
|
||||
import { rebindKuaishouCloudTaskRole } from '../../fulfillment/kuaishou-cloud/index.js'
|
||||
import {
|
||||
isKuaishouCloudTask,
|
||||
normalizeKuaishouCloudFlow,
|
||||
@@ -210,17 +205,22 @@ export async function prepareAdminTaskKuaishouCloudFulfillment(
|
||||
updated_at: now,
|
||||
})
|
||||
|
||||
await createTaskEvent(task.id, 'kuaishou_cloud_binding_prepared', {
|
||||
skuId: flowWithResolvedBinding.binding.skuId,
|
||||
skuName: flowWithResolvedBinding.binding.skuName,
|
||||
vnKey: preparedBinding.vnKey,
|
||||
vnId,
|
||||
vnPhoneMasked: maskPhone(vnPhone),
|
||||
bindUrl: preparedBinding.bindUrl,
|
||||
purchaseTriggered,
|
||||
usedKnapsack,
|
||||
resolvedByName: resolvedBinding.resolvedByName,
|
||||
}, now)
|
||||
await createTaskEvent(
|
||||
task.id,
|
||||
'kuaishou_cloud_binding_prepared',
|
||||
{
|
||||
skuId: flowWithResolvedBinding.binding.skuId,
|
||||
skuName: flowWithResolvedBinding.binding.skuName,
|
||||
vnKey: preparedBinding.vnKey,
|
||||
vnId,
|
||||
vnPhoneMasked: maskPhone(vnPhone),
|
||||
bindUrl: preparedBinding.bindUrl,
|
||||
purchaseTriggered,
|
||||
usedKnapsack,
|
||||
resolvedByName: resolvedBinding.resolvedByName,
|
||||
},
|
||||
now,
|
||||
)
|
||||
|
||||
return {
|
||||
task: mapTaskActionPayload(updatedTask),
|
||||
@@ -280,10 +280,10 @@ export async function dispatchAdminTaskKuaishouCloudFulfillment(
|
||||
now,
|
||||
actor: session
|
||||
? {
|
||||
userId: Number(session.userId || 0) || 0,
|
||||
username: String(session.username || '').trim(),
|
||||
role: String(session.role || '').trim(),
|
||||
}
|
||||
userId: Number(session.userId || 0) || 0,
|
||||
username: String(session.username || '').trim(),
|
||||
role: String(session.role || '').trim(),
|
||||
}
|
||||
: null,
|
||||
source: 'admin_task_dispatch',
|
||||
errorCodePrefix: 'admin_task_kuaishou_cloud',
|
||||
@@ -309,13 +309,14 @@ export async function dispatchAdminTaskKuaishouCloudFulfillment(
|
||||
...syncedFlow.ticket,
|
||||
code: ticketCode || persistedTicketCode,
|
||||
capturedAt: ticketCode ? now : syncedFlow.ticket.capturedAt,
|
||||
capturedBy: ticketCode && session
|
||||
? {
|
||||
userId: Number(session.userId || 0) || 0,
|
||||
username: String(session.username || '').trim(),
|
||||
role: String(session.role || '').trim(),
|
||||
}
|
||||
: syncedFlow.ticket.capturedBy,
|
||||
capturedBy:
|
||||
ticketCode && session
|
||||
? {
|
||||
userId: Number(session.userId || 0) || 0,
|
||||
username: String(session.username || '').trim(),
|
||||
role: String(session.role || '').trim(),
|
||||
}
|
||||
: syncedFlow.ticket.capturedBy,
|
||||
},
|
||||
dispatch: {
|
||||
...syncedFlow.dispatch,
|
||||
@@ -323,10 +324,10 @@ export async function dispatchAdminTaskKuaishouCloudFulfillment(
|
||||
dispatchAt: now,
|
||||
dispatchBy: session
|
||||
? {
|
||||
userId: Number(session.userId || 0) || 0,
|
||||
username: String(session.username || '').trim(),
|
||||
role: String(session.role || '').trim(),
|
||||
}
|
||||
userId: Number(session.userId || 0) || 0,
|
||||
username: String(session.username || '').trim(),
|
||||
role: String(session.role || '').trim(),
|
||||
}
|
||||
: null,
|
||||
sendType: Number(dispatchResult.sendType || 0) || 0,
|
||||
note: String(dispatchResult.note || dispatchResult.responseMessage || '').trim(),
|
||||
@@ -338,20 +339,27 @@ export async function dispatchAdminTaskKuaishouCloudFulfillment(
|
||||
task_status: TASK_STATUS.DISPATCHED_PENDING_RETURN,
|
||||
delivery_status: 'delivered',
|
||||
result_code: 'kuaishou_cloud_dispatched',
|
||||
result_message: String(dispatchResult.responseMessage || dispatchResult.note || 'cloudtentacles 发货成功').trim(),
|
||||
result_message: String(
|
||||
dispatchResult.responseMessage || dispatchResult.note || 'cloudtentacles 发货成功',
|
||||
).trim(),
|
||||
last_error: '',
|
||||
context_json: JSON.stringify(nextContext),
|
||||
updated_at: now,
|
||||
})
|
||||
|
||||
await createTaskEvent(task.id, 'kuaishou_cloud_dispatched', {
|
||||
ticketCodeMasked: maskCode(ticketCode || persistedTicketCode),
|
||||
skuId: syncedFlow.binding.skuId,
|
||||
vnId: syncedFlow.binding.vnId,
|
||||
vnPhoneMasked: maskPhone(syncedFlow.binding.vnPhone),
|
||||
sendType: dispatchResult.sendType,
|
||||
note: dispatchResult.note,
|
||||
}, now)
|
||||
await createTaskEvent(
|
||||
task.id,
|
||||
'kuaishou_cloud_dispatched',
|
||||
{
|
||||
ticketCodeMasked: maskCode(ticketCode || persistedTicketCode),
|
||||
skuId: syncedFlow.binding.skuId,
|
||||
vnId: syncedFlow.binding.vnId,
|
||||
vnPhoneMasked: maskPhone(syncedFlow.binding.vnPhone),
|
||||
sendType: dispatchResult.sendType,
|
||||
note: dispatchResult.note,
|
||||
},
|
||||
now,
|
||||
)
|
||||
|
||||
return {
|
||||
task: mapTaskActionPayload(updatedTask),
|
||||
@@ -414,7 +422,8 @@ export async function refreshAdminTaskKuaishouCloudRoleInfo(
|
||||
name: bindInfo.name,
|
||||
rid: bindInfo.rid,
|
||||
refreshedAt: now,
|
||||
errorMessage: bindInfo.name || bindInfo.rid ? '' : '当前还没有查询到角色信息,请让客户完成绑定后再刷新',
|
||||
errorMessage:
|
||||
bindInfo.name || bindInfo.rid ? '' : '当前还没有查询到角色信息,请让客户完成绑定后再刷新',
|
||||
rawInfo: bindInfo.rawInfo,
|
||||
},
|
||||
},
|
||||
@@ -427,24 +436,61 @@ export async function refreshAdminTaskKuaishouCloudRoleInfo(
|
||||
updated_at: now,
|
||||
})
|
||||
|
||||
await createTaskEvent(task.id, 'kuaishou_cloud_role_info_refreshed', {
|
||||
roleName: bindInfo.name,
|
||||
roleId: bindInfo.rid,
|
||||
vnId: flow.binding.vnId,
|
||||
refreshedBy: session
|
||||
? {
|
||||
userId: Number(session.userId || 0) || 0,
|
||||
username: String(session.username || '').trim(),
|
||||
role: String(session.role || '').trim(),
|
||||
}
|
||||
: null,
|
||||
}, now)
|
||||
await createTaskEvent(
|
||||
task.id,
|
||||
'kuaishou_cloud_role_info_refreshed',
|
||||
{
|
||||
roleName: bindInfo.name,
|
||||
roleId: bindInfo.rid,
|
||||
vnId: flow.binding.vnId,
|
||||
refreshedBy: session
|
||||
? {
|
||||
userId: Number(session.userId || 0) || 0,
|
||||
username: String(session.username || '').trim(),
|
||||
role: String(session.role || '').trim(),
|
||||
}
|
||||
: null,
|
||||
},
|
||||
now,
|
||||
)
|
||||
|
||||
return {
|
||||
task: mapTaskActionPayload(updatedTask),
|
||||
}
|
||||
}
|
||||
|
||||
export async function rebindAdminTaskKuaishouCloudRole(
|
||||
taskId: AdminEntityIdInput,
|
||||
session: AdminViewerSessionInput | null = null,
|
||||
): Promise<AdminTaskActionResponse> {
|
||||
const task = await getRequiredTask(taskId)
|
||||
const viewerContext = createAdminViewerContext(session)
|
||||
|
||||
if (!viewerContext.canManageTaskLifecycle) {
|
||||
throw createHttpError('当前账号没有权限换绑角色', {
|
||||
statusCode: 403,
|
||||
errorCode: 'admin_task_rebind_kuaishou_cloud_forbidden',
|
||||
})
|
||||
}
|
||||
|
||||
const result = await rebindKuaishouCloudTaskRole(task, {
|
||||
source: 'admin_task_rebind_role',
|
||||
actor: session
|
||||
? {
|
||||
userId: Number(session.userId || 0) || 0,
|
||||
username: String(session.username || '').trim(),
|
||||
role: String(session.role || '').trim(),
|
||||
}
|
||||
: null,
|
||||
})
|
||||
|
||||
return {
|
||||
task: mapTaskActionPayload(result.task),
|
||||
claimUrl: result.claimUrl,
|
||||
token: result.token,
|
||||
}
|
||||
}
|
||||
|
||||
export async function returnNumberAdminTaskKuaishouCloudFulfillment(
|
||||
taskId: AdminEntityIdInput,
|
||||
session: AdminViewerSessionInput | null = null,
|
||||
@@ -519,7 +565,11 @@ export async function returnNumberAdminTaskKuaishouCloudFulfillment(
|
||||
consumeStatus = consumeResult.consumed ? 'success' : 'failed'
|
||||
consumedAt = consumeResult.consumed ? now : null
|
||||
consumeErrorMessage = String(consumeResult.errorMessage || '').trim()
|
||||
} else if (!shopConfig || shopConfig.enabled === false || !String(shopConfig.cookie || '').trim()) {
|
||||
} else if (
|
||||
!shopConfig ||
|
||||
shopConfig.enabled === false ||
|
||||
!String(shopConfig.cookie || '').trim()
|
||||
) {
|
||||
consumeStatus = 'failed'
|
||||
consumeErrorMessage = '订单对应快手小店缺少可用 Cookie,无法执行快手核销'
|
||||
} else {
|
||||
@@ -561,10 +611,10 @@ export async function returnNumberAdminTaskKuaishouCloudFulfillment(
|
||||
returnedAt: now,
|
||||
returnedBy: session
|
||||
? {
|
||||
userId: Number(session.userId || 0) || 0,
|
||||
username: String(session.username || '').trim(),
|
||||
role: String(session.role || '').trim(),
|
||||
}
|
||||
userId: Number(session.userId || 0) || 0,
|
||||
username: String(session.username || '').trim(),
|
||||
role: String(session.role || '').trim(),
|
||||
}
|
||||
: null,
|
||||
},
|
||||
consume: {
|
||||
@@ -590,10 +640,15 @@ export async function returnNumberAdminTaskKuaishouCloudFulfillment(
|
||||
updated_at: now,
|
||||
})
|
||||
|
||||
await createTaskEvent(task.id, 'kuaishou_cloud_number_returned', {
|
||||
vnId: flow.binding.vnId,
|
||||
vnPhoneMasked: maskPhone(flow.binding.vnPhone),
|
||||
}, now)
|
||||
await createTaskEvent(
|
||||
task.id,
|
||||
'kuaishou_cloud_number_returned',
|
||||
{
|
||||
vnId: flow.binding.vnId,
|
||||
vnPhoneMasked: maskPhone(flow.binding.vnPhone),
|
||||
},
|
||||
now,
|
||||
)
|
||||
|
||||
await createTaskEvent(
|
||||
task.id,
|
||||
|
||||
@@ -26,6 +26,7 @@ import {
|
||||
maskCode,
|
||||
normalizeKuaishouCloudFlow,
|
||||
prepareKuaishouCloudFulfillmentTask,
|
||||
rebindKuaishouCloudTaskRole,
|
||||
refreshKuaishouCloudTaskRoleInfo,
|
||||
} from '../fulfillment/kuaishou-cloud/index.js'
|
||||
import { buildClaimDetailPayload, getClaimContext } from './kuaishou-cloud-claim-context.js'
|
||||
@@ -59,7 +60,9 @@ export async function verifyKuaishouCloudClaimTicket(token: unknown, payload: Js
|
||||
}
|
||||
|
||||
const mockTicketCode = isKuaishouEticketMockTicketCode(ticketCode)
|
||||
let shopId = String(flow.consume.shopId || context.order.shop_id || (mockTicketCode ? 'mock' : '')).trim()
|
||||
let shopId = String(
|
||||
flow.consume.shopId || context.order.shop_id || (mockTicketCode ? 'mock' : ''),
|
||||
).trim()
|
||||
let shopName = String(flow.consume.shopName || context.order.shop_name || '').trim()
|
||||
let detailResult
|
||||
|
||||
@@ -134,13 +137,16 @@ export async function verifyKuaishouCloudClaimTicket(token: unknown, payload: Js
|
||||
} else {
|
||||
const preparedFlow = normalizeKuaishouCloudFlow(nextContext.kuaishouCloudFulfillment)
|
||||
if (preparedFlow.binding.prepareStatus !== 'ready' || !preparedFlow.binding.bindUrl) {
|
||||
await prepareKuaishouCloudFulfillmentTask({
|
||||
...context.task,
|
||||
context_json: JSON.stringify(nextContext),
|
||||
}, {
|
||||
source: 'claim_page_ticket_verified',
|
||||
actor: { source: 'claim_page' },
|
||||
})
|
||||
await prepareKuaishouCloudFulfillmentTask(
|
||||
{
|
||||
...context.task,
|
||||
context_json: JSON.stringify(nextContext),
|
||||
},
|
||||
{
|
||||
source: 'claim_page_ticket_verified',
|
||||
actor: { source: 'claim_page' },
|
||||
},
|
||||
)
|
||||
} else {
|
||||
await updateTask(context.task.id, {
|
||||
claim_token: context.claimToken.token,
|
||||
@@ -159,13 +165,18 @@ export async function verifyKuaishouCloudClaimTicket(token: unknown, payload: Js
|
||||
}
|
||||
}
|
||||
|
||||
await createTaskEvent(context.task.id, 'kuaishou_cloud_ticket_verified', {
|
||||
ticketCodeMasked: maskCode(detailResult.eTicketId || ticketCode),
|
||||
shopId,
|
||||
shopName,
|
||||
goodsTitle: String(detailResult.goods?.itemTitle || '').trim(),
|
||||
mock: mockTicketCode,
|
||||
}, now)
|
||||
await createTaskEvent(
|
||||
context.task.id,
|
||||
'kuaishou_cloud_ticket_verified',
|
||||
{
|
||||
ticketCodeMasked: maskCode(detailResult.eTicketId || ticketCode),
|
||||
shopId,
|
||||
shopName,
|
||||
goodsTitle: String(detailResult.goods?.itemTitle || '').trim(),
|
||||
mock: mockTicketCode,
|
||||
},
|
||||
now,
|
||||
)
|
||||
|
||||
return getKuaishouCloudClaimDetail(token)
|
||||
}
|
||||
@@ -195,10 +206,10 @@ export async function getKuaishouCloudClaimDetail(token: unknown) {
|
||||
let task = context.task
|
||||
|
||||
if (
|
||||
String(task.executor_key || '').trim() === 'kuaishou_ct_assisted'
|
||||
&& !isKuaishouCloudMockTask(task)
|
||||
String(task.executor_key || '').trim() === 'kuaishou_ct_assisted' &&
|
||||
!isKuaishouCloudMockTask(task)
|
||||
) {
|
||||
task = await syncKuaishouCloudRoleInfo(task) || task
|
||||
task = (await syncKuaishouCloudRoleInfo(task)) || task
|
||||
}
|
||||
|
||||
return buildClaimDetailPayload({
|
||||
@@ -209,6 +220,24 @@ export async function getKuaishouCloudClaimDetail(token: unknown) {
|
||||
})
|
||||
}
|
||||
|
||||
export async function rebindKuaishouCloudClaimRole(token: unknown) {
|
||||
const context = await getClaimContext(token)
|
||||
|
||||
if (String(context.task.executor_key || '').trim() !== 'kuaishou_ct_assisted') {
|
||||
throw createHttpError('当前领取链接不是快手 Cloud 客户领取流程', {
|
||||
statusCode: 409,
|
||||
errorCode: 'claim_not_kuaishou_cloud',
|
||||
})
|
||||
}
|
||||
|
||||
await rebindKuaishouCloudTaskRole(context.task, {
|
||||
source: 'claim_page_rebind_role',
|
||||
actor: { source: 'claim_page' },
|
||||
})
|
||||
|
||||
return getKuaishouCloudClaimDetail(token)
|
||||
}
|
||||
|
||||
function parseTaskContext(task: Partial<TaskRow> | null | undefined): JsonObject {
|
||||
const rawValue = task?.context_json
|
||||
if (!rawValue) {
|
||||
@@ -289,10 +318,13 @@ async function queryKuaishouEticketConsumeDetailWithShopFallback({
|
||||
}
|
||||
}
|
||||
|
||||
throw createHttpError(lastError instanceof Error ? lastError.message : '核销码校验失败,请确认是否复制完整', {
|
||||
statusCode: 409,
|
||||
errorCode: 'claim_kuaishou_cloud_ticket_invalid',
|
||||
})
|
||||
throw createHttpError(
|
||||
lastError instanceof Error ? lastError.message : '核销码校验失败,请确认是否复制完整',
|
||||
{
|
||||
statusCode: 409,
|
||||
errorCode: 'claim_kuaishou_cloud_ticket_invalid',
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
function resolveKuaishouEticketDetailCandidateShops({
|
||||
@@ -333,9 +365,7 @@ function isUsableKuaishouEticketShopConfig(
|
||||
shopConfig: KuaishouEticketShopConfig | null | undefined,
|
||||
): shopConfig is KuaishouEticketShopConfig {
|
||||
return Boolean(
|
||||
shopConfig &&
|
||||
shopConfig.enabled !== false &&
|
||||
String(shopConfig.cookie || '').trim(),
|
||||
shopConfig && shopConfig.enabled !== false && String(shopConfig.cookie || '').trim(),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -373,11 +403,11 @@ export async function confirmKuaishouCloudClaimRole(token: unknown) {
|
||||
const refreshed = mockMode
|
||||
? { task: context.task }
|
||||
: await refreshKuaishouCloudTaskRoleInfo(context.task, {
|
||||
source: 'claim_page_role_confirm',
|
||||
actor: { source: 'claim_page' },
|
||||
recordEvent: false,
|
||||
forceProbe: true,
|
||||
})
|
||||
source: 'claim_page_role_confirm',
|
||||
actor: { source: 'claim_page' },
|
||||
recordEvent: false,
|
||||
forceProbe: true,
|
||||
})
|
||||
const flow = normalizeKuaishouCloudFlow(parseTaskContext(refreshed.task).kuaishouCloudFulfillment)
|
||||
|
||||
if (flow.ticket.status !== 'verified') {
|
||||
@@ -404,11 +434,16 @@ export async function confirmKuaishouCloudClaimRole(token: unknown) {
|
||||
updated_at: now,
|
||||
})
|
||||
|
||||
await createTaskEvent(context.task.id, 'kuaishou_cloud_role_confirmed', {
|
||||
vnPhone: flow.binding.vnPhone,
|
||||
roleName: flow.binding.roleName,
|
||||
roleId: flow.binding.roleId,
|
||||
}, now)
|
||||
await createTaskEvent(
|
||||
context.task.id,
|
||||
'kuaishou_cloud_role_confirmed',
|
||||
{
|
||||
vnPhone: flow.binding.vnPhone,
|
||||
roleName: flow.binding.roleName,
|
||||
roleId: flow.binding.roleId,
|
||||
},
|
||||
now,
|
||||
)
|
||||
|
||||
return getKuaishouCloudClaimDetail(token)
|
||||
}
|
||||
@@ -474,10 +509,15 @@ export async function redeemKuaishouCloudClaim(token: unknown) {
|
||||
updated_at: nowIso(),
|
||||
})
|
||||
|
||||
await createTaskEvent(lockedTask.id, 'kuaishou_cloud_redeem_failed', {
|
||||
source: 'claim_page_redeem',
|
||||
errorMessage: message,
|
||||
}, nowIso())
|
||||
await createTaskEvent(
|
||||
lockedTask.id,
|
||||
'kuaishou_cloud_redeem_failed',
|
||||
{
|
||||
source: 'claim_page_redeem',
|
||||
errorMessage: message,
|
||||
},
|
||||
nowIso(),
|
||||
)
|
||||
|
||||
throw error
|
||||
}
|
||||
@@ -490,10 +530,10 @@ function isKuaishouCloudMockTask(task: Partial<TaskRow> | null | undefined) {
|
||||
}
|
||||
|
||||
function isKuaishouCloudMockContext(context: JsonObject = {}) {
|
||||
const flow = context.kuaishouCloudFulfillment
|
||||
&& typeof context.kuaishouCloudFulfillment === 'object'
|
||||
? context.kuaishouCloudFulfillment
|
||||
: {}
|
||||
const flow =
|
||||
context.kuaishouCloudFulfillment && typeof context.kuaishouCloudFulfillment === 'object'
|
||||
? context.kuaishouCloudFulfillment
|
||||
: {}
|
||||
const mock = flow.mock && typeof flow.mock === 'object' ? flow.mock : context.mock
|
||||
|
||||
return Boolean(mock && typeof mock === 'object' && mock.enabled === true)
|
||||
@@ -502,8 +542,9 @@ function isKuaishouCloudMockContext(context: JsonObject = {}) {
|
||||
function buildMockVerifiedKuaishouCloudFlow(value: unknown, timestamp: string) {
|
||||
const flow = normalizeKuaishouCloudFlow(value)
|
||||
const source = flow as JsonObject
|
||||
const bindUrl = flow.binding.bindUrl
|
||||
|| `https://example.com/mock-kuaishou-cloud-bind?task=mock&ts=${encodeURIComponent(timestamp)}`
|
||||
const bindUrl =
|
||||
flow.binding.bindUrl ||
|
||||
`https://example.com/mock-kuaishou-cloud-bind?task=mock&ts=${encodeURIComponent(timestamp)}`
|
||||
const vnPhone = flow.binding.vnPhone || '13800000000'
|
||||
const roleName = flow.binding.roleName || flow.role.name || '测试角色'
|
||||
const roleId = flow.binding.roleId || flow.role.rid || '10001'
|
||||
@@ -590,8 +631,13 @@ async function completeMockKuaishouCloudClaimTask(task: TaskRow, timestamp: stri
|
||||
updated_at: timestamp,
|
||||
})
|
||||
|
||||
await createTaskEvent(task.id, 'kuaishou_cloud_mock_redeemed', {
|
||||
source: 'claim_page_mock',
|
||||
deliveryItems: flow.deliveryItems,
|
||||
}, timestamp)
|
||||
await createTaskEvent(
|
||||
task.id,
|
||||
'kuaishou_cloud_mock_redeemed',
|
||||
{
|
||||
source: 'claim_page_mock',
|
||||
deliveryItems: flow.deliveryItems,
|
||||
},
|
||||
timestamp,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,98 +1,77 @@
|
||||
import { resolveCloudtentaclesConfig } from "../../platforms/cloudtentacles/helpers.js";
|
||||
import {
|
||||
maskCode as maskCodeValue,
|
||||
maskPhone as maskPhoneValue,
|
||||
} from "../../../utils/masking.js";
|
||||
import { resolveCloudtentaclesConfig } from '../../platforms/cloudtentacles/helpers.js'
|
||||
import { maskCode as maskCodeValue, maskPhone as maskPhoneValue } from '../../../utils/masking.js'
|
||||
|
||||
export const KUAISHOU_CLOUD_FIXED_VN_KEY = "1";
|
||||
export const KUAISHOU_CLOUD_FIXED_VN_KEY = '1'
|
||||
|
||||
export type JsonObject = Record<string, any>;
|
||||
export type JsonObject = Record<string, any>
|
||||
|
||||
export function isKuaishouCloudTask(task: unknown) {
|
||||
const source = task && typeof task === "object" ? task as JsonObject : {};
|
||||
return String(source.executor_key || "").trim() === "kuaishou_ct_assisted";
|
||||
const source = task && typeof task === 'object' ? (task as JsonObject) : {}
|
||||
return String(source.executor_key || '').trim() === 'kuaishou_ct_assisted'
|
||||
}
|
||||
|
||||
export function normalizeKuaishouCloudFlow(value: unknown) {
|
||||
const source: JsonObject = value && typeof value === "object" ? value as JsonObject : {};
|
||||
const binding =
|
||||
source.binding && typeof source.binding === "object" ? source.binding : {};
|
||||
const role =
|
||||
source.role && typeof source.role === "object" ? source.role : {};
|
||||
const purchase =
|
||||
source.purchase && typeof source.purchase === "object"
|
||||
? source.purchase
|
||||
: {};
|
||||
const dispatch =
|
||||
source.dispatch && typeof source.dispatch === "object"
|
||||
? source.dispatch
|
||||
: {};
|
||||
const source: JsonObject = value && typeof value === 'object' ? (value as JsonObject) : {}
|
||||
const binding = source.binding && typeof source.binding === 'object' ? source.binding : {}
|
||||
const role = source.role && typeof source.role === 'object' ? source.role : {}
|
||||
const purchase = source.purchase && typeof source.purchase === 'object' ? source.purchase : {}
|
||||
const dispatch = source.dispatch && typeof source.dispatch === 'object' ? source.dispatch : {}
|
||||
const 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 deliveryItems = normalizeDeliveryItems(source, binding);
|
||||
source.returnNumber && typeof source.returnNumber === 'object' ? source.returnNumber : {}
|
||||
const consume = source.consume && typeof source.consume === 'object' ? source.consume : {}
|
||||
const ticket = source.ticket && typeof source.ticket === 'object' ? source.ticket : {}
|
||||
const rebind = source.rebind && typeof source.rebind === 'object' ? source.rebind : {}
|
||||
const deliveryItems = normalizeDeliveryItems(source, binding)
|
||||
|
||||
const roleName = String(role.name || binding.roleName || "").trim();
|
||||
const roleId = String(role.rid || binding.roleId || "").trim();
|
||||
const bindPreparedAt = binding.bindPreparedAt || null;
|
||||
const roleName = String(role.name || binding.roleName || '').trim()
|
||||
const roleId = String(role.rid || binding.roleId || '').trim()
|
||||
const bindPreparedAt = binding.bindPreparedAt || null
|
||||
const bindExpiresAt =
|
||||
binding.bindExpiresAt ||
|
||||
resolveKuaishouCloudBindUrlExpiresAt(bindPreparedAt);
|
||||
binding.bindExpiresAt || resolveKuaishouCloudBindUrlExpiresAt(bindPreparedAt)
|
||||
|
||||
return {
|
||||
...source,
|
||||
configId: String(source.configId || "").trim(),
|
||||
internalSkuCode: String(source.internalSkuCode || "").trim(),
|
||||
internalSkuName: String(source.internalSkuName || "").trim(),
|
||||
configId: String(source.configId || '').trim(),
|
||||
internalSkuCode: String(source.internalSkuCode || '').trim(),
|
||||
internalSkuName: String(source.internalSkuName || '').trim(),
|
||||
deliveryItems,
|
||||
ticket: {
|
||||
code: String(ticket.code || "").trim(),
|
||||
status: String(ticket.status || "pending").trim() || "pending",
|
||||
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(),
|
||||
oid: String(ticket.oid || '').trim(),
|
||||
formToken: String(ticket.formToken || '').trim(),
|
||||
leftCount: Number(ticket.leftCount || 0) || 0,
|
||||
goodsTitle: String(ticket.goodsTitle || "").trim(),
|
||||
goodsTitle: String(ticket.goodsTitle || '').trim(),
|
||||
},
|
||||
binding: {
|
||||
prepareStatus:
|
||||
String(binding.prepareStatus || "pending").trim() || "pending",
|
||||
prepareStatus: String(binding.prepareStatus || 'pending').trim() || 'pending',
|
||||
cloudSourceKeys: normalizeStringArray(binding.cloudSourceKeys),
|
||||
resolvedSourceKey: String(binding.resolvedSourceKey || "").trim(),
|
||||
resolvedSourceKey: String(binding.resolvedSourceKey || '').trim(),
|
||||
skuId: Number(binding.skuId || 0) || 0,
|
||||
skuName: String(binding.skuName || "").trim(),
|
||||
skuName: String(binding.skuName || '').trim(),
|
||||
vnKey:
|
||||
String(binding.vnKey || KUAISHOU_CLOUD_FIXED_VN_KEY).trim() ||
|
||||
KUAISHOU_CLOUD_FIXED_VN_KEY,
|
||||
String(binding.vnKey || KUAISHOU_CLOUD_FIXED_VN_KEY).trim() || KUAISHOU_CLOUD_FIXED_VN_KEY,
|
||||
vnId: Number(binding.vnId || 0) || 0,
|
||||
vnPhone: String(binding.vnPhone || "").trim(),
|
||||
bindUrl: String(binding.bindUrl || "").trim(),
|
||||
vnPhone: String(binding.vnPhone || '').trim(),
|
||||
bindUrl: String(binding.bindUrl || '').trim(),
|
||||
bindPreparedAt,
|
||||
bindExpiresAt,
|
||||
bindProbeAt: binding.bindProbeAt || null,
|
||||
bindProbeStatus: String(binding.bindProbeStatus || "").trim(),
|
||||
bindProbeMessage: String(binding.bindProbeMessage || "").trim(),
|
||||
bindProbeStatus: String(binding.bindProbeStatus || '').trim(),
|
||||
bindProbeMessage: String(binding.bindProbeMessage || '').trim(),
|
||||
roleName,
|
||||
roleId,
|
||||
},
|
||||
role: {
|
||||
status:
|
||||
String(
|
||||
role.status || (roleName || roleId ? "ready" : "pending")
|
||||
).trim() || "pending",
|
||||
status: String(role.status || (roleName || roleId ? 'ready' : 'pending')).trim() || 'pending',
|
||||
name: roleName,
|
||||
rid: roleId,
|
||||
refreshedAt: role.refreshedAt || null,
|
||||
errorMessage: String(role.errorMessage || "").trim(),
|
||||
rawInfo:
|
||||
role.rawInfo && typeof role.rawInfo === "object" ? role.rawInfo : null,
|
||||
errorMessage: String(role.errorMessage || '').trim(),
|
||||
rawInfo: role.rawInfo && typeof role.rawInfo === 'object' ? role.rawInfo : null,
|
||||
},
|
||||
purchase: {
|
||||
autoBuyEnabled: purchase.autoBuyEnabled !== false,
|
||||
@@ -105,171 +84,166 @@ export function normalizeKuaishouCloudFlow(value: unknown) {
|
||||
items: Array.isArray(purchase.items) ? purchase.items : [],
|
||||
},
|
||||
dispatch: {
|
||||
status: String(dispatch.status || "pending").trim() || "pending",
|
||||
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(),
|
||||
note: String(dispatch.note || '').trim(),
|
||||
items: Array.isArray(dispatch.items) ? dispatch.items : [],
|
||||
},
|
||||
returnNumber: {
|
||||
status: String(returnNumber.status || "pending").trim() || "pending",
|
||||
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(),
|
||||
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(),
|
||||
errorMessage: String(consume.errorMessage || '').trim(),
|
||||
},
|
||||
notes: String(source.notes || "").trim(),
|
||||
};
|
||||
rebind: {
|
||||
currentAttempt: Math.max(0, Number(rebind.currentAttempt || 0) || 0),
|
||||
history: Array.isArray(rebind.history) ? rebind.history : [],
|
||||
},
|
||||
notes: String(source.notes || '').trim(),
|
||||
}
|
||||
}
|
||||
|
||||
export function normalizeKuaishouCloudDeliveryItems(value: unknown) {
|
||||
const source: JsonObject = value && typeof value === "object" ? value as JsonObject : {};
|
||||
const binding =
|
||||
source.binding && typeof source.binding === "object" ? source.binding : {};
|
||||
return normalizeDeliveryItems(source, binding);
|
||||
const source: JsonObject = value && typeof value === 'object' ? (value as JsonObject) : {}
|
||||
const binding = source.binding && typeof source.binding === 'object' ? source.binding : {}
|
||||
return normalizeDeliveryItems(source, binding)
|
||||
}
|
||||
|
||||
export function normalizeKuaishouCloudRoleInfo(value: unknown) {
|
||||
const rawInfo: JsonObject | null = value && typeof value === "object" ? value as JsonObject : null;
|
||||
const rawInfo: JsonObject | null =
|
||||
value && typeof value === 'object' ? (value as JsonObject) : null
|
||||
const nestedBindInfo =
|
||||
rawInfo?.sBindInfo && typeof rawInfo.sBindInfo === "object"
|
||||
? rawInfo.sBindInfo
|
||||
: null;
|
||||
const source = nestedBindInfo || rawInfo;
|
||||
rawInfo?.sBindInfo && typeof rawInfo.sBindInfo === 'object' ? rawInfo.sBindInfo : null
|
||||
const source = nestedBindInfo || rawInfo
|
||||
|
||||
return {
|
||||
name: String(
|
||||
source?.name ||
|
||||
source?.roleName ||
|
||||
source?.nickname ||
|
||||
source?.sRoleName ||
|
||||
""
|
||||
source?.name || source?.roleName || source?.nickname || source?.sRoleName || '',
|
||||
).trim(),
|
||||
rid: String(
|
||||
source?.rid ||
|
||||
source?.roleId ||
|
||||
source?.uid ||
|
||||
source?.sRoleId ||
|
||||
source?.sUserId ||
|
||||
""
|
||||
source?.rid || source?.roleId || source?.uid || source?.sRoleId || source?.sUserId || '',
|
||||
).trim(),
|
||||
rawInfo,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export function maskPhone(value: unknown) {
|
||||
return maskPhoneValue(value);
|
||||
return maskPhoneValue(value)
|
||||
}
|
||||
|
||||
export function maskCode(value: unknown) {
|
||||
return maskCodeValue(value);
|
||||
return maskCodeValue(value)
|
||||
}
|
||||
|
||||
export function resolveKuaishouCloudBindUrlExpiresAt(preparedAt: unknown) {
|
||||
const preparedTime = Date.parse(String(preparedAt || ""));
|
||||
const preparedTime = Date.parse(String(preparedAt || ''))
|
||||
if (!Number.isFinite(preparedTime)) {
|
||||
return null;
|
||||
return null
|
||||
}
|
||||
|
||||
const config = resolveCloudtentaclesConfig();
|
||||
const ttlSeconds = Number(config.bindUrlTtlSeconds || 600);
|
||||
return new Date(preparedTime + Math.max(1, ttlSeconds) * 1000).toISOString();
|
||||
const config = resolveCloudtentaclesConfig()
|
||||
const ttlSeconds = Number(config.bindUrlTtlSeconds || 600)
|
||||
return new Date(preparedTime + Math.max(1, ttlSeconds) * 1000).toISOString()
|
||||
}
|
||||
|
||||
export function isKuaishouCloudBindUrlFresh(flow: unknown, now = new Date()) {
|
||||
const normalizedFlow = normalizeKuaishouCloudFlow(flow);
|
||||
const normalizedFlow = normalizeKuaishouCloudFlow(flow)
|
||||
if (!normalizedFlow.binding.bindUrl) {
|
||||
return false;
|
||||
return false
|
||||
}
|
||||
|
||||
const expiresAt = normalizedFlow.binding.bindExpiresAt;
|
||||
const expiresAt = normalizedFlow.binding.bindExpiresAt
|
||||
if (!expiresAt) {
|
||||
return false;
|
||||
return false
|
||||
}
|
||||
|
||||
const expiresTime = Date.parse(String(expiresAt || ""));
|
||||
const expiresTime = Date.parse(String(expiresAt || ''))
|
||||
if (!Number.isFinite(expiresTime)) {
|
||||
return false;
|
||||
return false
|
||||
}
|
||||
|
||||
return expiresTime > now.getTime();
|
||||
return expiresTime > now.getTime()
|
||||
}
|
||||
|
||||
export function normalizeStringArray(value: unknown) {
|
||||
if (Array.isArray(value)) {
|
||||
return value.map((v) => String(v || "").trim()).filter(Boolean);
|
||||
return value.map((v) => String(v || '').trim()).filter(Boolean)
|
||||
}
|
||||
if (typeof value === "string") {
|
||||
if (typeof value === 'string') {
|
||||
return value
|
||||
.split(",")
|
||||
.split(',')
|
||||
.map((v) => v.trim())
|
||||
.filter(Boolean);
|
||||
.filter(Boolean)
|
||||
}
|
||||
return [];
|
||||
return []
|
||||
}
|
||||
|
||||
function normalizeDeliveryItems(source: JsonObject, binding: JsonObject) {
|
||||
const rawItems = Array.isArray(source.deliveryItems) ? source.deliveryItems : [];
|
||||
const items = rawItems
|
||||
.map((item) => normalizeDeliveryItem(item))
|
||||
.filter(Boolean) as Array<{ cloudSkuId: number; cloudSkuName: string; quantity: number }>;
|
||||
const rawItems = Array.isArray(source.deliveryItems) ? source.deliveryItems : []
|
||||
const items = rawItems.map((item) => normalizeDeliveryItem(item)).filter(Boolean) as Array<{
|
||||
cloudSkuId: number
|
||||
cloudSkuName: string
|
||||
quantity: number
|
||||
}>
|
||||
|
||||
if (items.length > 0) {
|
||||
return mergeDeliveryItems(items);
|
||||
return mergeDeliveryItems(items)
|
||||
}
|
||||
|
||||
const skuId = Number(binding.skuId || 0) || 0;
|
||||
const skuId = Number(binding.skuId || 0) || 0
|
||||
if (!skuId) {
|
||||
return [];
|
||||
return []
|
||||
}
|
||||
|
||||
return [
|
||||
{
|
||||
cloudSkuId: skuId,
|
||||
cloudSkuName: String(binding.skuName || "").trim(),
|
||||
cloudSkuName: String(binding.skuName || '').trim(),
|
||||
quantity: 1,
|
||||
},
|
||||
];
|
||||
]
|
||||
}
|
||||
|
||||
function normalizeDeliveryItem(value: unknown) {
|
||||
const source = value && typeof value === "object" ? value as JsonObject : {};
|
||||
const cloudSkuId = Number(source.cloudSkuId || source.skuId || 0) || 0;
|
||||
const source = value && typeof value === 'object' ? (value as JsonObject) : {}
|
||||
const cloudSkuId = Number(source.cloudSkuId || source.skuId || 0) || 0
|
||||
if (!Number.isInteger(cloudSkuId) || cloudSkuId <= 0) {
|
||||
return null;
|
||||
return null
|
||||
}
|
||||
|
||||
const quantity = Number(source.quantity || 1) || 1;
|
||||
const quantity = Number(source.quantity || 1) || 1
|
||||
return {
|
||||
cloudSkuId,
|
||||
cloudSkuName: String(source.cloudSkuName || source.skuName || "").trim(),
|
||||
cloudSkuName: String(source.cloudSkuName || source.skuName || '').trim(),
|
||||
quantity: Number.isInteger(quantity) && quantity > 0 ? quantity : 1,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function mergeDeliveryItems(
|
||||
items: Array<{ cloudSkuId: number; cloudSkuName: string; quantity: number }>
|
||||
items: Array<{ cloudSkuId: number; cloudSkuName: string; quantity: number }>,
|
||||
) {
|
||||
const merged = new Map<number, { cloudSkuId: number; cloudSkuName: string; quantity: number }>();
|
||||
const merged = new Map<number, { cloudSkuId: number; cloudSkuName: string; quantity: number }>()
|
||||
|
||||
for (const item of items) {
|
||||
const existing = merged.get(item.cloudSkuId);
|
||||
const existing = merged.get(item.cloudSkuId)
|
||||
if (existing) {
|
||||
existing.quantity += item.quantity;
|
||||
existing.cloudSkuName = existing.cloudSkuName || item.cloudSkuName;
|
||||
continue;
|
||||
existing.quantity += item.quantity
|
||||
existing.cloudSkuName = existing.cloudSkuName || item.cloudSkuName
|
||||
continue
|
||||
}
|
||||
|
||||
merged.set(item.cloudSkuId, { ...item });
|
||||
merged.set(item.cloudSkuId, { ...item })
|
||||
}
|
||||
|
||||
return Array.from(merged.values());
|
||||
return Array.from(merged.values())
|
||||
}
|
||||
|
||||
@@ -456,6 +456,286 @@ export async function refreshKuaishouCloudTaskBindUrl(task: TaskRow, options: Js
|
||||
}
|
||||
}
|
||||
|
||||
export async function rebindKuaishouCloudTaskRole(task: TaskRow, options: JsonObject = {}) {
|
||||
if (!isKuaishouCloudTask(task)) {
|
||||
throw createHttpError('当前任务不是快手 Cloud 履约任务', {
|
||||
statusCode: 409,
|
||||
errorCode: 'kuaishou_cloud_task_invalid',
|
||||
})
|
||||
}
|
||||
|
||||
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)
|
||||
) {
|
||||
throw createHttpError('当前任务状态不可换绑角色', {
|
||||
statusCode: 409,
|
||||
errorCode: 'kuaishou_cloud_rebind_not_allowed',
|
||||
})
|
||||
}
|
||||
|
||||
const now = nowIso()
|
||||
const actor = normalizeActor(options.actor)
|
||||
const taskContext = parseTaskContext(task)
|
||||
const flow = normalizeKuaishouCloudFlow(taskContext.kuaishouCloudFulfillment)
|
||||
|
||||
if (
|
||||
flow.dispatch.status === 'success' ||
|
||||
normalizeTaskStatus(task.task_status) === TASK_STATUS.DISPATCHED_PENDING_RETURN
|
||||
) {
|
||||
throw createHttpError('当前任务已经发货,不能换绑角色', {
|
||||
statusCode: 409,
|
||||
errorCode: 'kuaishou_cloud_rebind_after_dispatch_forbidden',
|
||||
})
|
||||
}
|
||||
|
||||
if (!flow.binding.vnId || !flow.binding.vnKey) {
|
||||
throw createHttpError('当前任务缺少可退还的虚拟号信息,请先准备绑定资源', {
|
||||
statusCode: 409,
|
||||
errorCode: 'kuaishou_cloud_rebind_missing_binding_context',
|
||||
})
|
||||
}
|
||||
|
||||
const source = String(options.source || 'system_rebind_role').trim() || 'system_rebind_role'
|
||||
const cloudContext = resolvePersistedCloudtentaclesContextBySourceKeys([
|
||||
flow.binding.resolvedSourceKey,
|
||||
...flow.binding.cloudSourceKeys,
|
||||
])
|
||||
const oldBinding = {
|
||||
vnKey: flow.binding.vnKey,
|
||||
vnId: flow.binding.vnId,
|
||||
vnPhone: flow.binding.vnPhone,
|
||||
bindUrl: flow.binding.bindUrl,
|
||||
bindPreparedAt: flow.binding.bindPreparedAt,
|
||||
bindExpiresAt: flow.binding.bindExpiresAt,
|
||||
roleName: flow.binding.roleName || flow.role.name || task.role_name || '',
|
||||
roleId: flow.binding.roleId || flow.role.rid || task.role_id || '',
|
||||
}
|
||||
const previousRebind: JsonObject =
|
||||
flow.rebind && typeof flow.rebind === 'object' ? (flow.rebind as JsonObject) : {}
|
||||
const history = Array.isArray(previousRebind.history) ? previousRebind.history : []
|
||||
const attempt = Math.max(1, Number(previousRebind.currentAttempt || history.length || 0) + 1)
|
||||
const baseHistoryItem = {
|
||||
attempt,
|
||||
source,
|
||||
requestedAt: now,
|
||||
requestedBy: actor,
|
||||
oldBinding,
|
||||
}
|
||||
|
||||
await createTaskEvent(
|
||||
task.id,
|
||||
'kuaishou_cloud_rebind_requested',
|
||||
{
|
||||
source,
|
||||
attempt,
|
||||
oldVnId: oldBinding.vnId,
|
||||
oldVnPhoneMasked: maskPhone(oldBinding.vnPhone),
|
||||
oldRoleName: oldBinding.roleName,
|
||||
oldRoleId: oldBinding.roleId,
|
||||
actor,
|
||||
},
|
||||
now,
|
||||
)
|
||||
|
||||
await backCloudtentaclesVirtualNumber({
|
||||
...cloudContext,
|
||||
key: oldBinding.vnKey,
|
||||
id: oldBinding.vnId,
|
||||
})
|
||||
|
||||
await createTaskEvent(
|
||||
task.id,
|
||||
'kuaishou_cloud_rebind_old_number_returned',
|
||||
{
|
||||
source,
|
||||
attempt,
|
||||
vnKey: oldBinding.vnKey,
|
||||
vnId: oldBinding.vnId,
|
||||
vnPhoneMasked: maskPhone(oldBinding.vnPhone),
|
||||
actor,
|
||||
},
|
||||
now,
|
||||
)
|
||||
|
||||
let preparedBinding
|
||||
try {
|
||||
preparedBinding = await prepareKuaishouCloudBindResourceWithFallback({
|
||||
cloudContext,
|
||||
vnKeyCandidates: resolveKuaishouCloudVnKeyCandidates({
|
||||
flow,
|
||||
binding: flow.binding,
|
||||
}),
|
||||
})
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : '新绑定资源准备失败'
|
||||
const failedContext = {
|
||||
...taskContext,
|
||||
kuaishouCloudFulfillment: {
|
||||
...flow,
|
||||
binding: {
|
||||
...flow.binding,
|
||||
prepareStatus: 'pending',
|
||||
vnId: 0,
|
||||
vnPhone: '',
|
||||
bindUrl: '',
|
||||
bindPreparedAt: null,
|
||||
bindExpiresAt: null,
|
||||
bindProbeAt: now,
|
||||
bindProbeStatus: 'rebind_failed',
|
||||
bindProbeMessage: errorMessage,
|
||||
roleName: '',
|
||||
roleId: '',
|
||||
},
|
||||
role: {
|
||||
status: 'pending',
|
||||
name: '',
|
||||
rid: '',
|
||||
refreshedAt: now,
|
||||
errorMessage: `旧虚拟号已退还,新绑定资源准备失败:${errorMessage}`,
|
||||
rawInfo: null,
|
||||
},
|
||||
rebind: {
|
||||
...previousRebind,
|
||||
currentAttempt: attempt,
|
||||
history: [
|
||||
...history,
|
||||
{
|
||||
...baseHistoryItem,
|
||||
status: 'failed',
|
||||
errorMessage,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
}
|
||||
const failedTask = await updateTask(task.id, {
|
||||
task_status: TASK_STATUS.PENDING_BINDING_PREPARE,
|
||||
role_id: '',
|
||||
role_name: '',
|
||||
role_confirmed_at: null,
|
||||
last_error: `换绑失败,旧虚拟号已退还,新绑定资源准备失败:${errorMessage}`,
|
||||
context_json: JSON.stringify(failedContext),
|
||||
updated_at: now,
|
||||
})
|
||||
|
||||
await createTaskEvent(
|
||||
task.id,
|
||||
'kuaishou_cloud_rebind_failed',
|
||||
{
|
||||
source,
|
||||
attempt,
|
||||
errorMessage,
|
||||
actor,
|
||||
},
|
||||
now,
|
||||
)
|
||||
|
||||
return {
|
||||
task: failedTask || task,
|
||||
claimUrl: buildClaimUrl(String(task.primary_claim_token || task.claim_token || '')),
|
||||
token: String(task.primary_claim_token || task.claim_token || ''),
|
||||
flow: normalizeKuaishouCloudFlow(
|
||||
parseTaskContext(failedTask || task).kuaishouCloudFulfillment,
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
const claimLinkState = await ensureTaskClaimLink(task)
|
||||
const nextBindExpiresAt = resolveKuaishouCloudBindUrlExpiresAt(now)
|
||||
const nextContext = {
|
||||
...taskContext,
|
||||
kuaishouCloudFulfillment: {
|
||||
...flow,
|
||||
binding: {
|
||||
...flow.binding,
|
||||
prepareStatus: 'ready',
|
||||
vnKey: preparedBinding.vnKey,
|
||||
vnId: preparedBinding.vnId,
|
||||
vnPhone: preparedBinding.vnPhone,
|
||||
bindUrl: preparedBinding.bindUrl,
|
||||
bindPreparedAt: now,
|
||||
bindExpiresAt: nextBindExpiresAt,
|
||||
bindProbeAt: null,
|
||||
bindProbeStatus: 'pending',
|
||||
bindProbeMessage: '',
|
||||
roleName: '',
|
||||
roleId: '',
|
||||
},
|
||||
role: {
|
||||
status: 'pending',
|
||||
name: '',
|
||||
rid: '',
|
||||
refreshedAt: null,
|
||||
errorMessage: '',
|
||||
rawInfo: null,
|
||||
},
|
||||
rebind: {
|
||||
...previousRebind,
|
||||
currentAttempt: attempt,
|
||||
history: [
|
||||
...history,
|
||||
{
|
||||
...baseHistoryItem,
|
||||
newBinding: {
|
||||
vnKey: preparedBinding.vnKey,
|
||||
vnId: preparedBinding.vnId,
|
||||
vnPhone: preparedBinding.vnPhone,
|
||||
bindUrl: preparedBinding.bindUrl,
|
||||
bindPreparedAt: now,
|
||||
bindExpiresAt: nextBindExpiresAt,
|
||||
},
|
||||
status: 'success',
|
||||
errorMessage: '',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
const updatedTask = await updateTask(task.id, {
|
||||
task_status: TASK_STATUS.WAITING_BINDING,
|
||||
user_action_status: 'pending_claim',
|
||||
claim_token: claimLinkState.token || task.claim_token || '',
|
||||
claim_expires_at: claimLinkState.expiredAt || getTaskClaimExpiresAt(task),
|
||||
role_id: '',
|
||||
role_name: '',
|
||||
role_confirmed_at: null,
|
||||
last_error: '',
|
||||
context_json: JSON.stringify(nextContext),
|
||||
updated_at: now,
|
||||
})
|
||||
|
||||
await createTaskEvent(
|
||||
task.id,
|
||||
'kuaishou_cloud_rebind_prepared',
|
||||
{
|
||||
source,
|
||||
attempt,
|
||||
oldVnId: oldBinding.vnId,
|
||||
oldVnPhoneMasked: maskPhone(oldBinding.vnPhone),
|
||||
vnKey: preparedBinding.vnKey,
|
||||
vnId: preparedBinding.vnId,
|
||||
vnPhoneMasked: maskPhone(preparedBinding.vnPhone),
|
||||
bindUrl: preparedBinding.bindUrl,
|
||||
actor,
|
||||
},
|
||||
now,
|
||||
)
|
||||
|
||||
return {
|
||||
task: updatedTask,
|
||||
claimUrl: claimLinkState.claimUrl,
|
||||
token: claimLinkState.token,
|
||||
flow: normalizeKuaishouCloudFlow(nextContext.kuaishouCloudFulfillment),
|
||||
}
|
||||
}
|
||||
|
||||
export async function probeKuaishouCloudTaskBindUrl(task: TaskRow, options: JsonObject = {}) {
|
||||
if (!isKuaishouCloudTask(task)) {
|
||||
throw createHttpError('当前任务不是快手 Cloud 履约任务', {
|
||||
|
||||
Reference in New Issue
Block a user