添加快手 Cloud 换绑角色功能
This commit is contained in:
@@ -17,7 +17,7 @@ export const TASK_STATUS = {
|
|||||||
CLOSED: 'closed',
|
CLOSED: 'closed',
|
||||||
} as const
|
} 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 & {})
|
export type TaskStatus = KnownTaskStatus | (string & {})
|
||||||
|
|
||||||
const TASK_TRANSITIONS: Record<KnownTaskStatus, readonly KnownTaskStatus[]> = {
|
const TASK_TRANSITIONS: Record<KnownTaskStatus, readonly KnownTaskStatus[]> = {
|
||||||
@@ -66,6 +66,7 @@ const TASK_TRANSITIONS: Record<KnownTaskStatus, readonly KnownTaskStatus[]> = {
|
|||||||
TASK_STATUS.CLOSED,
|
TASK_STATUS.CLOSED,
|
||||||
],
|
],
|
||||||
[TASK_STATUS.ROLE_CONFIRMED]: [
|
[TASK_STATUS.ROLE_CONFIRMED]: [
|
||||||
|
TASK_STATUS.WAITING_BINDING,
|
||||||
TASK_STATUS.REDEEMING,
|
TASK_STATUS.REDEEMING,
|
||||||
TASK_STATUS.RETRY_PENDING,
|
TASK_STATUS.RETRY_PENDING,
|
||||||
TASK_STATUS.MANUAL_REVIEW,
|
TASK_STATUS.MANUAL_REVIEW,
|
||||||
@@ -102,11 +103,7 @@ const TASK_TRANSITIONS: Record<KnownTaskStatus, readonly KnownTaskStatus[]> = {
|
|||||||
TASK_STATUS.REDEEMED,
|
TASK_STATUS.REDEEMED,
|
||||||
TASK_STATUS.CLOSED,
|
TASK_STATUS.CLOSED,
|
||||||
],
|
],
|
||||||
[TASK_STATUS.FAILED]: [
|
[TASK_STATUS.FAILED]: [TASK_STATUS.RETRY_PENDING, TASK_STATUS.MANUAL_REVIEW, TASK_STATUS.CLOSED],
|
||||||
TASK_STATUS.RETRY_PENDING,
|
|
||||||
TASK_STATUS.MANUAL_REVIEW,
|
|
||||||
TASK_STATUS.CLOSED,
|
|
||||||
],
|
|
||||||
[TASK_STATUS.COMPLETED]: [],
|
[TASK_STATUS.COMPLETED]: [],
|
||||||
[TASK_STATUS.REDEEMED]: [],
|
[TASK_STATUS.REDEEMED]: [],
|
||||||
[TASK_STATUS.EXPIRED]: [],
|
[TASK_STATUS.EXPIRED]: [],
|
||||||
@@ -206,6 +203,13 @@ const KUAISHOU_CLOUD_PREPARABLE_STATUSES = new Set<TaskStatus>([
|
|||||||
TASK_STATUS.FAILED,
|
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 {
|
export function normalizeTaskStatus(value: unknown): TaskStatus {
|
||||||
return String(value || '').trim() as 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))
|
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 {
|
export function canDispatchKuaishouCloudFulfillmentStatus(status: unknown): boolean {
|
||||||
return normalizeTaskStatus(status) === TASK_STATUS.WAITING_BINDING
|
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
|
return normalizeTaskStatus(status) === TASK_STATUS.DISPATCHED_PENDING_RETURN
|
||||||
}
|
}
|
||||||
|
|
||||||
export function resolveInitialPaidTaskStatus(profile: {
|
export function resolveInitialPaidTaskStatus(
|
||||||
requires_claim?: boolean
|
profile:
|
||||||
executor_key?: unknown
|
| {
|
||||||
} | null | undefined): TaskStatus {
|
requires_claim?: boolean
|
||||||
|
executor_key?: unknown
|
||||||
|
}
|
||||||
|
| null
|
||||||
|
| undefined,
|
||||||
|
): TaskStatus {
|
||||||
if (Boolean(profile?.requires_claim)) {
|
if (Boolean(profile?.requires_claim)) {
|
||||||
return TASK_STATUS.PAID
|
return TASK_STATUS.PAID
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,177 +1,166 @@
|
|||||||
import type { Request } from "express";
|
import type { Request } from 'express'
|
||||||
import { Router } from "express";
|
import { Router } from 'express'
|
||||||
|
|
||||||
import {
|
import { getAdminTaskDetail, getAdminTasks } from '../../services/admin/admin-read-service.js'
|
||||||
getAdminTaskDetail,
|
import { closeAdminTask } from '../../services/admin/write/task-actions.js'
|
||||||
getAdminTasks,
|
|
||||||
} from "../../services/admin/admin-read-service.js";
|
|
||||||
import { closeAdminTask } from "../../services/admin/write/task-actions.js";
|
|
||||||
import {
|
import {
|
||||||
dispatchAdminTaskKuaishouCloudFulfillment,
|
dispatchAdminTaskKuaishouCloudFulfillment,
|
||||||
prepareAdminTaskKuaishouCloudFulfillment,
|
prepareAdminTaskKuaishouCloudFulfillment,
|
||||||
|
rebindAdminTaskKuaishouCloudRole,
|
||||||
refreshAdminTaskKuaishouCloudRoleInfo,
|
refreshAdminTaskKuaishouCloudRoleInfo,
|
||||||
returnNumberAdminTaskKuaishouCloudFulfillment,
|
returnNumberAdminTaskKuaishouCloudFulfillment,
|
||||||
} from "../../services/admin/write/kuaishou-cloud-actions.js";
|
} from '../../services/admin/write/kuaishou-cloud-actions.js'
|
||||||
import {
|
import { createJsonHandler, requireAdminRoles } from './session.js'
|
||||||
createJsonHandler,
|
|
||||||
requireAdminRoles,
|
|
||||||
} from "./session.js";
|
|
||||||
import type {
|
import type {
|
||||||
AdminTaskKuaishouCloudDispatchRouteBody,
|
AdminTaskKuaishouCloudDispatchRouteBody,
|
||||||
AdminTaskRouteParams,
|
AdminTaskRouteParams,
|
||||||
AdminTaskRouteQuery,
|
AdminTaskRouteQuery,
|
||||||
} from "../../types/admin/route-inputs.js";
|
} from '../../types/admin/route-inputs.js'
|
||||||
import type { AdminTaskActionResponse } from "../../types/admin/write-models.js";
|
import type { AdminTaskActionResponse } from '../../types/admin/write-models.js'
|
||||||
|
|
||||||
const router = Router();
|
const router = Router()
|
||||||
|
|
||||||
function getTaskId(req: Request): string {
|
function getTaskId(req: Request): string {
|
||||||
return String((req.params as AdminTaskRouteParams).taskId || "");
|
return String((req.params as AdminTaskRouteParams).taskId || '')
|
||||||
}
|
}
|
||||||
|
|
||||||
router.get(
|
router.get(
|
||||||
"/tasks",
|
'/tasks',
|
||||||
createJsonHandler(
|
createJsonHandler(
|
||||||
(req) =>
|
(req) => getAdminTasks(req.query as AdminTaskRouteQuery, req.adminSession || null),
|
||||||
getAdminTasks(req.query as AdminTaskRouteQuery, req.adminSession || null),
|
|
||||||
{
|
{
|
||||||
successMessage: "ok",
|
successMessage: 'ok',
|
||||||
errorMessage: "读取任务列表失败",
|
errorMessage: '读取任务列表失败',
|
||||||
scope: "[admin/tasks]",
|
scope: '[admin/tasks]',
|
||||||
}
|
},
|
||||||
)
|
),
|
||||||
);
|
)
|
||||||
|
|
||||||
router.get(
|
router.get(
|
||||||
"/tasks/:taskId",
|
'/tasks/:taskId',
|
||||||
createJsonHandler(
|
createJsonHandler((req) => getAdminTaskDetail(getTaskId(req), req.adminSession || null), {
|
||||||
(req) => getAdminTaskDetail(getTaskId(req), req.adminSession || null),
|
successMessage: 'ok',
|
||||||
{
|
errorMessage: '读取任务详情失败',
|
||||||
successMessage: "ok",
|
scope: '[admin/tasks/:taskId]',
|
||||||
errorMessage: "读取任务详情失败",
|
}),
|
||||||
scope: "[admin/tasks/:taskId]",
|
)
|
||||||
}
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
router.post(
|
router.post(
|
||||||
"/tasks/:taskId/close",
|
'/tasks/:taskId/close',
|
||||||
requireAdminRoles(["admin", "operator", "support"]),
|
requireAdminRoles(['admin', 'operator', 'support']),
|
||||||
createJsonHandler(
|
createJsonHandler((req) => closeAdminTask(getTaskId(req), req.adminSession || null), {
|
||||||
(req) => closeAdminTask(getTaskId(req), req.adminSession || null),
|
successMessage: '任务已关闭',
|
||||||
{
|
errorMessage: '关闭任务失败',
|
||||||
successMessage: "任务已关闭",
|
scope: '[admin/tasks/:taskId/close]',
|
||||||
errorMessage: "关闭任务失败",
|
audit: (req, data) => buildTaskAudit('task_closed', req, data),
|
||||||
scope: "[admin/tasks/:taskId/close]",
|
}),
|
||||||
audit: (req, data) => buildTaskAudit("task_closed", req, data),
|
)
|
||||||
}
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
router.post(
|
router.post(
|
||||||
"/tasks/:taskId/kuaishou-cloud/prepare",
|
'/tasks/:taskId/kuaishou-cloud/prepare',
|
||||||
requireAdminRoles(["admin", "operator"]),
|
requireAdminRoles(['admin', 'operator']),
|
||||||
createJsonHandler(
|
createJsonHandler(
|
||||||
(req) =>
|
(req) => prepareAdminTaskKuaishouCloudFulfillment(getTaskId(req), req.adminSession || null),
|
||||||
prepareAdminTaskKuaishouCloudFulfillment(
|
|
||||||
getTaskId(req),
|
|
||||||
req.adminSession || null
|
|
||||||
),
|
|
||||||
{
|
{
|
||||||
successMessage: "绑定资源已准备完成",
|
successMessage: '绑定资源已准备完成',
|
||||||
errorMessage: "准备绑定资源失败",
|
errorMessage: '准备绑定资源失败',
|
||||||
scope: "[admin/tasks/:taskId/kuaishou-cloud/prepare]",
|
scope: '[admin/tasks/:taskId/kuaishou-cloud/prepare]',
|
||||||
audit: (req, data) => buildTaskAudit("task_kuaishou_cloud_prepare", req, data),
|
audit: (req, data) => buildTaskAudit('task_kuaishou_cloud_prepare', req, data),
|
||||||
}
|
},
|
||||||
)
|
),
|
||||||
);
|
)
|
||||||
|
|
||||||
router.post(
|
router.post(
|
||||||
"/tasks/:taskId/kuaishou-cloud/refresh-role-info",
|
'/tasks/:taskId/kuaishou-cloud/refresh-role-info',
|
||||||
requireAdminRoles(["admin", "operator", "support"]),
|
requireAdminRoles(['admin', 'operator', 'support']),
|
||||||
createJsonHandler(
|
createJsonHandler(
|
||||||
(req) =>
|
(req) => refreshAdminTaskKuaishouCloudRoleInfo(getTaskId(req), req.adminSession || null),
|
||||||
refreshAdminTaskKuaishouCloudRoleInfo(
|
|
||||||
getTaskId(req),
|
|
||||||
req.adminSession || null
|
|
||||||
),
|
|
||||||
{
|
{
|
||||||
successMessage: "角色信息已刷新",
|
successMessage: '角色信息已刷新',
|
||||||
errorMessage: "刷新角色信息失败",
|
errorMessage: '刷新角色信息失败',
|
||||||
scope: "[admin/tasks/:taskId/kuaishou-cloud/refresh-role-info]",
|
scope: '[admin/tasks/:taskId/kuaishou-cloud/refresh-role-info]',
|
||||||
audit: (req, data) =>
|
audit: (req, data) => buildTaskAudit('task_kuaishou_cloud_role_info_refreshed', req, data),
|
||||||
buildTaskAudit("task_kuaishou_cloud_role_info_refreshed", req, data),
|
},
|
||||||
}
|
),
|
||||||
)
|
)
|
||||||
);
|
|
||||||
|
|
||||||
router.post(
|
router.post(
|
||||||
"/tasks/:taskId/kuaishou-cloud/dispatch",
|
'/tasks/:taskId/kuaishou-cloud/rebind-role',
|
||||||
requireAdminRoles(["admin", "operator"]),
|
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(
|
createJsonHandler(
|
||||||
(req) =>
|
(req) =>
|
||||||
dispatchAdminTaskKuaishouCloudFulfillment(
|
dispatchAdminTaskKuaishouCloudFulfillment(
|
||||||
getTaskId(req),
|
getTaskId(req),
|
||||||
req.body as AdminTaskKuaishouCloudDispatchRouteBody,
|
req.body as AdminTaskKuaishouCloudDispatchRouteBody,
|
||||||
req.adminSession || null
|
req.adminSession || null,
|
||||||
),
|
),
|
||||||
{
|
{
|
||||||
successMessage: "已完成绑定确认并发货",
|
successMessage: '已完成绑定确认并发货',
|
||||||
errorMessage: "执行发货失败",
|
errorMessage: '执行发货失败',
|
||||||
scope: "[admin/tasks/:taskId/kuaishou-cloud/dispatch]",
|
scope: '[admin/tasks/:taskId/kuaishou-cloud/dispatch]',
|
||||||
audit: (req, data) => {
|
audit: (req, data) => {
|
||||||
const result = data as AdminTaskActionResponse;
|
const result = data as AdminTaskActionResponse
|
||||||
const body = req.body as AdminTaskKuaishouCloudDispatchRouteBody;
|
const body = req.body as AdminTaskKuaishouCloudDispatchRouteBody
|
||||||
|
|
||||||
return {
|
return {
|
||||||
action: "task_kuaishou_cloud_dispatch",
|
action: 'task_kuaishou_cloud_dispatch',
|
||||||
targetType: "task",
|
targetType: 'task',
|
||||||
targetId: String(getTaskId(req)),
|
targetId: String(getTaskId(req)),
|
||||||
data: {
|
data: {
|
||||||
taskId: result.task.taskId,
|
taskId: result.task.taskId,
|
||||||
taskNo: result.task.taskNo,
|
taskNo: result.task.taskNo,
|
||||||
status: result.task.status,
|
status: result.task.status,
|
||||||
deliveryStatus: result.task.deliveryStatus,
|
deliveryStatus: result.task.deliveryStatus,
|
||||||
ticketCodeProvided: Boolean(String(body.ticketCode || "").trim()),
|
ticketCodeProvided: Boolean(String(body.ticketCode || '').trim()),
|
||||||
},
|
},
|
||||||
};
|
}
|
||||||
},
|
},
|
||||||
}
|
},
|
||||||
)
|
),
|
||||||
);
|
)
|
||||||
|
|
||||||
router.post(
|
router.post(
|
||||||
"/tasks/:taskId/kuaishou-cloud/return-number",
|
'/tasks/:taskId/kuaishou-cloud/return-number',
|
||||||
requireAdminRoles(["admin", "operator"]),
|
requireAdminRoles(['admin', 'operator']),
|
||||||
createJsonHandler(
|
createJsonHandler(
|
||||||
(req) =>
|
(req) =>
|
||||||
returnNumberAdminTaskKuaishouCloudFulfillment(
|
returnNumberAdminTaskKuaishouCloudFulfillment(getTaskId(req), req.adminSession || null),
|
||||||
getTaskId(req),
|
|
||||||
req.adminSession || null
|
|
||||||
),
|
|
||||||
{
|
{
|
||||||
successMessage: "号码已退还",
|
successMessage: '号码已退还',
|
||||||
errorMessage: "退还号码失败",
|
errorMessage: '退还号码失败',
|
||||||
scope: "[admin/tasks/:taskId/kuaishou-cloud/return-number]",
|
scope: '[admin/tasks/:taskId/kuaishou-cloud/return-number]',
|
||||||
audit: (req, data) =>
|
audit: (req, data) => buildTaskAudit('task_kuaishou_cloud_return_number', req, data),
|
||||||
buildTaskAudit("task_kuaishou_cloud_return_number", req, data),
|
},
|
||||||
}
|
),
|
||||||
)
|
)
|
||||||
);
|
|
||||||
|
|
||||||
function buildTaskAudit(action: string, req: Request, data: unknown) {
|
function buildTaskAudit(action: string, req: Request, data: unknown) {
|
||||||
const result = data as AdminTaskActionResponse;
|
const result = data as AdminTaskActionResponse
|
||||||
|
|
||||||
return {
|
return {
|
||||||
action,
|
action,
|
||||||
targetType: "task",
|
targetType: 'task',
|
||||||
targetId: String(getTaskId(req)),
|
targetId: String(getTaskId(req)),
|
||||||
data: {
|
data: {
|
||||||
taskId: result.task.taskId,
|
taskId: result.task.taskId,
|
||||||
taskNo: result.task.taskNo,
|
taskNo: result.task.taskNo,
|
||||||
status: result.task.status,
|
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 {
|
import {
|
||||||
confirmKuaishouCloudClaimRole,
|
confirmKuaishouCloudClaimRole,
|
||||||
getKuaishouCloudClaimDetail,
|
getKuaishouCloudClaimDetail,
|
||||||
getKuaishouCloudClaimGuideAssetPath,
|
getKuaishouCloudClaimGuideAssetPath,
|
||||||
|
rebindKuaishouCloudClaimRole,
|
||||||
redeemKuaishouCloudClaim,
|
redeemKuaishouCloudClaim,
|
||||||
verifyKuaishouCloudClaimTicket,
|
verifyKuaishouCloudClaimTicket,
|
||||||
} from "../services/claim/kuaishou-cloud-claim-service.js";
|
} from '../services/claim/kuaishou-cloud-claim-service.js'
|
||||||
import {
|
import { buildNotFoundPayload, createRouteFileHandler, createRouteHandler } from '../utils/http.js'
|
||||||
buildNotFoundPayload,
|
|
||||||
createRouteFileHandler,
|
|
||||||
createRouteHandler,
|
|
||||||
} from "../utils/http.js";
|
|
||||||
|
|
||||||
const router = Router();
|
const router = Router()
|
||||||
const claimReadRateLimit = createRateLimitMiddleware({
|
const claimReadRateLimit = createRateLimitMiddleware({
|
||||||
scope: "claim:read",
|
scope: 'claim:read',
|
||||||
windowMs: 60_000,
|
windowMs: 60_000,
|
||||||
max: 120,
|
max: 120,
|
||||||
key: getParamRateLimitKey("token"),
|
key: getParamRateLimitKey('token'),
|
||||||
});
|
})
|
||||||
const claimWriteRateLimit = createRateLimitMiddleware({
|
const claimWriteRateLimit = createRateLimitMiddleware({
|
||||||
scope: "claim:write",
|
scope: 'claim:write',
|
||||||
windowMs: 60_000,
|
windowMs: 60_000,
|
||||||
max: 30,
|
max: 30,
|
||||||
key: getParamRateLimitKey("token"),
|
key: getParamRateLimitKey('token'),
|
||||||
});
|
})
|
||||||
|
|
||||||
router.get(
|
router.get(
|
||||||
"/:token",
|
'/:token',
|
||||||
claimReadRateLimit,
|
claimReadRateLimit,
|
||||||
createRouteHandler((req) => getKuaishouCloudClaimDetail(req.params.token), {
|
createRouteHandler((req) => getKuaishouCloudClaimDetail(req.params.token), {
|
||||||
errorMessage: "查询快手领取详情失败",
|
errorMessage: '查询快手领取详情失败',
|
||||||
scope: "[claims/:token]",
|
scope: '[claims/:token]',
|
||||||
})
|
}),
|
||||||
);
|
)
|
||||||
|
|
||||||
router.post(
|
router.post(
|
||||||
"/:token/kuaishou-cloud/verify-ticket",
|
'/:token/kuaishou-cloud/verify-ticket',
|
||||||
claimWriteRateLimit,
|
claimWriteRateLimit,
|
||||||
createRouteHandler(
|
createRouteHandler((req) => verifyKuaishouCloudClaimTicket(req.params.token, req.body), {
|
||||||
(req) => verifyKuaishouCloudClaimTicket(req.params.token, req.body),
|
successMessage: '核销码校验成功',
|
||||||
{
|
errorMessage: '校验核销码失败',
|
||||||
successMessage: "核销码校验成功",
|
scope: '[claims/:token/kuaishou-cloud/verify-ticket]',
|
||||||
errorMessage: "校验核销码失败",
|
}),
|
||||||
scope: "[claims/:token/kuaishou-cloud/verify-ticket]",
|
)
|
||||||
}
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
router.post(
|
router.post(
|
||||||
"/:token/kuaishou-cloud/confirm-role",
|
'/:token/kuaishou-cloud/confirm-role',
|
||||||
claimWriteRateLimit,
|
claimWriteRateLimit,
|
||||||
createRouteHandler((req) => confirmKuaishouCloudClaimRole(req.params.token), {
|
createRouteHandler((req) => confirmKuaishouCloudClaimRole(req.params.token), {
|
||||||
successMessage: "角色已确认",
|
successMessage: '角色已确认',
|
||||||
errorMessage: "确认角色失败",
|
errorMessage: '确认角色失败',
|
||||||
scope: "[claims/:token/kuaishou-cloud/confirm-role]",
|
scope: '[claims/:token/kuaishou-cloud/confirm-role]',
|
||||||
})
|
}),
|
||||||
);
|
)
|
||||||
|
|
||||||
router.post(
|
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,
|
claimWriteRateLimit,
|
||||||
createRouteHandler((req) => redeemKuaishouCloudClaim(req.params.token), {
|
createRouteHandler((req) => redeemKuaishouCloudClaim(req.params.token), {
|
||||||
successMessage: "兑换请求已提交",
|
successMessage: '兑换请求已提交',
|
||||||
errorMessage: "兑换失败",
|
errorMessage: '兑换失败',
|
||||||
scope: "[claims/:token/kuaishou-cloud/redeem]",
|
scope: '[claims/:token/kuaishou-cloud/redeem]',
|
||||||
})
|
}),
|
||||||
);
|
)
|
||||||
|
|
||||||
router.get(
|
router.get(
|
||||||
"/assets/kuaishou-cloud/:filename",
|
'/assets/kuaishou-cloud/:filename',
|
||||||
createRouteFileHandler(
|
createRouteFileHandler((req) => getKuaishouCloudClaimGuideAssetPath(req.params.filename), {
|
||||||
(req) => getKuaishouCloudClaimGuideAssetPath(req.params.filename),
|
errorMessage: '读取指引图片失败',
|
||||||
{
|
scope: '[claims/assets/kuaishou-cloud/:filename]',
|
||||||
errorMessage: "读取指引图片失败",
|
}),
|
||||||
scope: "[claims/assets/kuaishou-cloud/:filename]",
|
)
|
||||||
}
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
router.use((req, res) => {
|
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 { listCloudtentaclesSources } from '../platforms/cloudtentacles/source-config-service.js'
|
||||||
import { createHttpError } from '../../utils/http.js'
|
import { createHttpError } from '../../utils/http.js'
|
||||||
import { formatFenToAmount, normalizeFen } from '../../utils/money.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 {
|
import {
|
||||||
canCompleteManualDispatchTaskStatus,
|
canCompleteManualDispatchTaskStatus,
|
||||||
canDispatchKuaishouCloudFulfillmentStatus,
|
canDispatchKuaishouCloudFulfillmentStatus,
|
||||||
canMarkManualReviewTaskStatus,
|
canMarkManualReviewTaskStatus,
|
||||||
canPrepareKuaishouCloudFulfillmentStatus,
|
canPrepareKuaishouCloudFulfillmentStatus,
|
||||||
|
canRebindKuaishouCloudRoleStatus,
|
||||||
canReturnKuaishouCloudFulfillmentStatus,
|
canReturnKuaishouCloudFulfillmentStatus,
|
||||||
canRetryTaskStatus,
|
canRetryTaskStatus,
|
||||||
} from '../../domain/task-status.js'
|
} from '../../domain/task-status.js'
|
||||||
@@ -55,7 +61,9 @@ import type {
|
|||||||
|
|
||||||
type JsonRecord = Record<string, any>
|
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 page = normalizePage(query.page)
|
||||||
const pageSize = normalizePageSize(query.pageSize)
|
const pageSize = normalizePageSize(query.pageSize)
|
||||||
const { items, total } = await listOrders({
|
const { items, total } = await listOrders({
|
||||||
@@ -110,9 +118,10 @@ export async function getAdminOrderDetail(orderId: AdminEntityIdInput): Promise<
|
|||||||
createdAt: order.created_at,
|
createdAt: order.created_at,
|
||||||
updatedAt: order.updated_at,
|
updatedAt: order.updated_at,
|
||||||
itemSummary,
|
itemSummary,
|
||||||
rawPayload: order.raw_payload_json && typeof order.raw_payload_json === 'object'
|
rawPayload:
|
||||||
? order.raw_payload_json
|
order.raw_payload_json && typeof order.raw_payload_json === 'object'
|
||||||
: JSON.parse(String(order.raw_payload_json || '{}')),
|
? order.raw_payload_json
|
||||||
|
: JSON.parse(String(order.raw_payload_json || '{}')),
|
||||||
fulfillmentProgress: buildOrderFulfillmentProgress(tasks),
|
fulfillmentProgress: buildOrderFulfillmentProgress(tasks),
|
||||||
},
|
},
|
||||||
items: items.map((item) => ({
|
items: items.map((item) => ({
|
||||||
@@ -122,9 +131,10 @@ export async function getAdminOrderDetail(orderId: AdminEntityIdInput): Promise<
|
|||||||
itemTitle: resolveOrderItemTitle(item),
|
itemTitle: resolveOrderItemTitle(item),
|
||||||
quantity: item.quantity,
|
quantity: item.quantity,
|
||||||
deliveryMode: resolveOrderItemDeliveryMode(tasks, item.id),
|
deliveryMode: resolveOrderItemDeliveryMode(tasks, item.id),
|
||||||
spec: item.spec_json && typeof item.spec_json === 'object'
|
spec:
|
||||||
? item.spec_json
|
item.spec_json && typeof item.spec_json === 'object'
|
||||||
: JSON.parse(String(item.spec_json || '{}')),
|
? item.spec_json
|
||||||
|
: JSON.parse(String(item.spec_json || '{}')),
|
||||||
})),
|
})),
|
||||||
tasks: tasks.map((task) => mapAdminTaskSummary(task)),
|
tasks: tasks.map((task) => mapAdminTaskSummary(task)),
|
||||||
}
|
}
|
||||||
@@ -182,46 +192,49 @@ export async function getAdminTaskDetail(
|
|||||||
const claimUrl = claimToken ? buildClaimUrl(claimToken.token) : ''
|
const claimUrl = claimToken ? buildClaimUrl(claimToken.token) : ''
|
||||||
const screenshotUrl = await resolveAdminTaskScreenshotUrl(task, viewerContext)
|
const screenshotUrl = await resolveAdminTaskScreenshotUrl(task, viewerContext)
|
||||||
const cloudSourceLabelMap = buildCloudSourceLabelMap()
|
const cloudSourceLabelMap = buildCloudSourceLabelMap()
|
||||||
const kuaishouCloudFulfillment = mapKuaishouCloudFulfillmentContext(
|
const kuaishouCloudFulfillment =
|
||||||
taskContext.kuaishouCloudFulfillment,
|
mapKuaishouCloudFulfillmentContext(taskContext.kuaishouCloudFulfillment, {
|
||||||
{ cloudSourceLabelMap },
|
cloudSourceLabelMap,
|
||||||
) || mapKuaishouCloudTaskStateProjection(task, { cloudSourceLabelMap })
|
}) || mapKuaishouCloudTaskStateProjection(task, { cloudSourceLabelMap })
|
||||||
|
|
||||||
return {
|
return {
|
||||||
task: mapAdminTaskListItem({
|
task: mapAdminTaskListItem(
|
||||||
...task,
|
{
|
||||||
sku_code: orderItem?.sku_code || '',
|
...task,
|
||||||
sku_name: orderItem?.sku_name || '',
|
sku_code: orderItem?.sku_code || '',
|
||||||
claim_token: claimToken?.token || '',
|
sku_name: orderItem?.sku_name || '',
|
||||||
}, viewerContext),
|
claim_token: claimToken?.token || '',
|
||||||
|
},
|
||||||
|
viewerContext,
|
||||||
|
),
|
||||||
order: order
|
order: order
|
||||||
? {
|
? {
|
||||||
orderId: order.id,
|
orderId: order.id,
|
||||||
provider: order.provider || '',
|
provider: order.provider || '',
|
||||||
platform: order.platform,
|
platform: order.platform,
|
||||||
shopId: order.shop_id || '',
|
shopId: order.shop_id || '',
|
||||||
shopName: order.shop_name || '',
|
shopName: order.shop_name || '',
|
||||||
platformOrderId: order.platform_order_id,
|
platformOrderId: order.platform_order_id,
|
||||||
payStatus: order.pay_status,
|
payStatus: order.pay_status,
|
||||||
orderStatus: order.order_status,
|
orderStatus: order.order_status,
|
||||||
}
|
}
|
||||||
: null,
|
: null,
|
||||||
orderItem: orderItem
|
orderItem: orderItem
|
||||||
? {
|
? {
|
||||||
orderItemId: orderItem.id,
|
orderItemId: orderItem.id,
|
||||||
skuCode: orderItem.sku_code,
|
skuCode: orderItem.sku_code,
|
||||||
skuName: orderItem.sku_name,
|
skuName: orderItem.sku_name,
|
||||||
quantity: orderItem.quantity,
|
quantity: orderItem.quantity,
|
||||||
}
|
}
|
||||||
: null,
|
: null,
|
||||||
claimToken: claimToken
|
claimToken: claimToken
|
||||||
? {
|
? {
|
||||||
primaryClaimTokenId: claimToken.id,
|
primaryClaimTokenId: claimToken.id,
|
||||||
token: viewerContext.canViewSensitiveTaskData ? claimToken.token : '',
|
token: viewerContext.canViewSensitiveTaskData ? claimToken.token : '',
|
||||||
status: claimToken.status,
|
status: claimToken.status,
|
||||||
expiredAt: claimToken.expired_at,
|
expiredAt: claimToken.expired_at,
|
||||||
claimUrl,
|
claimUrl,
|
||||||
}
|
}
|
||||||
: null,
|
: null,
|
||||||
artifacts: viewerContext.canViewSensitiveTaskData ? safeParseJson(task.artifacts_json) : {},
|
artifacts: viewerContext.canViewSensitiveTaskData ? safeParseJson(task.artifacts_json) : {},
|
||||||
screenshotUrl,
|
screenshotUrl,
|
||||||
@@ -236,27 +249,42 @@ export async function getAdminTaskDetail(
|
|||||||
manualDispatch: mapManualDispatchContext(taskContext.manualDispatch, viewerContext),
|
manualDispatch: mapManualDispatchContext(taskContext.manualDispatch, viewerContext),
|
||||||
events: taskEvents.map(mapAdminTaskEvent),
|
events: taskEvents.map(mapAdminTaskEvent),
|
||||||
operations: {
|
operations: {
|
||||||
canRetry: viewerContext.canManageTaskLifecycle
|
canRetry:
|
||||||
&& !isManualDispatchTask(task)
|
viewerContext.canManageTaskLifecycle &&
|
||||||
&& canRetryTaskStatus(task.task_status),
|
!isManualDispatchTask(task) &&
|
||||||
|
canRetryTaskStatus(task.task_status),
|
||||||
canRegenerateClaimLink: canRegenerateClaimLinkForViewer(task, viewerContext),
|
canRegenerateClaimLink: canRegenerateClaimLinkForViewer(task, viewerContext),
|
||||||
canClose: canViewerCloseTask(task, viewerContext),
|
canClose: canViewerCloseTask(task, viewerContext),
|
||||||
canMarkManualReview: viewerContext.canManageTaskLifecycle && canMarkManualReviewTaskStatus(task.task_status),
|
canMarkManualReview:
|
||||||
canCompleteManualDispatch: viewerContext.canManageTaskLifecycle && isManualDispatchTask(task) && canCompleteManualDispatchTaskStatus(task.task_status),
|
viewerContext.canManageTaskLifecycle && canMarkManualReviewTaskStatus(task.task_status),
|
||||||
canPrepareKuaishouCloudFulfillment: viewerContext.canManageTaskLifecycle
|
canCompleteManualDispatch:
|
||||||
&& String(task.executor_key || '').trim() === 'kuaishou_ct_assisted'
|
viewerContext.canManageTaskLifecycle &&
|
||||||
&& String(kuaishouCloudFulfillment?.dispatch.status || 'pending').trim() === 'pending'
|
isManualDispatchTask(task) &&
|
||||||
&& String(kuaishouCloudFulfillment?.returnNumber.status || 'pending').trim() === 'pending'
|
canCompleteManualDispatchTaskStatus(task.task_status),
|
||||||
&& canPrepareKuaishouCloudFulfillmentStatus(task.task_status),
|
canPrepareKuaishouCloudFulfillment:
|
||||||
canRefreshKuaishouCloudRoleInfo: viewerContext.canOperateAssistedTask
|
viewerContext.canManageTaskLifecycle &&
|
||||||
&& String(task.executor_key || '').trim() === 'kuaishou_ct_assisted'
|
String(task.executor_key || '').trim() === 'kuaishou_ct_assisted' &&
|
||||||
&& Number(kuaishouCloudFulfillment?.binding.vnId || 0) > 0,
|
String(kuaishouCloudFulfillment?.dispatch.status || 'pending').trim() === 'pending' &&
|
||||||
canDispatchKuaishouCloudFulfillment: viewerContext.canManageTaskLifecycle
|
String(kuaishouCloudFulfillment?.returnNumber.status || 'pending').trim() === 'pending' &&
|
||||||
&& String(task.executor_key || '').trim() === 'kuaishou_ct_assisted'
|
canPrepareKuaishouCloudFulfillmentStatus(task.task_status),
|
||||||
&& canDispatchKuaishouCloudFulfillmentStatus(task.task_status),
|
canRefreshKuaishouCloudRoleInfo:
|
||||||
canReturnKuaishouCloudFulfillment: viewerContext.canManageTaskLifecycle
|
viewerContext.canOperateAssistedTask &&
|
||||||
&& String(task.executor_key || '').trim() === 'kuaishou_ct_assisted'
|
String(task.executor_key || '').trim() === 'kuaishou_ct_assisted' &&
|
||||||
&& canReturnKuaishouCloudFulfillmentStatus(task.task_status),
|
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,
|
canViewSensitiveTaskData: viewerContext.canViewSensitiveTaskData,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,10 +4,7 @@ import {
|
|||||||
parseTaskContext as parseTaskContextValue,
|
parseTaskContext as parseTaskContextValue,
|
||||||
parseTaskState as parseTaskStateValue,
|
parseTaskState as parseTaskStateValue,
|
||||||
} from '../../utils/task-json.js'
|
} from '../../utils/task-json.js'
|
||||||
import {
|
import { canRegenerateClaimLinkStatus, isTaskFinalStatus } from '../../domain/task-status.js'
|
||||||
canRegenerateClaimLinkStatus,
|
|
||||||
isTaskFinalStatus,
|
|
||||||
} from '../../domain/task-status.js'
|
|
||||||
|
|
||||||
import type { AdminViewerSessionInput } from '../../types/admin/read-inputs.js'
|
import type { AdminViewerSessionInput } from '../../types/admin/read-inputs.js'
|
||||||
import type { OrderItemRow, TaskRow } from '../../types/repository/rows.js'
|
import type { OrderItemRow, TaskRow } from '../../types/repository/rows.js'
|
||||||
@@ -51,16 +48,19 @@ export function mapManualDispatchContext(
|
|||||||
return {
|
return {
|
||||||
outcome: String(record.outcome || '').trim(),
|
outcome: String(record.outcome || '').trim(),
|
||||||
deliveryReference: String(record.deliveryReference || '').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(),
|
resultMessage: String(record.resultMessage || '').trim(),
|
||||||
completedAt: record.completedAt || null,
|
completedAt: record.completedAt || null,
|
||||||
completedBy: record.completedBy && typeof record.completedBy === 'object'
|
completedBy:
|
||||||
? {
|
record.completedBy && typeof record.completedBy === 'object'
|
||||||
userId: Number(record.completedBy.userId || 0) || 0,
|
? {
|
||||||
username: String(record.completedBy.username || '').trim(),
|
userId: Number(record.completedBy.userId || 0) || 0,
|
||||||
role: String(record.completedBy.role || '').trim(),
|
username: String(record.completedBy.username || '').trim(),
|
||||||
}
|
role: String(record.completedBy.role || '').trim(),
|
||||||
: null,
|
}
|
||||||
|
: null,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -78,7 +78,8 @@ export function mapKuaishouCloudFulfillmentContext(
|
|||||||
const role = record.role && typeof record.role === 'object' ? record.role : {}
|
const role = record.role && typeof record.role === 'object' ? record.role : {}
|
||||||
const purchase = record.purchase && typeof record.purchase === 'object' ? record.purchase : {}
|
const purchase = record.purchase && typeof record.purchase === 'object' ? record.purchase : {}
|
||||||
const dispatch = record.dispatch && typeof record.dispatch === 'object' ? record.dispatch : {}
|
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 consume = record.consume && typeof record.consume === 'object' ? record.consume : {}
|
||||||
const cloudSourceKeys = Array.isArray(binding.cloudSourceKeys)
|
const cloudSourceKeys = Array.isArray(binding.cloudSourceKeys)
|
||||||
? binding.cloudSourceKeys.map((value: unknown) => String(value || '').trim()).filter(Boolean)
|
? binding.cloudSourceKeys.map((value: unknown) => String(value || '').trim()).filter(Boolean)
|
||||||
@@ -156,6 +157,7 @@ export function mapKuaishouCloudFulfillmentContext(
|
|||||||
consumedAt: consume.consumedAt || null,
|
consumedAt: consume.consumedAt || null,
|
||||||
errorMessage: String(consume.errorMessage || '').trim(),
|
errorMessage: String(consume.errorMessage || '').trim(),
|
||||||
},
|
},
|
||||||
|
rebind: mapKuaishouCloudRebindContext(record.rebind),
|
||||||
notes: String(record.notes || '').trim(),
|
notes: String(record.notes || '').trim(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -204,9 +206,13 @@ export function mapKuaishouCloudTaskStateProjection(
|
|||||||
binding: {
|
binding: {
|
||||||
prepareStatus: task.kuaishou_bind_url ? 'ready' : 'pending',
|
prepareStatus: task.kuaishou_bind_url ? 'ready' : 'pending',
|
||||||
cloudSourceKeys: sourceKey ? [sourceKey] : [],
|
cloudSourceKeys: sourceKey ? [sourceKey] : [],
|
||||||
cloudSourceLabels: sourceKey ? [resolveCloudSourceLabel(sourceKey, options.cloudSourceLabelMap)] : [],
|
cloudSourceLabels: sourceKey
|
||||||
|
? [resolveCloudSourceLabel(sourceKey, options.cloudSourceLabelMap)]
|
||||||
|
: [],
|
||||||
resolvedSourceKey: sourceKey,
|
resolvedSourceKey: sourceKey,
|
||||||
resolvedSourceLabel: sourceKey ? resolveCloudSourceLabel(sourceKey, options.cloudSourceLabelMap) : '',
|
resolvedSourceLabel: sourceKey
|
||||||
|
? resolveCloudSourceLabel(sourceKey, options.cloudSourceLabelMap)
|
||||||
|
: '',
|
||||||
skuId: 0,
|
skuId: 0,
|
||||||
skuName: '',
|
skuName: '',
|
||||||
vnKey: '',
|
vnKey: '',
|
||||||
@@ -255,6 +261,10 @@ export function mapKuaishouCloudTaskStateProjection(
|
|||||||
consumedAt: null,
|
consumedAt: null,
|
||||||
errorMessage: '',
|
errorMessage: '',
|
||||||
},
|
},
|
||||||
|
rebind: {
|
||||||
|
currentAttempt: 0,
|
||||||
|
history: [],
|
||||||
|
},
|
||||||
notes: '',
|
notes: '',
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -263,13 +273,67 @@ function resolveCloudSourceLabel(sourceKey: string, labelMap: CloudSourceLabelMa
|
|||||||
const key = String(sourceKey || '').trim()
|
const key = String(sourceKey || '').trim()
|
||||||
if (!key) return ''
|
if (!key) return ''
|
||||||
|
|
||||||
const label = labelMap instanceof Map
|
const label = labelMap instanceof Map ? labelMap.get(key) : labelMap?.[key]
|
||||||
? labelMap.get(key)
|
|
||||||
: labelMap?.[key]
|
|
||||||
|
|
||||||
return String(label || key).trim() || 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 {
|
export function mapRedeemResolutionContext(value: unknown): JsonRecord | null {
|
||||||
if (!value || typeof value !== 'object') {
|
if (!value || typeof value !== 'object') {
|
||||||
return null
|
return null
|
||||||
@@ -278,8 +342,8 @@ export function mapRedeemResolutionContext(value: unknown): JsonRecord | null {
|
|||||||
const record = value as JsonRecord
|
const record = value as JsonRecord
|
||||||
const attempts = Array.isArray(record.attempts)
|
const attempts = Array.isArray(record.attempts)
|
||||||
? record.attempts
|
? record.attempts
|
||||||
.map((attempt) => mapRedeemResolutionAttempt(attempt))
|
.map((attempt) => mapRedeemResolutionAttempt(attempt))
|
||||||
.filter((attempt): attempt is RedeemResolutionAttempt => Boolean(attempt))
|
.filter((attempt): attempt is RedeemResolutionAttempt => Boolean(attempt))
|
||||||
: []
|
: []
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -296,7 +360,9 @@ export function getTaskPrimaryClaimTokenId(task: TaskLike | null | undefined): n
|
|||||||
return Number.isFinite(value) && value > 0 ? value : null
|
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)
|
const role = normalizeAdminRole(session?.role)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -326,7 +392,11 @@ export function canViewerCloseTask(task: TaskLike, viewerContext: AdminViewerCon
|
|||||||
return !isTaskFinalStatus(task?.task_status)
|
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()
|
const normalizedShopName = String(shopName || '').trim()
|
||||||
if (normalizedShopName) {
|
if (normalizedShopName) {
|
||||||
return 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 {
|
export function resolveOrderItemDeliveryMode(
|
||||||
const task = (Array.isArray(tasks) ? tasks : []).find((item) => item.order_item_id === orderItemId)
|
tasks: TaskLike[] | null | undefined,
|
||||||
|
orderItemId: number | string,
|
||||||
|
): string {
|
||||||
|
const task = (Array.isArray(tasks) ? tasks : []).find(
|
||||||
|
(item) => item.order_item_id === orderItemId,
|
||||||
|
)
|
||||||
|
|
||||||
if (!task) {
|
if (!task) {
|
||||||
return ''
|
return ''
|
||||||
@@ -382,7 +457,12 @@ export function resolveOrderItemDeliveryMode(tasks: TaskLike[] | null | undefine
|
|||||||
return 'kuaishou_cloud'
|
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'
|
return 'claim_link'
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -393,7 +473,10 @@ export function isManualDispatchTask(task: TaskLike | null | undefined): boolean
|
|||||||
return String(task?.executor_key || '').trim() === 'manual_dispatch'
|
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)) {
|
if (isManualDispatchTask(task)) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,20 +21,15 @@ import {
|
|||||||
import { createHttpError } from '../../../utils/http.js'
|
import { createHttpError } from '../../../utils/http.js'
|
||||||
import { nowIso } from '../../../utils/time.js'
|
import { nowIso } from '../../../utils/time.js'
|
||||||
import { TASK_STATUS } from '../../../domain/task-status.js'
|
import { TASK_STATUS } from '../../../domain/task-status.js'
|
||||||
import {
|
import { createAdminViewerContext, parseTaskContext } from '../admin-read-shared-helpers.js'
|
||||||
createAdminViewerContext,
|
import { getRequiredTask, mapTaskActionPayload } from '../admin-task-read-helpers.js'
|
||||||
parseTaskContext,
|
|
||||||
} from '../admin-read-shared-helpers.js'
|
|
||||||
import {
|
|
||||||
getRequiredTask,
|
|
||||||
mapTaskActionPayload,
|
|
||||||
} from '../admin-task-read-helpers.js'
|
|
||||||
import {
|
import {
|
||||||
ensureTaskClaimLink,
|
ensureTaskClaimLink,
|
||||||
getTaskClaimExpiresAt,
|
getTaskClaimExpiresAt,
|
||||||
maskCode,
|
maskCode,
|
||||||
maskPhone,
|
maskPhone,
|
||||||
} from '../write-helpers.js'
|
} from '../write-helpers.js'
|
||||||
|
import { rebindKuaishouCloudTaskRole } from '../../fulfillment/kuaishou-cloud/index.js'
|
||||||
import {
|
import {
|
||||||
isKuaishouCloudTask,
|
isKuaishouCloudTask,
|
||||||
normalizeKuaishouCloudFlow,
|
normalizeKuaishouCloudFlow,
|
||||||
@@ -210,17 +205,22 @@ export async function prepareAdminTaskKuaishouCloudFulfillment(
|
|||||||
updated_at: now,
|
updated_at: now,
|
||||||
})
|
})
|
||||||
|
|
||||||
await createTaskEvent(task.id, 'kuaishou_cloud_binding_prepared', {
|
await createTaskEvent(
|
||||||
skuId: flowWithResolvedBinding.binding.skuId,
|
task.id,
|
||||||
skuName: flowWithResolvedBinding.binding.skuName,
|
'kuaishou_cloud_binding_prepared',
|
||||||
vnKey: preparedBinding.vnKey,
|
{
|
||||||
vnId,
|
skuId: flowWithResolvedBinding.binding.skuId,
|
||||||
vnPhoneMasked: maskPhone(vnPhone),
|
skuName: flowWithResolvedBinding.binding.skuName,
|
||||||
bindUrl: preparedBinding.bindUrl,
|
vnKey: preparedBinding.vnKey,
|
||||||
purchaseTriggered,
|
vnId,
|
||||||
usedKnapsack,
|
vnPhoneMasked: maskPhone(vnPhone),
|
||||||
resolvedByName: resolvedBinding.resolvedByName,
|
bindUrl: preparedBinding.bindUrl,
|
||||||
}, now)
|
purchaseTriggered,
|
||||||
|
usedKnapsack,
|
||||||
|
resolvedByName: resolvedBinding.resolvedByName,
|
||||||
|
},
|
||||||
|
now,
|
||||||
|
)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
task: mapTaskActionPayload(updatedTask),
|
task: mapTaskActionPayload(updatedTask),
|
||||||
@@ -280,10 +280,10 @@ export async function dispatchAdminTaskKuaishouCloudFulfillment(
|
|||||||
now,
|
now,
|
||||||
actor: session
|
actor: session
|
||||||
? {
|
? {
|
||||||
userId: Number(session.userId || 0) || 0,
|
userId: Number(session.userId || 0) || 0,
|
||||||
username: String(session.username || '').trim(),
|
username: String(session.username || '').trim(),
|
||||||
role: String(session.role || '').trim(),
|
role: String(session.role || '').trim(),
|
||||||
}
|
}
|
||||||
: null,
|
: null,
|
||||||
source: 'admin_task_dispatch',
|
source: 'admin_task_dispatch',
|
||||||
errorCodePrefix: 'admin_task_kuaishou_cloud',
|
errorCodePrefix: 'admin_task_kuaishou_cloud',
|
||||||
@@ -309,13 +309,14 @@ export async function dispatchAdminTaskKuaishouCloudFulfillment(
|
|||||||
...syncedFlow.ticket,
|
...syncedFlow.ticket,
|
||||||
code: ticketCode || persistedTicketCode,
|
code: ticketCode || persistedTicketCode,
|
||||||
capturedAt: ticketCode ? now : syncedFlow.ticket.capturedAt,
|
capturedAt: ticketCode ? now : syncedFlow.ticket.capturedAt,
|
||||||
capturedBy: ticketCode && session
|
capturedBy:
|
||||||
? {
|
ticketCode && session
|
||||||
userId: Number(session.userId || 0) || 0,
|
? {
|
||||||
username: String(session.username || '').trim(),
|
userId: Number(session.userId || 0) || 0,
|
||||||
role: String(session.role || '').trim(),
|
username: String(session.username || '').trim(),
|
||||||
}
|
role: String(session.role || '').trim(),
|
||||||
: syncedFlow.ticket.capturedBy,
|
}
|
||||||
|
: syncedFlow.ticket.capturedBy,
|
||||||
},
|
},
|
||||||
dispatch: {
|
dispatch: {
|
||||||
...syncedFlow.dispatch,
|
...syncedFlow.dispatch,
|
||||||
@@ -323,10 +324,10 @@ export async function dispatchAdminTaskKuaishouCloudFulfillment(
|
|||||||
dispatchAt: now,
|
dispatchAt: now,
|
||||||
dispatchBy: session
|
dispatchBy: session
|
||||||
? {
|
? {
|
||||||
userId: Number(session.userId || 0) || 0,
|
userId: Number(session.userId || 0) || 0,
|
||||||
username: String(session.username || '').trim(),
|
username: String(session.username || '').trim(),
|
||||||
role: String(session.role || '').trim(),
|
role: String(session.role || '').trim(),
|
||||||
}
|
}
|
||||||
: null,
|
: null,
|
||||||
sendType: Number(dispatchResult.sendType || 0) || 0,
|
sendType: Number(dispatchResult.sendType || 0) || 0,
|
||||||
note: String(dispatchResult.note || dispatchResult.responseMessage || '').trim(),
|
note: String(dispatchResult.note || dispatchResult.responseMessage || '').trim(),
|
||||||
@@ -338,20 +339,27 @@ export async function dispatchAdminTaskKuaishouCloudFulfillment(
|
|||||||
task_status: TASK_STATUS.DISPATCHED_PENDING_RETURN,
|
task_status: TASK_STATUS.DISPATCHED_PENDING_RETURN,
|
||||||
delivery_status: 'delivered',
|
delivery_status: 'delivered',
|
||||||
result_code: 'kuaishou_cloud_dispatched',
|
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: '',
|
last_error: '',
|
||||||
context_json: JSON.stringify(nextContext),
|
context_json: JSON.stringify(nextContext),
|
||||||
updated_at: now,
|
updated_at: now,
|
||||||
})
|
})
|
||||||
|
|
||||||
await createTaskEvent(task.id, 'kuaishou_cloud_dispatched', {
|
await createTaskEvent(
|
||||||
ticketCodeMasked: maskCode(ticketCode || persistedTicketCode),
|
task.id,
|
||||||
skuId: syncedFlow.binding.skuId,
|
'kuaishou_cloud_dispatched',
|
||||||
vnId: syncedFlow.binding.vnId,
|
{
|
||||||
vnPhoneMasked: maskPhone(syncedFlow.binding.vnPhone),
|
ticketCodeMasked: maskCode(ticketCode || persistedTicketCode),
|
||||||
sendType: dispatchResult.sendType,
|
skuId: syncedFlow.binding.skuId,
|
||||||
note: dispatchResult.note,
|
vnId: syncedFlow.binding.vnId,
|
||||||
}, now)
|
vnPhoneMasked: maskPhone(syncedFlow.binding.vnPhone),
|
||||||
|
sendType: dispatchResult.sendType,
|
||||||
|
note: dispatchResult.note,
|
||||||
|
},
|
||||||
|
now,
|
||||||
|
)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
task: mapTaskActionPayload(updatedTask),
|
task: mapTaskActionPayload(updatedTask),
|
||||||
@@ -414,7 +422,8 @@ export async function refreshAdminTaskKuaishouCloudRoleInfo(
|
|||||||
name: bindInfo.name,
|
name: bindInfo.name,
|
||||||
rid: bindInfo.rid,
|
rid: bindInfo.rid,
|
||||||
refreshedAt: now,
|
refreshedAt: now,
|
||||||
errorMessage: bindInfo.name || bindInfo.rid ? '' : '当前还没有查询到角色信息,请让客户完成绑定后再刷新',
|
errorMessage:
|
||||||
|
bindInfo.name || bindInfo.rid ? '' : '当前还没有查询到角色信息,请让客户完成绑定后再刷新',
|
||||||
rawInfo: bindInfo.rawInfo,
|
rawInfo: bindInfo.rawInfo,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -427,24 +436,61 @@ export async function refreshAdminTaskKuaishouCloudRoleInfo(
|
|||||||
updated_at: now,
|
updated_at: now,
|
||||||
})
|
})
|
||||||
|
|
||||||
await createTaskEvent(task.id, 'kuaishou_cloud_role_info_refreshed', {
|
await createTaskEvent(
|
||||||
roleName: bindInfo.name,
|
task.id,
|
||||||
roleId: bindInfo.rid,
|
'kuaishou_cloud_role_info_refreshed',
|
||||||
vnId: flow.binding.vnId,
|
{
|
||||||
refreshedBy: session
|
roleName: bindInfo.name,
|
||||||
? {
|
roleId: bindInfo.rid,
|
||||||
userId: Number(session.userId || 0) || 0,
|
vnId: flow.binding.vnId,
|
||||||
username: String(session.username || '').trim(),
|
refreshedBy: session
|
||||||
role: String(session.role || '').trim(),
|
? {
|
||||||
}
|
userId: Number(session.userId || 0) || 0,
|
||||||
: null,
|
username: String(session.username || '').trim(),
|
||||||
}, now)
|
role: String(session.role || '').trim(),
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
},
|
||||||
|
now,
|
||||||
|
)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
task: mapTaskActionPayload(updatedTask),
|
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(
|
export async function returnNumberAdminTaskKuaishouCloudFulfillment(
|
||||||
taskId: AdminEntityIdInput,
|
taskId: AdminEntityIdInput,
|
||||||
session: AdminViewerSessionInput | null = null,
|
session: AdminViewerSessionInput | null = null,
|
||||||
@@ -519,7 +565,11 @@ export async function returnNumberAdminTaskKuaishouCloudFulfillment(
|
|||||||
consumeStatus = consumeResult.consumed ? 'success' : 'failed'
|
consumeStatus = consumeResult.consumed ? 'success' : 'failed'
|
||||||
consumedAt = consumeResult.consumed ? now : null
|
consumedAt = consumeResult.consumed ? now : null
|
||||||
consumeErrorMessage = String(consumeResult.errorMessage || '').trim()
|
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'
|
consumeStatus = 'failed'
|
||||||
consumeErrorMessage = '订单对应快手小店缺少可用 Cookie,无法执行快手核销'
|
consumeErrorMessage = '订单对应快手小店缺少可用 Cookie,无法执行快手核销'
|
||||||
} else {
|
} else {
|
||||||
@@ -561,10 +611,10 @@ export async function returnNumberAdminTaskKuaishouCloudFulfillment(
|
|||||||
returnedAt: now,
|
returnedAt: now,
|
||||||
returnedBy: session
|
returnedBy: session
|
||||||
? {
|
? {
|
||||||
userId: Number(session.userId || 0) || 0,
|
userId: Number(session.userId || 0) || 0,
|
||||||
username: String(session.username || '').trim(),
|
username: String(session.username || '').trim(),
|
||||||
role: String(session.role || '').trim(),
|
role: String(session.role || '').trim(),
|
||||||
}
|
}
|
||||||
: null,
|
: null,
|
||||||
},
|
},
|
||||||
consume: {
|
consume: {
|
||||||
@@ -590,10 +640,15 @@ export async function returnNumberAdminTaskKuaishouCloudFulfillment(
|
|||||||
updated_at: now,
|
updated_at: now,
|
||||||
})
|
})
|
||||||
|
|
||||||
await createTaskEvent(task.id, 'kuaishou_cloud_number_returned', {
|
await createTaskEvent(
|
||||||
vnId: flow.binding.vnId,
|
task.id,
|
||||||
vnPhoneMasked: maskPhone(flow.binding.vnPhone),
|
'kuaishou_cloud_number_returned',
|
||||||
}, now)
|
{
|
||||||
|
vnId: flow.binding.vnId,
|
||||||
|
vnPhoneMasked: maskPhone(flow.binding.vnPhone),
|
||||||
|
},
|
||||||
|
now,
|
||||||
|
)
|
||||||
|
|
||||||
await createTaskEvent(
|
await createTaskEvent(
|
||||||
task.id,
|
task.id,
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import {
|
|||||||
maskCode,
|
maskCode,
|
||||||
normalizeKuaishouCloudFlow,
|
normalizeKuaishouCloudFlow,
|
||||||
prepareKuaishouCloudFulfillmentTask,
|
prepareKuaishouCloudFulfillmentTask,
|
||||||
|
rebindKuaishouCloudTaskRole,
|
||||||
refreshKuaishouCloudTaskRoleInfo,
|
refreshKuaishouCloudTaskRoleInfo,
|
||||||
} from '../fulfillment/kuaishou-cloud/index.js'
|
} from '../fulfillment/kuaishou-cloud/index.js'
|
||||||
import { buildClaimDetailPayload, getClaimContext } from './kuaishou-cloud-claim-context.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)
|
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 shopName = String(flow.consume.shopName || context.order.shop_name || '').trim()
|
||||||
let detailResult
|
let detailResult
|
||||||
|
|
||||||
@@ -134,13 +137,16 @@ export async function verifyKuaishouCloudClaimTicket(token: unknown, payload: Js
|
|||||||
} else {
|
} else {
|
||||||
const preparedFlow = normalizeKuaishouCloudFlow(nextContext.kuaishouCloudFulfillment)
|
const preparedFlow = normalizeKuaishouCloudFlow(nextContext.kuaishouCloudFulfillment)
|
||||||
if (preparedFlow.binding.prepareStatus !== 'ready' || !preparedFlow.binding.bindUrl) {
|
if (preparedFlow.binding.prepareStatus !== 'ready' || !preparedFlow.binding.bindUrl) {
|
||||||
await prepareKuaishouCloudFulfillmentTask({
|
await prepareKuaishouCloudFulfillmentTask(
|
||||||
...context.task,
|
{
|
||||||
context_json: JSON.stringify(nextContext),
|
...context.task,
|
||||||
}, {
|
context_json: JSON.stringify(nextContext),
|
||||||
source: 'claim_page_ticket_verified',
|
},
|
||||||
actor: { source: 'claim_page' },
|
{
|
||||||
})
|
source: 'claim_page_ticket_verified',
|
||||||
|
actor: { source: 'claim_page' },
|
||||||
|
},
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
await updateTask(context.task.id, {
|
await updateTask(context.task.id, {
|
||||||
claim_token: context.claimToken.token,
|
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', {
|
await createTaskEvent(
|
||||||
ticketCodeMasked: maskCode(detailResult.eTicketId || ticketCode),
|
context.task.id,
|
||||||
shopId,
|
'kuaishou_cloud_ticket_verified',
|
||||||
shopName,
|
{
|
||||||
goodsTitle: String(detailResult.goods?.itemTitle || '').trim(),
|
ticketCodeMasked: maskCode(detailResult.eTicketId || ticketCode),
|
||||||
mock: mockTicketCode,
|
shopId,
|
||||||
}, now)
|
shopName,
|
||||||
|
goodsTitle: String(detailResult.goods?.itemTitle || '').trim(),
|
||||||
|
mock: mockTicketCode,
|
||||||
|
},
|
||||||
|
now,
|
||||||
|
)
|
||||||
|
|
||||||
return getKuaishouCloudClaimDetail(token)
|
return getKuaishouCloudClaimDetail(token)
|
||||||
}
|
}
|
||||||
@@ -195,10 +206,10 @@ export async function getKuaishouCloudClaimDetail(token: unknown) {
|
|||||||
let task = context.task
|
let task = context.task
|
||||||
|
|
||||||
if (
|
if (
|
||||||
String(task.executor_key || '').trim() === 'kuaishou_ct_assisted'
|
String(task.executor_key || '').trim() === 'kuaishou_ct_assisted' &&
|
||||||
&& !isKuaishouCloudMockTask(task)
|
!isKuaishouCloudMockTask(task)
|
||||||
) {
|
) {
|
||||||
task = await syncKuaishouCloudRoleInfo(task) || task
|
task = (await syncKuaishouCloudRoleInfo(task)) || task
|
||||||
}
|
}
|
||||||
|
|
||||||
return buildClaimDetailPayload({
|
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 {
|
function parseTaskContext(task: Partial<TaskRow> | null | undefined): JsonObject {
|
||||||
const rawValue = task?.context_json
|
const rawValue = task?.context_json
|
||||||
if (!rawValue) {
|
if (!rawValue) {
|
||||||
@@ -289,10 +318,13 @@ async function queryKuaishouEticketConsumeDetailWithShopFallback({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
throw createHttpError(lastError instanceof Error ? lastError.message : '核销码校验失败,请确认是否复制完整', {
|
throw createHttpError(
|
||||||
statusCode: 409,
|
lastError instanceof Error ? lastError.message : '核销码校验失败,请确认是否复制完整',
|
||||||
errorCode: 'claim_kuaishou_cloud_ticket_invalid',
|
{
|
||||||
})
|
statusCode: 409,
|
||||||
|
errorCode: 'claim_kuaishou_cloud_ticket_invalid',
|
||||||
|
},
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolveKuaishouEticketDetailCandidateShops({
|
function resolveKuaishouEticketDetailCandidateShops({
|
||||||
@@ -333,9 +365,7 @@ function isUsableKuaishouEticketShopConfig(
|
|||||||
shopConfig: KuaishouEticketShopConfig | null | undefined,
|
shopConfig: KuaishouEticketShopConfig | null | undefined,
|
||||||
): shopConfig is KuaishouEticketShopConfig {
|
): shopConfig is KuaishouEticketShopConfig {
|
||||||
return Boolean(
|
return Boolean(
|
||||||
shopConfig &&
|
shopConfig && shopConfig.enabled !== false && String(shopConfig.cookie || '').trim(),
|
||||||
shopConfig.enabled !== false &&
|
|
||||||
String(shopConfig.cookie || '').trim(),
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -373,11 +403,11 @@ export async function confirmKuaishouCloudClaimRole(token: unknown) {
|
|||||||
const refreshed = mockMode
|
const refreshed = mockMode
|
||||||
? { task: context.task }
|
? { task: context.task }
|
||||||
: await refreshKuaishouCloudTaskRoleInfo(context.task, {
|
: await refreshKuaishouCloudTaskRoleInfo(context.task, {
|
||||||
source: 'claim_page_role_confirm',
|
source: 'claim_page_role_confirm',
|
||||||
actor: { source: 'claim_page' },
|
actor: { source: 'claim_page' },
|
||||||
recordEvent: false,
|
recordEvent: false,
|
||||||
forceProbe: true,
|
forceProbe: true,
|
||||||
})
|
})
|
||||||
const flow = normalizeKuaishouCloudFlow(parseTaskContext(refreshed.task).kuaishouCloudFulfillment)
|
const flow = normalizeKuaishouCloudFlow(parseTaskContext(refreshed.task).kuaishouCloudFulfillment)
|
||||||
|
|
||||||
if (flow.ticket.status !== 'verified') {
|
if (flow.ticket.status !== 'verified') {
|
||||||
@@ -404,11 +434,16 @@ export async function confirmKuaishouCloudClaimRole(token: unknown) {
|
|||||||
updated_at: now,
|
updated_at: now,
|
||||||
})
|
})
|
||||||
|
|
||||||
await createTaskEvent(context.task.id, 'kuaishou_cloud_role_confirmed', {
|
await createTaskEvent(
|
||||||
vnPhone: flow.binding.vnPhone,
|
context.task.id,
|
||||||
roleName: flow.binding.roleName,
|
'kuaishou_cloud_role_confirmed',
|
||||||
roleId: flow.binding.roleId,
|
{
|
||||||
}, now)
|
vnPhone: flow.binding.vnPhone,
|
||||||
|
roleName: flow.binding.roleName,
|
||||||
|
roleId: flow.binding.roleId,
|
||||||
|
},
|
||||||
|
now,
|
||||||
|
)
|
||||||
|
|
||||||
return getKuaishouCloudClaimDetail(token)
|
return getKuaishouCloudClaimDetail(token)
|
||||||
}
|
}
|
||||||
@@ -474,10 +509,15 @@ export async function redeemKuaishouCloudClaim(token: unknown) {
|
|||||||
updated_at: nowIso(),
|
updated_at: nowIso(),
|
||||||
})
|
})
|
||||||
|
|
||||||
await createTaskEvent(lockedTask.id, 'kuaishou_cloud_redeem_failed', {
|
await createTaskEvent(
|
||||||
source: 'claim_page_redeem',
|
lockedTask.id,
|
||||||
errorMessage: message,
|
'kuaishou_cloud_redeem_failed',
|
||||||
}, nowIso())
|
{
|
||||||
|
source: 'claim_page_redeem',
|
||||||
|
errorMessage: message,
|
||||||
|
},
|
||||||
|
nowIso(),
|
||||||
|
)
|
||||||
|
|
||||||
throw error
|
throw error
|
||||||
}
|
}
|
||||||
@@ -490,10 +530,10 @@ function isKuaishouCloudMockTask(task: Partial<TaskRow> | null | undefined) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function isKuaishouCloudMockContext(context: JsonObject = {}) {
|
function isKuaishouCloudMockContext(context: JsonObject = {}) {
|
||||||
const flow = context.kuaishouCloudFulfillment
|
const flow =
|
||||||
&& typeof context.kuaishouCloudFulfillment === 'object'
|
context.kuaishouCloudFulfillment && typeof context.kuaishouCloudFulfillment === 'object'
|
||||||
? context.kuaishouCloudFulfillment
|
? context.kuaishouCloudFulfillment
|
||||||
: {}
|
: {}
|
||||||
const mock = flow.mock && typeof flow.mock === 'object' ? flow.mock : context.mock
|
const mock = flow.mock && typeof flow.mock === 'object' ? flow.mock : context.mock
|
||||||
|
|
||||||
return Boolean(mock && typeof mock === 'object' && mock.enabled === true)
|
return Boolean(mock && typeof mock === 'object' && mock.enabled === true)
|
||||||
@@ -502,8 +542,9 @@ function isKuaishouCloudMockContext(context: JsonObject = {}) {
|
|||||||
function buildMockVerifiedKuaishouCloudFlow(value: unknown, timestamp: string) {
|
function buildMockVerifiedKuaishouCloudFlow(value: unknown, timestamp: string) {
|
||||||
const flow = normalizeKuaishouCloudFlow(value)
|
const flow = normalizeKuaishouCloudFlow(value)
|
||||||
const source = flow as JsonObject
|
const source = flow as JsonObject
|
||||||
const bindUrl = flow.binding.bindUrl
|
const bindUrl =
|
||||||
|| `https://example.com/mock-kuaishou-cloud-bind?task=mock&ts=${encodeURIComponent(timestamp)}`
|
flow.binding.bindUrl ||
|
||||||
|
`https://example.com/mock-kuaishou-cloud-bind?task=mock&ts=${encodeURIComponent(timestamp)}`
|
||||||
const vnPhone = flow.binding.vnPhone || '13800000000'
|
const vnPhone = flow.binding.vnPhone || '13800000000'
|
||||||
const roleName = flow.binding.roleName || flow.role.name || '测试角色'
|
const roleName = flow.binding.roleName || flow.role.name || '测试角色'
|
||||||
const roleId = flow.binding.roleId || flow.role.rid || '10001'
|
const roleId = flow.binding.roleId || flow.role.rid || '10001'
|
||||||
@@ -590,8 +631,13 @@ async function completeMockKuaishouCloudClaimTask(task: TaskRow, timestamp: stri
|
|||||||
updated_at: timestamp,
|
updated_at: timestamp,
|
||||||
})
|
})
|
||||||
|
|
||||||
await createTaskEvent(task.id, 'kuaishou_cloud_mock_redeemed', {
|
await createTaskEvent(
|
||||||
source: 'claim_page_mock',
|
task.id,
|
||||||
deliveryItems: flow.deliveryItems,
|
'kuaishou_cloud_mock_redeemed',
|
||||||
}, timestamp)
|
{
|
||||||
|
source: 'claim_page_mock',
|
||||||
|
deliveryItems: flow.deliveryItems,
|
||||||
|
},
|
||||||
|
timestamp,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,98 +1,77 @@
|
|||||||
import { resolveCloudtentaclesConfig } from "../../platforms/cloudtentacles/helpers.js";
|
import { resolveCloudtentaclesConfig } from '../../platforms/cloudtentacles/helpers.js'
|
||||||
import {
|
import { maskCode as maskCodeValue, maskPhone as maskPhoneValue } from '../../../utils/masking.js'
|
||||||
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) {
|
export function isKuaishouCloudTask(task: unknown) {
|
||||||
const source = task && typeof task === "object" ? task as JsonObject : {};
|
const source = task && typeof task === 'object' ? (task as JsonObject) : {}
|
||||||
return String(source.executor_key || "").trim() === "kuaishou_ct_assisted";
|
return String(source.executor_key || '').trim() === 'kuaishou_ct_assisted'
|
||||||
}
|
}
|
||||||
|
|
||||||
export function normalizeKuaishouCloudFlow(value: unknown) {
|
export function normalizeKuaishouCloudFlow(value: unknown) {
|
||||||
const source: JsonObject = value && typeof value === "object" ? value as JsonObject : {};
|
const source: JsonObject = value && typeof value === 'object' ? (value as JsonObject) : {}
|
||||||
const binding =
|
const binding = source.binding && typeof source.binding === 'object' ? source.binding : {}
|
||||||
source.binding && typeof source.binding === "object" ? source.binding : {};
|
const role = source.role && typeof source.role === 'object' ? source.role : {}
|
||||||
const role =
|
const purchase = source.purchase && typeof source.purchase === 'object' ? source.purchase : {}
|
||||||
source.role && typeof source.role === "object" ? source.role : {};
|
const dispatch = source.dispatch && typeof source.dispatch === 'object' ? source.dispatch : {}
|
||||||
const purchase =
|
|
||||||
source.purchase && typeof source.purchase === "object"
|
|
||||||
? source.purchase
|
|
||||||
: {};
|
|
||||||
const dispatch =
|
|
||||||
source.dispatch && typeof source.dispatch === "object"
|
|
||||||
? source.dispatch
|
|
||||||
: {};
|
|
||||||
const returnNumber =
|
const returnNumber =
|
||||||
source.returnNumber && typeof source.returnNumber === "object"
|
source.returnNumber && typeof source.returnNumber === 'object' ? source.returnNumber : {}
|
||||||
? source.returnNumber
|
const consume = source.consume && typeof source.consume === 'object' ? source.consume : {}
|
||||||
: {};
|
const ticket = source.ticket && typeof source.ticket === 'object' ? source.ticket : {}
|
||||||
const consume =
|
const rebind = source.rebind && typeof source.rebind === 'object' ? source.rebind : {}
|
||||||
source.consume && typeof source.consume === "object" ? source.consume : {};
|
const deliveryItems = normalizeDeliveryItems(source, binding)
|
||||||
const ticket =
|
|
||||||
source.ticket && typeof source.ticket === "object" ? source.ticket : {};
|
|
||||||
const deliveryItems = normalizeDeliveryItems(source, binding);
|
|
||||||
|
|
||||||
const roleName = String(role.name || binding.roleName || "").trim();
|
const roleName = String(role.name || binding.roleName || '').trim()
|
||||||
const roleId = String(role.rid || binding.roleId || "").trim();
|
const roleId = String(role.rid || binding.roleId || '').trim()
|
||||||
const bindPreparedAt = binding.bindPreparedAt || null;
|
const bindPreparedAt = binding.bindPreparedAt || null
|
||||||
const bindExpiresAt =
|
const bindExpiresAt =
|
||||||
binding.bindExpiresAt ||
|
binding.bindExpiresAt || resolveKuaishouCloudBindUrlExpiresAt(bindPreparedAt)
|
||||||
resolveKuaishouCloudBindUrlExpiresAt(bindPreparedAt);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...source,
|
...source,
|
||||||
configId: String(source.configId || "").trim(),
|
configId: String(source.configId || '').trim(),
|
||||||
internalSkuCode: String(source.internalSkuCode || "").trim(),
|
internalSkuCode: String(source.internalSkuCode || '').trim(),
|
||||||
internalSkuName: String(source.internalSkuName || "").trim(),
|
internalSkuName: String(source.internalSkuName || '').trim(),
|
||||||
deliveryItems,
|
deliveryItems,
|
||||||
ticket: {
|
ticket: {
|
||||||
code: String(ticket.code || "").trim(),
|
code: String(ticket.code || '').trim(),
|
||||||
status: String(ticket.status || "pending").trim() || "pending",
|
status: String(ticket.status || 'pending').trim() || 'pending',
|
||||||
capturedAt: ticket.capturedAt || null,
|
capturedAt: ticket.capturedAt || null,
|
||||||
capturedBy: ticket.capturedBy || null,
|
capturedBy: ticket.capturedBy || null,
|
||||||
verifiedAt: ticket.verifiedAt || null,
|
verifiedAt: ticket.verifiedAt || null,
|
||||||
oid: String(ticket.oid || "").trim(),
|
oid: String(ticket.oid || '').trim(),
|
||||||
formToken: String(ticket.formToken || "").trim(),
|
formToken: String(ticket.formToken || '').trim(),
|
||||||
leftCount: Number(ticket.leftCount || 0) || 0,
|
leftCount: Number(ticket.leftCount || 0) || 0,
|
||||||
goodsTitle: String(ticket.goodsTitle || "").trim(),
|
goodsTitle: String(ticket.goodsTitle || '').trim(),
|
||||||
},
|
},
|
||||||
binding: {
|
binding: {
|
||||||
prepareStatus:
|
prepareStatus: String(binding.prepareStatus || 'pending').trim() || 'pending',
|
||||||
String(binding.prepareStatus || "pending").trim() || "pending",
|
|
||||||
cloudSourceKeys: normalizeStringArray(binding.cloudSourceKeys),
|
cloudSourceKeys: normalizeStringArray(binding.cloudSourceKeys),
|
||||||
resolvedSourceKey: String(binding.resolvedSourceKey || "").trim(),
|
resolvedSourceKey: String(binding.resolvedSourceKey || '').trim(),
|
||||||
skuId: Number(binding.skuId || 0) || 0,
|
skuId: Number(binding.skuId || 0) || 0,
|
||||||
skuName: String(binding.skuName || "").trim(),
|
skuName: String(binding.skuName || '').trim(),
|
||||||
vnKey:
|
vnKey:
|
||||||
String(binding.vnKey || KUAISHOU_CLOUD_FIXED_VN_KEY).trim() ||
|
String(binding.vnKey || KUAISHOU_CLOUD_FIXED_VN_KEY).trim() || KUAISHOU_CLOUD_FIXED_VN_KEY,
|
||||||
KUAISHOU_CLOUD_FIXED_VN_KEY,
|
|
||||||
vnId: Number(binding.vnId || 0) || 0,
|
vnId: Number(binding.vnId || 0) || 0,
|
||||||
vnPhone: String(binding.vnPhone || "").trim(),
|
vnPhone: String(binding.vnPhone || '').trim(),
|
||||||
bindUrl: String(binding.bindUrl || "").trim(),
|
bindUrl: String(binding.bindUrl || '').trim(),
|
||||||
bindPreparedAt,
|
bindPreparedAt,
|
||||||
bindExpiresAt,
|
bindExpiresAt,
|
||||||
bindProbeAt: binding.bindProbeAt || null,
|
bindProbeAt: binding.bindProbeAt || null,
|
||||||
bindProbeStatus: String(binding.bindProbeStatus || "").trim(),
|
bindProbeStatus: String(binding.bindProbeStatus || '').trim(),
|
||||||
bindProbeMessage: String(binding.bindProbeMessage || "").trim(),
|
bindProbeMessage: String(binding.bindProbeMessage || '').trim(),
|
||||||
roleName,
|
roleName,
|
||||||
roleId,
|
roleId,
|
||||||
},
|
},
|
||||||
role: {
|
role: {
|
||||||
status:
|
status: String(role.status || (roleName || roleId ? 'ready' : 'pending')).trim() || 'pending',
|
||||||
String(
|
|
||||||
role.status || (roleName || roleId ? "ready" : "pending")
|
|
||||||
).trim() || "pending",
|
|
||||||
name: roleName,
|
name: roleName,
|
||||||
rid: roleId,
|
rid: roleId,
|
||||||
refreshedAt: role.refreshedAt || null,
|
refreshedAt: role.refreshedAt || null,
|
||||||
errorMessage: String(role.errorMessage || "").trim(),
|
errorMessage: String(role.errorMessage || '').trim(),
|
||||||
rawInfo:
|
rawInfo: role.rawInfo && typeof role.rawInfo === 'object' ? role.rawInfo : null,
|
||||||
role.rawInfo && typeof role.rawInfo === "object" ? role.rawInfo : null,
|
|
||||||
},
|
},
|
||||||
purchase: {
|
purchase: {
|
||||||
autoBuyEnabled: purchase.autoBuyEnabled !== false,
|
autoBuyEnabled: purchase.autoBuyEnabled !== false,
|
||||||
@@ -105,171 +84,166 @@ export function normalizeKuaishouCloudFlow(value: unknown) {
|
|||||||
items: Array.isArray(purchase.items) ? purchase.items : [],
|
items: Array.isArray(purchase.items) ? purchase.items : [],
|
||||||
},
|
},
|
||||||
dispatch: {
|
dispatch: {
|
||||||
status: String(dispatch.status || "pending").trim() || "pending",
|
status: String(dispatch.status || 'pending').trim() || 'pending',
|
||||||
dispatchAt: dispatch.dispatchAt || null,
|
dispatchAt: dispatch.dispatchAt || null,
|
||||||
dispatchBy: dispatch.dispatchBy || null,
|
dispatchBy: dispatch.dispatchBy || null,
|
||||||
sendType: Number(dispatch.sendType || 0) || 0,
|
sendType: Number(dispatch.sendType || 0) || 0,
|
||||||
note: String(dispatch.note || "").trim(),
|
note: String(dispatch.note || '').trim(),
|
||||||
items: Array.isArray(dispatch.items) ? dispatch.items : [],
|
items: Array.isArray(dispatch.items) ? dispatch.items : [],
|
||||||
},
|
},
|
||||||
returnNumber: {
|
returnNumber: {
|
||||||
status: String(returnNumber.status || "pending").trim() || "pending",
|
status: String(returnNumber.status || 'pending').trim() || 'pending',
|
||||||
returnedAt: returnNumber.returnedAt || null,
|
returnedAt: returnNumber.returnedAt || null,
|
||||||
returnedBy: returnNumber.returnedBy || null,
|
returnedBy: returnNumber.returnedBy || null,
|
||||||
autoReturnEnabled: returnNumber.autoReturnEnabled === true,
|
autoReturnEnabled: returnNumber.autoReturnEnabled === true,
|
||||||
},
|
},
|
||||||
consume: {
|
consume: {
|
||||||
status: String(consume.status || "pending").trim() || "pending",
|
status: String(consume.status || 'pending').trim() || 'pending',
|
||||||
shopId: String(consume.shopId || "").trim(),
|
shopId: String(consume.shopId || '').trim(),
|
||||||
shopName: String(consume.shopName || "").trim(),
|
shopName: String(consume.shopName || '').trim(),
|
||||||
autoConsumeEnabled: consume.autoConsumeEnabled === true,
|
autoConsumeEnabled: consume.autoConsumeEnabled === true,
|
||||||
consumedAt: consume.consumedAt || null,
|
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) {
|
export function normalizeKuaishouCloudDeliveryItems(value: unknown) {
|
||||||
const source: JsonObject = value && typeof value === "object" ? value as JsonObject : {};
|
const source: JsonObject = value && typeof value === 'object' ? (value as JsonObject) : {}
|
||||||
const binding =
|
const binding = source.binding && typeof source.binding === 'object' ? source.binding : {}
|
||||||
source.binding && typeof source.binding === "object" ? source.binding : {};
|
return normalizeDeliveryItems(source, binding)
|
||||||
return normalizeDeliveryItems(source, binding);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function normalizeKuaishouCloudRoleInfo(value: unknown) {
|
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 =
|
const nestedBindInfo =
|
||||||
rawInfo?.sBindInfo && typeof rawInfo.sBindInfo === "object"
|
rawInfo?.sBindInfo && typeof rawInfo.sBindInfo === 'object' ? rawInfo.sBindInfo : null
|
||||||
? rawInfo.sBindInfo
|
const source = nestedBindInfo || rawInfo
|
||||||
: null;
|
|
||||||
const source = nestedBindInfo || rawInfo;
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
name: String(
|
name: String(
|
||||||
source?.name ||
|
source?.name || source?.roleName || source?.nickname || source?.sRoleName || '',
|
||||||
source?.roleName ||
|
|
||||||
source?.nickname ||
|
|
||||||
source?.sRoleName ||
|
|
||||||
""
|
|
||||||
).trim(),
|
).trim(),
|
||||||
rid: String(
|
rid: String(
|
||||||
source?.rid ||
|
source?.rid || source?.roleId || source?.uid || source?.sRoleId || source?.sUserId || '',
|
||||||
source?.roleId ||
|
|
||||||
source?.uid ||
|
|
||||||
source?.sRoleId ||
|
|
||||||
source?.sUserId ||
|
|
||||||
""
|
|
||||||
).trim(),
|
).trim(),
|
||||||
rawInfo,
|
rawInfo,
|
||||||
};
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function maskPhone(value: unknown) {
|
export function maskPhone(value: unknown) {
|
||||||
return maskPhoneValue(value);
|
return maskPhoneValue(value)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function maskCode(value: unknown) {
|
export function maskCode(value: unknown) {
|
||||||
return maskCodeValue(value);
|
return maskCodeValue(value)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function resolveKuaishouCloudBindUrlExpiresAt(preparedAt: unknown) {
|
export function resolveKuaishouCloudBindUrlExpiresAt(preparedAt: unknown) {
|
||||||
const preparedTime = Date.parse(String(preparedAt || ""));
|
const preparedTime = Date.parse(String(preparedAt || ''))
|
||||||
if (!Number.isFinite(preparedTime)) {
|
if (!Number.isFinite(preparedTime)) {
|
||||||
return null;
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
const config = resolveCloudtentaclesConfig();
|
const config = resolveCloudtentaclesConfig()
|
||||||
const ttlSeconds = Number(config.bindUrlTtlSeconds || 600);
|
const ttlSeconds = Number(config.bindUrlTtlSeconds || 600)
|
||||||
return new Date(preparedTime + Math.max(1, ttlSeconds) * 1000).toISOString();
|
return new Date(preparedTime + Math.max(1, ttlSeconds) * 1000).toISOString()
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isKuaishouCloudBindUrlFresh(flow: unknown, now = new Date()) {
|
export function isKuaishouCloudBindUrlFresh(flow: unknown, now = new Date()) {
|
||||||
const normalizedFlow = normalizeKuaishouCloudFlow(flow);
|
const normalizedFlow = normalizeKuaishouCloudFlow(flow)
|
||||||
if (!normalizedFlow.binding.bindUrl) {
|
if (!normalizedFlow.binding.bindUrl) {
|
||||||
return false;
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
const expiresAt = normalizedFlow.binding.bindExpiresAt;
|
const expiresAt = normalizedFlow.binding.bindExpiresAt
|
||||||
if (!expiresAt) {
|
if (!expiresAt) {
|
||||||
return false;
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
const expiresTime = Date.parse(String(expiresAt || ""));
|
const expiresTime = Date.parse(String(expiresAt || ''))
|
||||||
if (!Number.isFinite(expiresTime)) {
|
if (!Number.isFinite(expiresTime)) {
|
||||||
return false;
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
return expiresTime > now.getTime();
|
return expiresTime > now.getTime()
|
||||||
}
|
}
|
||||||
|
|
||||||
export function normalizeStringArray(value: unknown) {
|
export function normalizeStringArray(value: unknown) {
|
||||||
if (Array.isArray(value)) {
|
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
|
return value
|
||||||
.split(",")
|
.split(',')
|
||||||
.map((v) => v.trim())
|
.map((v) => v.trim())
|
||||||
.filter(Boolean);
|
.filter(Boolean)
|
||||||
}
|
}
|
||||||
return [];
|
return []
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalizeDeliveryItems(source: JsonObject, binding: JsonObject) {
|
function normalizeDeliveryItems(source: JsonObject, binding: JsonObject) {
|
||||||
const rawItems = Array.isArray(source.deliveryItems) ? source.deliveryItems : [];
|
const rawItems = Array.isArray(source.deliveryItems) ? source.deliveryItems : []
|
||||||
const items = rawItems
|
const items = rawItems.map((item) => normalizeDeliveryItem(item)).filter(Boolean) as Array<{
|
||||||
.map((item) => normalizeDeliveryItem(item))
|
cloudSkuId: number
|
||||||
.filter(Boolean) as Array<{ cloudSkuId: number; cloudSkuName: string; quantity: number }>;
|
cloudSkuName: string
|
||||||
|
quantity: number
|
||||||
|
}>
|
||||||
|
|
||||||
if (items.length > 0) {
|
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) {
|
if (!skuId) {
|
||||||
return [];
|
return []
|
||||||
}
|
}
|
||||||
|
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
cloudSkuId: skuId,
|
cloudSkuId: skuId,
|
||||||
cloudSkuName: String(binding.skuName || "").trim(),
|
cloudSkuName: String(binding.skuName || '').trim(),
|
||||||
quantity: 1,
|
quantity: 1,
|
||||||
},
|
},
|
||||||
];
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalizeDeliveryItem(value: unknown) {
|
function normalizeDeliveryItem(value: unknown) {
|
||||||
const source = value && typeof value === "object" ? value as JsonObject : {};
|
const source = value && typeof value === 'object' ? (value as JsonObject) : {}
|
||||||
const cloudSkuId = Number(source.cloudSkuId || source.skuId || 0) || 0;
|
const cloudSkuId = Number(source.cloudSkuId || source.skuId || 0) || 0
|
||||||
if (!Number.isInteger(cloudSkuId) || cloudSkuId <= 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 {
|
return {
|
||||||
cloudSkuId,
|
cloudSkuId,
|
||||||
cloudSkuName: String(source.cloudSkuName || source.skuName || "").trim(),
|
cloudSkuName: String(source.cloudSkuName || source.skuName || '').trim(),
|
||||||
quantity: Number.isInteger(quantity) && quantity > 0 ? quantity : 1,
|
quantity: Number.isInteger(quantity) && quantity > 0 ? quantity : 1,
|
||||||
};
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function mergeDeliveryItems(
|
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) {
|
for (const item of items) {
|
||||||
const existing = merged.get(item.cloudSkuId);
|
const existing = merged.get(item.cloudSkuId)
|
||||||
if (existing) {
|
if (existing) {
|
||||||
existing.quantity += item.quantity;
|
existing.quantity += item.quantity
|
||||||
existing.cloudSkuName = existing.cloudSkuName || item.cloudSkuName;
|
existing.cloudSkuName = existing.cloudSkuName || item.cloudSkuName
|
||||||
continue;
|
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 = {}) {
|
export async function probeKuaishouCloudTaskBindUrl(task: TaskRow, options: JsonObject = {}) {
|
||||||
if (!isKuaishouCloudTask(task)) {
|
if (!isKuaishouCloudTask(task)) {
|
||||||
throw createHttpError('当前任务不是快手 Cloud 履约任务', {
|
throw createHttpError('当前任务不是快手 Cloud 履约任务', {
|
||||||
|
|||||||
@@ -66,6 +66,13 @@ export function refreshAdminTaskKuaishouCloudRoleInfo(taskId: number | string) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function rebindAdminTaskKuaishouCloudRole(taskId: number | string) {
|
||||||
|
return apiPost<AdminTaskActionResponse>(
|
||||||
|
`/api/v1/admin/tasks/${taskId}/kuaishou-cloud/rebind-role`,
|
||||||
|
{},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
export function dispatchAdminTaskKuaishouCloudFulfillment(
|
export function dispatchAdminTaskKuaishouCloudFulfillment(
|
||||||
taskId: number | string,
|
taskId: number | string,
|
||||||
payload: {
|
payload: {
|
||||||
|
|||||||
@@ -18,6 +18,10 @@ export function confirmKuaishouCloudClaimRole(token: string) {
|
|||||||
return apiPost<ClaimDetailData>(`/api/v1/claim/${token}/kuaishou-cloud/confirm-role`, {})
|
return apiPost<ClaimDetailData>(`/api/v1/claim/${token}/kuaishou-cloud/confirm-role`, {})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function rebindKuaishouCloudClaimRole(token: string) {
|
||||||
|
return apiPost<ClaimDetailData>(`/api/v1/claim/${token}/kuaishou-cloud/rebind-role`, {})
|
||||||
|
}
|
||||||
|
|
||||||
export function redeemKuaishouCloudClaim(token: string) {
|
export function redeemKuaishouCloudClaim(token: string) {
|
||||||
return apiPost<ClaimDetailData>(`/api/v1/claim/${token}/kuaishou-cloud/redeem`, {})
|
return apiPost<ClaimDetailData>(`/api/v1/claim/${token}/kuaishou-cloud/redeem`, {})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ export interface AdminTaskOperations {
|
|||||||
canCompleteManualDispatch: boolean
|
canCompleteManualDispatch: boolean
|
||||||
canPrepareKuaishouCloudFulfillment: boolean
|
canPrepareKuaishouCloudFulfillment: boolean
|
||||||
canRefreshKuaishouCloudRoleInfo: boolean
|
canRefreshKuaishouCloudRoleInfo: boolean
|
||||||
|
canRebindKuaishouCloudRole: boolean
|
||||||
canDispatchKuaishouCloudFulfillment: boolean
|
canDispatchKuaishouCloudFulfillment: boolean
|
||||||
canReturnKuaishouCloudFulfillment: boolean
|
canReturnKuaishouCloudFulfillment: boolean
|
||||||
canViewSensitiveTaskData: boolean
|
canViewSensitiveTaskData: boolean
|
||||||
@@ -169,6 +170,35 @@ export interface AdminTaskDetail {
|
|||||||
consumedAt: string | null
|
consumedAt: string | null
|
||||||
errorMessage: string
|
errorMessage: string
|
||||||
}
|
}
|
||||||
|
rebind: {
|
||||||
|
currentAttempt: number
|
||||||
|
history: Array<{
|
||||||
|
attempt: number
|
||||||
|
source: string
|
||||||
|
requestedAt: string | null
|
||||||
|
requestedBy: Record<string, unknown> | null
|
||||||
|
status: string
|
||||||
|
errorMessage: string
|
||||||
|
oldBinding: {
|
||||||
|
vnKey: string
|
||||||
|
vnId: number
|
||||||
|
vnPhone: string
|
||||||
|
bindUrl: string
|
||||||
|
bindPreparedAt: string | null
|
||||||
|
bindExpiresAt: string | null
|
||||||
|
roleName: string
|
||||||
|
roleId: string
|
||||||
|
}
|
||||||
|
newBinding: null | {
|
||||||
|
vnKey: string
|
||||||
|
vnId: number
|
||||||
|
vnPhone: string
|
||||||
|
bindUrl: string
|
||||||
|
bindPreparedAt: string | null
|
||||||
|
bindExpiresAt: string | null
|
||||||
|
}
|
||||||
|
}>
|
||||||
|
}
|
||||||
notes: string
|
notes: string
|
||||||
}
|
}
|
||||||
manualDispatch: null | {
|
manualDispatch: null | {
|
||||||
|
|||||||
@@ -119,6 +119,10 @@ export interface ClaimKuaishouCloudFlowInfo {
|
|||||||
consumedAt: string | null
|
consumedAt: string | null
|
||||||
errorMessage: string
|
errorMessage: string
|
||||||
}
|
}
|
||||||
|
rebind?: {
|
||||||
|
currentAttempt: number
|
||||||
|
history: Array<Record<string, unknown>>
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ClaimDetailData {
|
export interface ClaimDetailData {
|
||||||
|
|||||||
@@ -1,134 +1,135 @@
|
|||||||
export function formatStatusWithRaw(
|
export function formatStatusWithRaw(status: string, labelMap: Record<string, string> = {}) {
|
||||||
status: string,
|
const normalized = String(status || '').trim()
|
||||||
labelMap: Record<string, string> = {}
|
|
||||||
) {
|
|
||||||
const normalized = String(status || "").trim();
|
|
||||||
|
|
||||||
if (!normalized) {
|
if (!normalized) {
|
||||||
return "-";
|
return '-'
|
||||||
}
|
}
|
||||||
|
|
||||||
const key = normalized.toLowerCase();
|
const key = normalized.toLowerCase()
|
||||||
const label = labelMap[key];
|
const label = labelMap[key]
|
||||||
|
|
||||||
if (!label || label === normalized) {
|
if (!label || label === normalized) {
|
||||||
return normalized;
|
return normalized
|
||||||
}
|
}
|
||||||
|
|
||||||
return `${label} (${normalized})`;
|
return `${label} (${normalized})`
|
||||||
}
|
}
|
||||||
|
|
||||||
export function formatAuditAction(action: string) {
|
export function formatAuditAction(action: string) {
|
||||||
const labelMap: Record<string, string> = {
|
const labelMap: Record<string, string> = {
|
||||||
admin_user_created: "创建后台用户",
|
admin_user_created: '创建后台用户',
|
||||||
admin_user_role_updated: "修改用户角色",
|
admin_user_role_updated: '修改用户角色',
|
||||||
admin_user_status_updated: "修改用户状态",
|
admin_user_status_updated: '修改用户状态',
|
||||||
admin_user_password_reset: "重置用户密码",
|
admin_user_password_reset: '重置用户密码',
|
||||||
task_regenerate_claim_link: "重发领取链接",
|
task_regenerate_claim_link: '重发领取链接',
|
||||||
task_closed: "关闭任务",
|
task_kuaishou_cloud_rebind_role: '换绑角色',
|
||||||
task_mark_manual_review: "转人工处理",
|
task_closed: '关闭任务',
|
||||||
task_complete_manual_dispatch: "完成人工履约",
|
task_mark_manual_review: '转人工处理',
|
||||||
platform_shop_config_updated: "更新店铺配置",
|
task_complete_manual_dispatch: '完成人工履约',
|
||||||
platform_fulfillment_config_updated: "更新履约配置",
|
platform_shop_config_updated: '更新店铺配置',
|
||||||
};
|
platform_fulfillment_config_updated: '更新履约配置',
|
||||||
|
}
|
||||||
|
|
||||||
return formatStatusWithRaw(action, labelMap);
|
return formatStatusWithRaw(action, labelMap)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function formatAuditTargetType(targetType: string) {
|
export function formatAuditTargetType(targetType: string) {
|
||||||
const labelMap: Record<string, string> = {
|
const labelMap: Record<string, string> = {
|
||||||
admin_user: "后台用户",
|
admin_user: '后台用户',
|
||||||
task: "交付任务",
|
task: '交付任务',
|
||||||
platform_config: "平台配置",
|
platform_config: '平台配置',
|
||||||
};
|
}
|
||||||
|
|
||||||
return formatStatusWithRaw(targetType, labelMap);
|
return formatStatusWithRaw(targetType, labelMap)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function formatTaskEventType(eventType: string) {
|
export function formatTaskEventType(eventType: string) {
|
||||||
const labelMap: Record<string, string> = {
|
const labelMap: Record<string, string> = {
|
||||||
manual_dispatch_completed: "人工履约已回写",
|
manual_dispatch_completed: '人工履约已回写',
|
||||||
claim_redeem_code_used: "兑换码已使用",
|
claim_redeem_code_used: '兑换码已使用',
|
||||||
claim_redeem_code_invalid: "兑换码错误",
|
claim_redeem_code_invalid: '兑换码错误',
|
||||||
claim_redeem_completed: "兑换链路完成",
|
claim_redeem_completed: '兑换链路完成',
|
||||||
};
|
kuaishou_cloud_rebind_requested: '发起角色换绑',
|
||||||
|
kuaishou_cloud_rebind_old_number_returned: '换绑旧号已退还',
|
||||||
|
kuaishou_cloud_rebind_prepared: '换绑资源已准备',
|
||||||
|
kuaishou_cloud_rebind_failed: '换绑失败',
|
||||||
|
}
|
||||||
|
|
||||||
return formatStatusWithRaw(eventType, labelMap);
|
return formatStatusWithRaw(eventType, labelMap)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function formatRedeemOutcomeLabel(value: string) {
|
export function formatRedeemOutcomeLabel(value: string) {
|
||||||
const normalized = String(value || "").trim();
|
const normalized = String(value || '').trim()
|
||||||
|
|
||||||
switch (normalized) {
|
switch (normalized) {
|
||||||
case "success":
|
case 'success':
|
||||||
return "兑换成功";
|
return '兑换成功'
|
||||||
case "code_used":
|
case 'code_used':
|
||||||
return "兑换码已使用";
|
return '兑换码已使用'
|
||||||
case "code_invalid":
|
case 'code_invalid':
|
||||||
return "兑换码错误";
|
return '兑换码错误'
|
||||||
case "captcha_rejected":
|
case 'captcha_rejected':
|
||||||
return "验证码错误";
|
return '验证码错误'
|
||||||
case "failed":
|
case 'failed':
|
||||||
return "兑换失败";
|
return '兑换失败'
|
||||||
default:
|
default:
|
||||||
return normalized || "-";
|
return normalized || '-'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function formatRedeemResolutionStatus(value: string) {
|
export function formatRedeemResolutionStatus(value: string) {
|
||||||
return value === "success"
|
return value === 'success' ? '已完成' : value === 'failed' ? '失败收口' : value || '-'
|
||||||
? "已完成"
|
|
||||||
: value === "failed"
|
|
||||||
? "失败收口"
|
|
||||||
: value || "-";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function formatKuaishouRoleInfoLabel(value: string) {
|
export function formatKuaishouRoleInfoLabel(value: string) {
|
||||||
const normalized = String(value || "").trim();
|
const normalized = String(value || '').trim()
|
||||||
|
|
||||||
switch (normalized) {
|
switch (normalized) {
|
||||||
case "name":
|
case 'name':
|
||||||
return "角色名称";
|
return '角色名称'
|
||||||
case "rid":
|
case 'rid':
|
||||||
return "角色 ID";
|
return '角色 ID'
|
||||||
default:
|
default:
|
||||||
return normalized || "-";
|
return normalized || '-'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function formatTaskEventPayload(payload: Record<string, unknown>) {
|
export function formatTaskEventPayload(payload: Record<string, unknown>) {
|
||||||
const parts = [
|
const parts = [
|
||||||
payload.outcome ? `结果 ${payload.outcome}` : "",
|
payload.outcome ? `结果 ${payload.outcome}` : '',
|
||||||
payload.resultCode ? `代码 ${payload.resultCode}` : "",
|
payload.resultCode ? `代码 ${payload.resultCode}` : '',
|
||||||
payload.resultMessage ? `说明 ${payload.resultMessage}` : "",
|
payload.resultMessage ? `说明 ${payload.resultMessage}` : '',
|
||||||
payload.codeMasked ? `凭据 ${payload.codeMasked}` : "",
|
payload.codeMasked ? `凭据 ${payload.codeMasked}` : '',
|
||||||
payload.previousCodeMasked ? `旧码 ${payload.previousCodeMasked}` : "",
|
payload.previousCodeMasked ? `旧码 ${payload.previousCodeMasked}` : '',
|
||||||
payload.nextCodeMasked ? `新码 ${payload.nextCodeMasked}` : "",
|
payload.nextCodeMasked ? `新码 ${payload.nextCodeMasked}` : '',
|
||||||
payload.ticketCodeMasked ? `卡券号 ${payload.ticketCodeMasked}` : "",
|
payload.ticketCodeMasked ? `卡券号 ${payload.ticketCodeMasked}` : '',
|
||||||
payload.vnId ? `虚拟号 #${payload.vnId}` : "",
|
payload.vnId ? `虚拟号 #${payload.vnId}` : '',
|
||||||
payload.vnPhoneMasked ? `号码 ${payload.vnPhoneMasked}` : "",
|
payload.oldVnId ? `旧虚拟号 #${payload.oldVnId}` : '',
|
||||||
payload.bindUrl ? `绑定链接 ${payload.bindUrl}` : "",
|
payload.vnPhoneMasked ? `号码 ${payload.vnPhoneMasked}` : '',
|
||||||
payload.sendType ? `发送类型 ${payload.sendType}` : "",
|
payload.oldVnPhoneMasked ? `旧号码 ${payload.oldVnPhoneMasked}` : '',
|
||||||
payload.note ? `备注 ${payload.note}` : "",
|
payload.oldRoleName ? `旧角色 ${payload.oldRoleName}` : '',
|
||||||
|
payload.oldRoleId ? `旧角色ID ${payload.oldRoleId}` : '',
|
||||||
|
payload.attempt ? `第 ${payload.attempt} 次` : '',
|
||||||
|
payload.bindUrl ? `绑定链接 ${payload.bindUrl}` : '',
|
||||||
|
payload.sendType ? `发送类型 ${payload.sendType}` : '',
|
||||||
|
payload.note ? `备注 ${payload.note}` : '',
|
||||||
payload.previousOutcome
|
payload.previousOutcome
|
||||||
? `切换原因 ${formatRedeemOutcomeLabel(
|
? `切换原因 ${formatRedeemOutcomeLabel(String(payload.previousOutcome || ''))}`
|
||||||
String(payload.previousOutcome || "")
|
: '',
|
||||||
)}`
|
payload.deliveryReference ? `单号 ${payload.deliveryReference}` : '',
|
||||||
: "",
|
payload.platformOrderId ? `平台单 ${payload.platformOrderId}` : '',
|
||||||
payload.deliveryReference ? `单号 ${payload.deliveryReference}` : "",
|
payload.trigger ? `触发 ${payload.trigger}` : '',
|
||||||
payload.platformOrderId ? `平台单 ${payload.platformOrderId}` : "",
|
payload.reason ? `原因 ${payload.reason}` : '',
|
||||||
payload.trigger ? `触发 ${payload.trigger}` : "",
|
payload.purchaseTriggered === true ? '已触发购买' : '',
|
||||||
payload.reason ? `原因 ${payload.reason}` : "",
|
payload.usedKnapsack === true ? '使用背包库存' : '',
|
||||||
payload.purchaseTriggered === true ? "已触发购买" : "",
|
payload.responseStatus ? `HTTP ${payload.responseStatus}` : '',
|
||||||
payload.usedKnapsack === true ? "使用背包库存" : "",
|
payload.requestId ? `请求 ${payload.requestId}` : '',
|
||||||
payload.responseStatus ? `HTTP ${payload.responseStatus}` : "",
|
payload.errorMessage ? `错误 ${payload.errorMessage}` : '',
|
||||||
payload.requestId ? `请求 ${payload.requestId}` : "",
|
].filter(Boolean)
|
||||||
payload.errorMessage ? `错误 ${payload.errorMessage}` : "",
|
|
||||||
].filter(Boolean);
|
|
||||||
|
|
||||||
if (parts.length > 0) {
|
if (parts.length > 0) {
|
||||||
return parts.join(" · ");
|
return parts.join(' · ')
|
||||||
}
|
}
|
||||||
|
|
||||||
return JSON.stringify(payload || {});
|
return JSON.stringify(payload || {})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ export const adminAuditActionOptions = [
|
|||||||
{ label: '修改用户角色', value: 'admin_user_role_updated' },
|
{ label: '修改用户角色', value: 'admin_user_role_updated' },
|
||||||
{ label: '修改用户状态', value: 'admin_user_status_updated' },
|
{ label: '修改用户状态', value: 'admin_user_status_updated' },
|
||||||
{ label: '重置用户密码', value: 'admin_user_password_reset' },
|
{ label: '重置用户密码', value: 'admin_user_password_reset' },
|
||||||
{ label: '重发领取链接', value: 'task_regenerate_claim_link' },
|
{ label: '换绑角色', value: 'task_kuaishou_cloud_rebind_role' },
|
||||||
{ label: '关闭任务', value: 'task_closed' },
|
{ label: '关闭任务', value: 'task_closed' },
|
||||||
{ label: '转人工处理', value: 'task_mark_manual_review' },
|
{ label: '转人工处理', value: 'task_mark_manual_review' },
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { onMounted } from 'vue'
|
|||||||
import {
|
import {
|
||||||
closeAdminTask,
|
closeAdminTask,
|
||||||
markAdminTaskManualReview,
|
markAdminTaskManualReview,
|
||||||
regenerateAdminTaskClaimLink,
|
rebindAdminTaskKuaishouCloudRole,
|
||||||
retryAdminTask,
|
retryAdminTask,
|
||||||
} from '@/services/admin'
|
} from '@/services/admin'
|
||||||
|
|
||||||
@@ -83,11 +83,11 @@ onMounted(loadDetail)
|
|||||||
`确认重试任务 ${detail!.task.taskNo} 吗?`,
|
`确认重试任务 ${detail!.task.taskNo} 吗?`,
|
||||||
)
|
)
|
||||||
"
|
"
|
||||||
@regenerate-claim-link="
|
@rebind-role="
|
||||||
runAction(
|
runAction(
|
||||||
() => regenerateAdminTaskClaimLink(detail!.task.taskId),
|
() => rebindAdminTaskKuaishouCloudRole(detail!.task.taskId),
|
||||||
'领取链接已重新生成',
|
'角色换绑资源已准备完成',
|
||||||
`确认重新生成任务 ${detail!.task.taskNo} 的领取链接吗?旧链接会失效。`,
|
`确认为任务 ${detail!.task.taskNo} 换绑角色吗?当前虚拟号会退还,并生成新的绑定二维码。`,
|
||||||
)
|
)
|
||||||
"
|
"
|
||||||
@prepare-kuaishou-cloud="submitKuaishouCloudPrepare"
|
@prepare-kuaishou-cloud="submitKuaishouCloudPrepare"
|
||||||
|
|||||||
@@ -1,38 +1,34 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { onMounted, ref } from "vue";
|
import { onMounted, ref } from 'vue'
|
||||||
|
|
||||||
import { useAdminAction } from "@/composables/useAdminAction";
|
import { useAdminAction } from '@/composables/useAdminAction'
|
||||||
import { useAdminListPage } from "@/composables/useAdminListPage";
|
import { useAdminListPage } from '@/composables/useAdminListPage'
|
||||||
import AdminPageHeader from "@/components/admin/AdminPageHeader.vue";
|
import AdminPageHeader from '@/components/admin/AdminPageHeader.vue'
|
||||||
import {
|
import {
|
||||||
closeAdminTask,
|
closeAdminTask,
|
||||||
fetchAdminTasks,
|
fetchAdminTasks,
|
||||||
markAdminTaskManualReview,
|
markAdminTaskManualReview,
|
||||||
regenerateAdminTaskClaimLink,
|
rebindAdminTaskKuaishouCloudRole,
|
||||||
retryAdminTask,
|
retryAdminTask,
|
||||||
} from "@/services/admin";
|
} from '@/services/admin'
|
||||||
import type { AdminTaskListItem } from "@/types/admin";
|
import type { AdminTaskListItem } from '@/types/admin'
|
||||||
import { hasAdminRole } from "@/utils/admin-auth";
|
import { hasAdminRole } from '@/utils/admin-auth'
|
||||||
import { adminTaskStatusOptions } from "@/utils/admin-options";
|
import { adminTaskStatusOptions } from '@/utils/admin-options'
|
||||||
import { formatAdminDateTime } from "@/utils/admin-time";
|
import { formatAdminDateTime } from '@/utils/admin-time'
|
||||||
|
|
||||||
const status = ref("");
|
const status = ref('')
|
||||||
const taskNo = ref("");
|
const taskNo = ref('')
|
||||||
const platformOrderId = ref("");
|
const platformOrderId = ref('')
|
||||||
const roleId = ref("");
|
const roleId = ref('')
|
||||||
const skuCode = ref("");
|
const skuCode = ref('')
|
||||||
const dateFrom = ref("");
|
const dateFrom = ref('')
|
||||||
const dateTo = ref("");
|
const dateTo = ref('')
|
||||||
const lastClaimUrl = ref("");
|
const lastClaimUrl = ref('')
|
||||||
|
|
||||||
const canManageTaskLifecycle = hasAdminRole("operator");
|
const canManageTaskLifecycle = hasAdminRole('operator')
|
||||||
const canCloseTasks = hasAdminRole("support");
|
const canCloseTasks = hasAdminRole('support')
|
||||||
|
|
||||||
type AdminTaskActionKey =
|
type AdminTaskActionKey = 'retry' | 'rebind_role' | 'manual_review' | 'close'
|
||||||
| "retry"
|
|
||||||
| "regenerate_claim_link"
|
|
||||||
| "manual_review"
|
|
||||||
| "close";
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
loading,
|
loading,
|
||||||
@@ -41,7 +37,7 @@ const {
|
|||||||
pagination,
|
pagination,
|
||||||
loadPage: loadTasks,
|
loadPage: loadTasks,
|
||||||
} = useAdminListPage<AdminTaskListItem>({
|
} = useAdminListPage<AdminTaskListItem>({
|
||||||
defaultErrorMessage: "读取任务列表失败",
|
defaultErrorMessage: '读取任务列表失败',
|
||||||
fetchPage: (page, pageSize) =>
|
fetchPage: (page, pageSize) =>
|
||||||
fetchAdminTasks({
|
fetchAdminTasks({
|
||||||
page,
|
page,
|
||||||
@@ -54,141 +50,144 @@ const {
|
|||||||
dateFrom: dateFrom.value.trim(),
|
dateFrom: dateFrom.value.trim(),
|
||||||
dateTo: dateTo.value.trim(),
|
dateTo: dateTo.value.trim(),
|
||||||
}),
|
}),
|
||||||
});
|
})
|
||||||
|
|
||||||
const { actionLoadingId, runAction } = useAdminAction();
|
const { actionLoadingId, runAction } = useAdminAction()
|
||||||
|
|
||||||
function handleActionCommand(
|
function handleActionCommand(actionKey: AdminTaskActionKey, item: AdminTaskListItem) {
|
||||||
actionKey: AdminTaskActionKey,
|
if (actionKey === 'retry') {
|
||||||
item: AdminTaskListItem
|
|
||||||
) {
|
|
||||||
if (actionKey === "retry") {
|
|
||||||
void runAction({
|
void runAction({
|
||||||
id: item.taskId,
|
id: item.taskId,
|
||||||
action: async () => {
|
action: async () => {
|
||||||
const response = await retryAdminTask(item.taskId);
|
const response = await retryAdminTask(item.taskId)
|
||||||
if (response.data.claimUrl) lastClaimUrl.value = response.data.claimUrl;
|
if (response.data.claimUrl) lastClaimUrl.value = response.data.claimUrl
|
||||||
},
|
},
|
||||||
successMessage: "任务已重试",
|
successMessage: '任务已重试',
|
||||||
confirmText: `确认重试任务 ${item.taskNo} 吗?`,
|
confirmText: `确认重试任务 ${item.taskNo} 吗?`,
|
||||||
onAfterAction: async () => {
|
onAfterAction: async () => {
|
||||||
await loadTasks();
|
await loadTasks()
|
||||||
},
|
},
|
||||||
});
|
})
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
if (actionKey === "regenerate_claim_link") {
|
if (actionKey === 'rebind_role') {
|
||||||
void runAction({
|
void runAction({
|
||||||
id: item.taskId,
|
id: item.taskId,
|
||||||
action: async () => {
|
action: async () => {
|
||||||
const response = await regenerateAdminTaskClaimLink(item.taskId);
|
const response = await rebindAdminTaskKuaishouCloudRole(item.taskId)
|
||||||
if (response.data.claimUrl) lastClaimUrl.value = response.data.claimUrl;
|
if (response.data.claimUrl) lastClaimUrl.value = response.data.claimUrl
|
||||||
},
|
},
|
||||||
successMessage: "领取链接已重新生成",
|
successMessage: '角色换绑资源已准备完成',
|
||||||
confirmText: `确认重新生成任务 ${item.taskNo} 的领取链接吗?旧链接会失效。`,
|
confirmText: `确认为任务 ${item.taskNo} 换绑角色吗?当前虚拟号会退还,并生成新的绑定二维码。`,
|
||||||
onAfterAction: async () => {
|
onAfterAction: async () => {
|
||||||
await loadTasks();
|
await loadTasks()
|
||||||
},
|
},
|
||||||
});
|
})
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
if (actionKey === "manual_review") {
|
if (actionKey === 'manual_review') {
|
||||||
void runAction({
|
void runAction({
|
||||||
id: item.taskId,
|
id: item.taskId,
|
||||||
action: () => markAdminTaskManualReview(item.taskId),
|
action: () => markAdminTaskManualReview(item.taskId),
|
||||||
successMessage: "任务已转人工处理",
|
successMessage: '任务已转人工处理',
|
||||||
confirmText: `确认将任务 ${item.taskNo} 转为人工处理吗?`,
|
confirmText: `确认将任务 ${item.taskNo} 转为人工处理吗?`,
|
||||||
onAfterAction: async () => {
|
onAfterAction: async () => {
|
||||||
await loadTasks();
|
await loadTasks()
|
||||||
},
|
},
|
||||||
});
|
})
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
void runAction({
|
void runAction({
|
||||||
id: item.taskId,
|
id: item.taskId,
|
||||||
action: () => closeAdminTask(item.taskId),
|
action: () => closeAdminTask(item.taskId),
|
||||||
successMessage: "任务已关闭",
|
successMessage: '任务已关闭',
|
||||||
confirmText: `确认关闭任务 ${item.taskNo} 吗?关闭后不会自动继续推进。`,
|
confirmText: `确认关闭任务 ${item.taskNo} 吗?关闭后不会自动继续推进。`,
|
||||||
onAfterAction: async () => {
|
onAfterAction: async () => {
|
||||||
await loadTasks();
|
await loadTasks()
|
||||||
},
|
},
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function resetFilters() {
|
function resetFilters() {
|
||||||
status.value = "";
|
status.value = ''
|
||||||
taskNo.value = "";
|
taskNo.value = ''
|
||||||
platformOrderId.value = "";
|
platformOrderId.value = ''
|
||||||
roleId.value = "";
|
roleId.value = ''
|
||||||
skuCode.value = "";
|
skuCode.value = ''
|
||||||
dateFrom.value = "";
|
dateFrom.value = ''
|
||||||
dateTo.value = "";
|
dateTo.value = ''
|
||||||
void loadTasks(1);
|
void loadTasks(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolveResourceStageLabel(status: string) {
|
function resolveResourceStageLabel(status: string) {
|
||||||
const normalized = String(status || "").trim();
|
const normalized = String(status || '').trim()
|
||||||
const labelMap: Record<string, string> = {
|
const labelMap: Record<string, string> = {
|
||||||
resource_ready: "资源已准备",
|
resource_ready: '资源已准备',
|
||||||
pending_prepare: "待准备资源",
|
pending_prepare: '待准备资源',
|
||||||
pending_payment: "待支付",
|
pending_payment: '待支付',
|
||||||
pending_binding_prepare: "待准备资源",
|
pending_binding_prepare: '待准备资源',
|
||||||
retry_pending: "待重试",
|
retry_pending: '待重试',
|
||||||
manual_review: "人工处理",
|
manual_review: '人工处理',
|
||||||
resource_exception: "资源异常",
|
resource_exception: '资源异常',
|
||||||
closed: "已关闭",
|
closed: '已关闭',
|
||||||
expired: "已过期",
|
expired: '已过期',
|
||||||
completed: "已完成",
|
completed: '已完成',
|
||||||
not_started: "未开始",
|
not_started: '未开始',
|
||||||
};
|
}
|
||||||
|
|
||||||
return labelMap[normalized] || normalized || "-";
|
return labelMap[normalized] || normalized || '-'
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolveCustomerStageLabel(status: string) {
|
function resolveCustomerStageLabel(status: string) {
|
||||||
const normalized = String(status || "").trim();
|
const normalized = String(status || '').trim()
|
||||||
const labelMap: Record<string, string> = {
|
const labelMap: Record<string, string> = {
|
||||||
not_started: "待打开链接",
|
not_started: '待打开链接',
|
||||||
waiting_customer: "待客户绑定",
|
waiting_customer: '待客户绑定',
|
||||||
link_opened: "已打开链接",
|
link_opened: '已打开链接',
|
||||||
customer_processing: "处理中",
|
customer_processing: '处理中',
|
||||||
customer_confirmed: "已提交",
|
customer_confirmed: '已提交',
|
||||||
customer_completed: "客户已完成",
|
customer_completed: '客户已完成',
|
||||||
customer_exception: "客户步骤异常",
|
customer_exception: '客户步骤异常',
|
||||||
closed: "已关闭",
|
closed: '已关闭',
|
||||||
expired: "已过期",
|
expired: '已过期',
|
||||||
};
|
}
|
||||||
|
|
||||||
return labelMap[normalized] || normalized || "-";
|
return labelMap[normalized] || normalized || '-'
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolveTaskMainText(item: AdminTaskListItem) {
|
function resolveTaskMainText(item: AdminTaskListItem) {
|
||||||
if (item.lastError) return "需要关注";
|
if (item.lastError) return '需要关注'
|
||||||
if (["failed", "manual_review", "retry_pending"].includes(item.status)) return "待处理";
|
if (['failed', 'manual_review', 'retry_pending'].includes(item.status)) return '待处理'
|
||||||
if (["redeemed", "completed", "closed"].includes(item.status)) return "已完成";
|
if (['redeemed', 'completed', 'closed'].includes(item.status)) return '已完成'
|
||||||
if (item.customerStatus === "waiting_customer") return "等待客户绑定";
|
if (item.customerStatus === 'waiting_customer') return '等待客户绑定'
|
||||||
if (item.resourceStatus === "resource_ready") return "资源已准备";
|
if (item.resourceStatus === 'resource_ready') return '资源已准备'
|
||||||
if (item.status === "pending_binding_prepare") return "待准备资源";
|
if (item.status === 'pending_binding_prepare') return '待准备资源'
|
||||||
return item.status || "处理中";
|
return item.status || '处理中'
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolveTaskTone(item: AdminTaskListItem) {
|
function resolveTaskTone(item: AdminTaskListItem) {
|
||||||
if (item.lastError || ["failed", "manual_review", "retry_pending"].includes(item.status)) return "danger";
|
if (item.lastError || ['failed', 'manual_review', 'retry_pending'].includes(item.status))
|
||||||
if (["redeemed", "completed", "closed"].includes(item.status)) return "success";
|
return 'danger'
|
||||||
return "warning";
|
if (['redeemed', 'completed', 'closed'].includes(item.status)) return 'success'
|
||||||
|
return 'warning'
|
||||||
|
}
|
||||||
|
|
||||||
|
function canRebindListTask(item: AdminTaskListItem) {
|
||||||
|
return (
|
||||||
|
canManageTaskLifecycle &&
|
||||||
|
item.executorKey === 'kuaishou_ct_assisted' &&
|
||||||
|
['waiting_binding', 'role_confirmed', 'manual_review', 'retry_pending'].includes(item.status)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
void loadTasks(1);
|
void loadTasks(1)
|
||||||
});
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="tasks-page list-page">
|
<div class="tasks-page list-page">
|
||||||
<AdminPageHeader
|
<AdminPageHeader title="交付任务" description="查看自动履约任务、客户步骤和异常处理。">
|
||||||
title="交付任务"
|
|
||||||
description="查看自动履约任务、客户步骤和异常处理。"
|
|
||||||
>
|
|
||||||
<template #extra>
|
<template #extra>
|
||||||
<span class="total-badge">共 {{ pagination.total }} 条任务</span>
|
<span class="total-badge">共 {{ pagination.total }} 条任务</span>
|
||||||
</template>
|
</template>
|
||||||
@@ -198,9 +197,7 @@ onMounted(() => {
|
|||||||
<template #header>
|
<template #header>
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<span class="card-title">查询任务</span>
|
<span class="card-title">查询任务</span>
|
||||||
<span class="card-desc"
|
<span class="card-desc">按状态、任务号、订单号、商品和日期筛选。</span>
|
||||||
>按状态、任务号、订单号、商品和日期筛选。</span
|
|
||||||
>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<div class="filter-grid">
|
<div class="filter-grid">
|
||||||
@@ -224,12 +221,7 @@ onMounted(() => {
|
|||||||
clearable
|
clearable
|
||||||
@keyup.enter="loadTasks(1)"
|
@keyup.enter="loadTasks(1)"
|
||||||
/>
|
/>
|
||||||
<el-input
|
<el-input v-model="roleId" placeholder="角色 ID" clearable @keyup.enter="loadTasks(1)" />
|
||||||
v-model="roleId"
|
|
||||||
placeholder="角色 ID"
|
|
||||||
clearable
|
|
||||||
@keyup.enter="loadTasks(1)"
|
|
||||||
/>
|
|
||||||
<el-input
|
<el-input
|
||||||
v-model="skuCode"
|
v-model="skuCode"
|
||||||
placeholder="商品名 / SKU"
|
placeholder="商品名 / SKU"
|
||||||
@@ -265,7 +257,7 @@ onMounted(() => {
|
|||||||
/>
|
/>
|
||||||
<el-alert
|
<el-alert
|
||||||
v-if="lastClaimUrl"
|
v-if="lastClaimUrl"
|
||||||
:title="`最近生成的领取链接:${lastClaimUrl}`"
|
:title="`最近可用领取链接:${lastClaimUrl}`"
|
||||||
type="info"
|
type="info"
|
||||||
show-icon
|
show-icon
|
||||||
:closable="false"
|
:closable="false"
|
||||||
@@ -285,17 +277,10 @@ onMounted(() => {
|
|||||||
<template v-else>
|
<template v-else>
|
||||||
<div class="tasks-toolbar table-toolbar">
|
<div class="tasks-toolbar table-toolbar">
|
||||||
<strong>任务列表</strong>
|
<strong>任务列表</strong>
|
||||||
<span
|
<span>第 {{ pagination.page }} 页,当前展示 {{ items.length }} 条</span>
|
||||||
>第 {{ pagination.page }} 页,当前展示 {{ items.length }} 条</span
|
|
||||||
>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<el-table
|
<el-table :data="items" stripe size="small" class="tasks-table data-table">
|
||||||
:data="items"
|
|
||||||
stripe
|
|
||||||
size="small"
|
|
||||||
class="tasks-table data-table"
|
|
||||||
>
|
|
||||||
<el-table-column label="任务 / 订单" min-width="230">
|
<el-table-column label="任务 / 订单" min-width="230">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<div class="cell-stack">
|
<div class="cell-stack">
|
||||||
@@ -310,9 +295,7 @@ onMounted(() => {
|
|||||||
<span class="compact-chip" :title="row.platformOrderId"
|
<span class="compact-chip" :title="row.platformOrderId"
|
||||||
>订单 {{ row.platformOrderId }}</span
|
>订单 {{ row.platformOrderId }}</span
|
||||||
>
|
>
|
||||||
<span class="compact-chip compact-chip--muted"
|
<span class="compact-chip compact-chip--muted">重试 {{ row.retryCount }} 次</span>
|
||||||
>重试 {{ row.retryCount }} 次</span
|
|
||||||
>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -323,7 +306,7 @@ onMounted(() => {
|
|||||||
<span class="cell-title" :title="row.skuName || row.skuCode">{{
|
<span class="cell-title" :title="row.skuName || row.skuCode">{{
|
||||||
row.skuName || row.skuCode
|
row.skuName || row.skuCode
|
||||||
}}</span>
|
}}</span>
|
||||||
<span class="cell-subline">SKU {{ row.skuCode || "-" }}</span>
|
<span class="cell-subline">SKU {{ row.skuCode || '-' }}</span>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
@@ -334,11 +317,11 @@ onMounted(() => {
|
|||||||
{{ resolveTaskMainText(row) }}
|
{{ resolveTaskMainText(row) }}
|
||||||
</span>
|
</span>
|
||||||
<p class="task-status-note">
|
<p class="task-status-note">
|
||||||
客户 {{ resolveCustomerStageLabel(row.customerStatus) }}
|
客户 {{ resolveCustomerStageLabel(row.customerStatus) }} · 资源
|
||||||
· 资源 {{ resolveResourceStageLabel(row.resourceStatus) }}
|
{{ resolveResourceStageLabel(row.resourceStatus) }}
|
||||||
</p>
|
</p>
|
||||||
<p v-if="row.roleName || row.roleId" class="task-status-note">
|
<p v-if="row.roleName || row.roleId" class="task-status-note">
|
||||||
角色 {{ row.roleName || "-" }} / {{ row.roleId || "-" }}
|
角色 {{ row.roleName || '-' }} / {{ row.roleId || '-' }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -346,12 +329,8 @@ onMounted(() => {
|
|||||||
<el-table-column label="时间" min-width="180">
|
<el-table-column label="时间" min-width="180">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<div class="cell-stack">
|
<div class="cell-stack">
|
||||||
<span class="cell-subline"
|
<span class="cell-subline">创建 {{ formatAdminDateTime(row.createdAt) }}</span>
|
||||||
>创建 {{ formatAdminDateTime(row.createdAt) }}</span
|
<span class="cell-subline">更新 {{ formatAdminDateTime(row.updatedAt) }}</span>
|
||||||
>
|
|
||||||
<span class="cell-subline"
|
|
||||||
>更新 {{ formatAdminDateTime(row.updatedAt) }}</span
|
|
||||||
>
|
|
||||||
<span v-if="row.claimedAt" class="cell-subline"
|
<span v-if="row.claimedAt" class="cell-subline"
|
||||||
>领取 {{ formatAdminDateTime(row.claimedAt) }}</span
|
>领取 {{ formatAdminDateTime(row.claimedAt) }}</span
|
||||||
>
|
>
|
||||||
@@ -368,7 +347,7 @@ onMounted(() => {
|
|||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<div class="cell-stack">
|
<div class="cell-stack">
|
||||||
<span class="cell-error" :title="row.lastError || '-'">{{
|
<span class="cell-error" :title="row.lastError || '-'">{{
|
||||||
row.lastError || "当前无异常"
|
row.lastError || '当前无异常'
|
||||||
}}</span>
|
}}</span>
|
||||||
<div class="action-stack">
|
<div class="action-stack">
|
||||||
<el-button
|
<el-button
|
||||||
@@ -386,14 +365,11 @@ onMounted(() => {
|
|||||||
重试
|
重试
|
||||||
</el-button>
|
</el-button>
|
||||||
<el-button
|
<el-button
|
||||||
v-if="
|
v-if="canRebindListTask(row)"
|
||||||
canManageTaskLifecycle &&
|
|
||||||
row.executorKey !== 'manual_dispatch'
|
|
||||||
"
|
|
||||||
size="small"
|
size="small"
|
||||||
@click="handleActionCommand('regenerate_claim_link', row)"
|
@click="handleActionCommand('rebind_role', row)"
|
||||||
>
|
>
|
||||||
重发链接
|
换绑角色
|
||||||
</el-button>
|
</el-button>
|
||||||
<el-button
|
<el-button
|
||||||
v-if="canManageTaskLifecycle"
|
v-if="canManageTaskLifecycle"
|
||||||
@@ -403,10 +379,7 @@ onMounted(() => {
|
|||||||
转人工
|
转人工
|
||||||
</el-button>
|
</el-button>
|
||||||
<el-button
|
<el-button
|
||||||
v-if="
|
v-if="canCloseTasks && !['redeemed', 'closed'].includes(row.status)"
|
||||||
canCloseTasks &&
|
|
||||||
!['redeemed', 'closed'].includes(row.status)
|
|
||||||
"
|
|
||||||
size="small"
|
size="small"
|
||||||
type="danger"
|
type="danger"
|
||||||
@click="handleActionCommand('close', row)"
|
@click="handleActionCommand('close', row)"
|
||||||
@@ -434,7 +407,7 @@ onMounted(() => {
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
@import "../../../styles/admin-list-pages.css";
|
@import '../../../styles/admin-list-pages.css';
|
||||||
|
|
||||||
.filter-grid {
|
.filter-grid {
|
||||||
grid-template-columns:
|
grid-template-columns:
|
||||||
@@ -465,19 +438,19 @@ onMounted(() => {
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.task-main-chip[data-tone="success"] {
|
.task-main-chip[data-tone='success'] {
|
||||||
color: #0f766e;
|
color: #0f766e;
|
||||||
background: var(--color-success-light);
|
background: var(--color-success-light);
|
||||||
border-color: #a6f4c5;
|
border-color: #a6f4c5;
|
||||||
}
|
}
|
||||||
|
|
||||||
.task-main-chip[data-tone="warning"] {
|
.task-main-chip[data-tone='warning'] {
|
||||||
color: #b54708;
|
color: #b54708;
|
||||||
background: var(--color-warning-light);
|
background: var(--color-warning-light);
|
||||||
border-color: #fedf89;
|
border-color: #fedf89;
|
||||||
}
|
}
|
||||||
|
|
||||||
.task-main-chip[data-tone="danger"] {
|
.task-main-chip[data-tone='danger'] {
|
||||||
color: var(--color-danger-dark);
|
color: var(--color-danger-dark);
|
||||||
background: var(--color-danger-light);
|
background: var(--color-danger-light);
|
||||||
border-color: #fecaca;
|
border-color: #fecaca;
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ defineProps<Props>()
|
|||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
retry: []
|
retry: []
|
||||||
regenerateClaimLink: []
|
rebindRole: []
|
||||||
prepareKuaishouCloud: []
|
prepareKuaishouCloud: []
|
||||||
dispatchKuaishouCloud: []
|
dispatchKuaishouCloud: []
|
||||||
returnKuaishouCloud: []
|
returnKuaishouCloud: []
|
||||||
@@ -35,13 +35,13 @@ const emit = defineEmits<{
|
|||||||
重试任务
|
重试任务
|
||||||
</el-button>
|
</el-button>
|
||||||
<el-button
|
<el-button
|
||||||
v-if="detail.operations.canRegenerateClaimLink"
|
v-if="detail.operations.canRebindKuaishouCloudRole"
|
||||||
:disabled="!detail.operations.canRegenerateClaimLink"
|
:disabled="!detail.operations.canRebindKuaishouCloudRole"
|
||||||
:loading="actionLoading"
|
:loading="actionLoading"
|
||||||
round
|
round
|
||||||
@click="emit('regenerateClaimLink')"
|
@click="emit('rebindRole')"
|
||||||
>
|
>
|
||||||
重发链接
|
换绑角色
|
||||||
</el-button>
|
</el-button>
|
||||||
<el-button
|
<el-button
|
||||||
v-if="canManageTaskLifecycle && detail.operations.canPrepareKuaishouCloudFulfillment"
|
v-if="canManageTaskLifecycle && detail.operations.canPrepareKuaishouCloudFulfillment"
|
||||||
|
|||||||
@@ -69,11 +69,12 @@ function resolveTimelineItemClass(status: string, isCurrent: boolean) {
|
|||||||
return ''
|
return ''
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/** 计算 checklist 中第一个「非完成」的步骤索引,即当前卡点 */
|
/** 计算 checklist 中第一个「非完成」的步骤索引,即当前卡点 */
|
||||||
function findCurrentStepIndex(checklist: ChecklistItem[]) {
|
function findCurrentStepIndex(checklist: ChecklistItem[]) {
|
||||||
const doneStatuses = ['verified', 'ready', 'completed', 'done']
|
const doneStatuses = ['verified', 'ready', 'completed', 'done']
|
||||||
const idx = checklist.findIndex((item) => !doneStatuses.includes(String(item.status || '').trim()))
|
const idx = checklist.findIndex(
|
||||||
|
(item) => !doneStatuses.includes(String(item.status || '').trim()),
|
||||||
|
)
|
||||||
return idx
|
return idx
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -108,8 +109,29 @@ function badgeLabel(tone: string) {
|
|||||||
return '待处理'
|
return '待处理'
|
||||||
}
|
}
|
||||||
|
|
||||||
const roleHasIdentity = (flow: KuaishouCloudFlow) =>
|
const roleHasIdentity = (flow: KuaishouCloudFlow) => !!(flow.role?.name || flow.role?.rid)
|
||||||
!!(flow.role?.name || flow.role?.rid)
|
|
||||||
|
function formatRebindSource(source: string) {
|
||||||
|
const normalized = String(source || '').trim()
|
||||||
|
if (normalized.includes('claim_page')) return '客户自助'
|
||||||
|
if (normalized.includes('admin')) return '后台操作'
|
||||||
|
return normalized || '-'
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatRebindBinding(
|
||||||
|
binding: { vnId?: number; vnPhone?: string; roleName?: string; roleId?: string } | null,
|
||||||
|
) {
|
||||||
|
if (!binding) return '-'
|
||||||
|
|
||||||
|
const vn = binding.vnId ? `虚拟号 ${binding.vnId}` : '虚拟号 -'
|
||||||
|
const phone = binding.vnPhone ? ` / ${binding.vnPhone}` : ''
|
||||||
|
const role =
|
||||||
|
binding.roleName || binding.roleId
|
||||||
|
? `;角色 ${binding.roleName || '-'} / ${binding.roleId || '-'}`
|
||||||
|
: ''
|
||||||
|
|
||||||
|
return `${vn}${phone}${role}`
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -155,9 +177,7 @@ const roleHasIdentity = (flow: KuaishouCloudFlow) =>
|
|||||||
<div class="spotlight-head">
|
<div class="spotlight-head">
|
||||||
<div>
|
<div>
|
||||||
<h4>客户角色</h4>
|
<h4>客户角色</h4>
|
||||||
<p class="flow-copy">
|
<p class="flow-copy">客户绑定后刷新角色信息,确认无误后再执行发货。</p>
|
||||||
客户绑定后刷新角色信息,确认无误后再执行发货。
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="spotlight-actions">
|
<div class="spotlight-actions">
|
||||||
<AdminStatusTag :status="flow.role?.status || 'pending'" />
|
<AdminStatusTag :status="flow.role?.status || 'pending'" />
|
||||||
@@ -187,9 +207,7 @@ const roleHasIdentity = (flow: KuaishouCloudFlow) =>
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- 已识别徽章 -->
|
<!-- 已识别徽章 -->
|
||||||
<div v-if="roleHasIdentity(flow)" class="role-verified-badge">
|
<div v-if="roleHasIdentity(flow)" class="role-verified-badge">✓ 角色已识别</div>
|
||||||
✓ 角色已识别
|
|
||||||
</div>
|
|
||||||
<!-- 等待识别引导 -->
|
<!-- 等待识别引导 -->
|
||||||
<div v-else class="role-pending-hint">
|
<div v-else class="role-pending-hint">
|
||||||
客户完成绑定后,点击右上角「刷新」按钮获取角色信息,确认角色名与 ID 无误后再执行发货。
|
客户完成绑定后,点击右上角「刷新」按钮获取角色信息,确认角色名与 ID 无误后再执行发货。
|
||||||
@@ -202,9 +220,7 @@ const roleHasIdentity = (flow: KuaishouCloudFlow) =>
|
|||||||
<!-- 流程进度 Timeline -->
|
<!-- 流程进度 Timeline -->
|
||||||
<section class="flow-block flow-block--checklist">
|
<section class="flow-block flow-block--checklist">
|
||||||
<h4>流程进度</h4>
|
<h4>流程进度</h4>
|
||||||
<p class="flow-copy">
|
<p class="flow-copy">当前任务只需要按这个顺序推进,异常时再展开下方明细。</p>
|
||||||
当前任务只需要按这个顺序推进,异常时再展开下方明细。
|
|
||||||
</p>
|
|
||||||
<div class="flow-timeline">
|
<div class="flow-timeline">
|
||||||
<div
|
<div
|
||||||
v-for="(item, idx) in checklist"
|
v-for="(item, idx) in checklist"
|
||||||
@@ -218,10 +234,9 @@ const roleHasIdentity = (flow: KuaishouCloudFlow) =>
|
|||||||
<div class="timeline-body">
|
<div class="timeline-body">
|
||||||
<div class="timeline-head">
|
<div class="timeline-head">
|
||||||
<span class="timeline-label">{{ item.label }}</span>
|
<span class="timeline-label">{{ item.label }}</span>
|
||||||
<span
|
<span v-if="idx === findCurrentStepIndex(checklist)" class="timeline-current-badge"
|
||||||
v-if="idx === findCurrentStepIndex(checklist)"
|
>当前</span
|
||||||
class="timeline-current-badge"
|
>
|
||||||
>当前</span>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="timeline-detail">{{ item.detail }}</div>
|
<div class="timeline-detail">{{ item.detail }}</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -404,6 +419,34 @@ const roleHasIdentity = (flow: KuaishouCloudFlow) =>
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</el-collapse-item>
|
</el-collapse-item>
|
||||||
|
|
||||||
|
<el-collapse-item v-if="flow.rebind?.history?.length" name="rebind" title="换绑记录">
|
||||||
|
<div class="event-timeline compact-event-timeline">
|
||||||
|
<div
|
||||||
|
v-for="item in flow.rebind.history"
|
||||||
|
:key="`rebind-${item.attempt}-${item.requestedAt}`"
|
||||||
|
class="event-tl-item"
|
||||||
|
>
|
||||||
|
<div class="event-tl-gutter">
|
||||||
|
<div class="event-tl-dot"></div>
|
||||||
|
</div>
|
||||||
|
<div class="event-tl-body">
|
||||||
|
<div class="event-tl-type">
|
||||||
|
第 {{ item.attempt }} 次换绑 · {{ formatRebindSource(item.source) }} ·
|
||||||
|
{{ item.status === 'success' ? '成功' : '失败' }}
|
||||||
|
</div>
|
||||||
|
<div class="event-tl-payload">原 {{ formatRebindBinding(item.oldBinding) }}</div>
|
||||||
|
<div v-if="item.newBinding" class="event-tl-payload">
|
||||||
|
新 {{ formatRebindBinding(item.newBinding) }}
|
||||||
|
</div>
|
||||||
|
<div v-if="item.errorMessage" class="event-tl-payload">
|
||||||
|
{{ item.errorMessage }}
|
||||||
|
</div>
|
||||||
|
<div class="event-tl-time">{{ formatAdminDateTime(item.requestedAt) }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-collapse-item>
|
||||||
</el-collapse>
|
</el-collapse>
|
||||||
</el-card>
|
</el-card>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -69,7 +69,9 @@ const claim = useKuaishouCloudClaim(() => props.token)
|
|||||||
:role-id="claim.roleId.value"
|
:role-id="claim.roleId.value"
|
||||||
:product="claim.product.value"
|
:product="claim.product.value"
|
||||||
:redeeming="claim.redeeming.value"
|
:redeeming="claim.redeeming.value"
|
||||||
|
:rebinding-role="claim.rebindingRole.value"
|
||||||
@confirm-redeem="claim.confirmRedeem()"
|
@confirm-redeem="claim.confirmRedeem()"
|
||||||
|
@rebind-role="claim.rebindRole()"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<ClaimResultStep
|
<ClaimResultStep
|
||||||
|
|||||||
@@ -10,10 +10,12 @@ defineProps<{
|
|||||||
roleId: string
|
roleId: string
|
||||||
product: ClaimProductInfo | null
|
product: ClaimProductInfo | null
|
||||||
redeeming: boolean
|
redeeming: boolean
|
||||||
|
rebindingRole: boolean
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
defineEmits<{
|
defineEmits<{
|
||||||
confirmRedeem: []
|
confirmRedeem: []
|
||||||
|
rebindRole: []
|
||||||
}>()
|
}>()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -48,9 +50,26 @@ defineEmits<{
|
|||||||
<span>兑换后不可取消,也不可退货。</span>
|
<span>兑换后不可取消,也不可退货。</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<el-button type="primary" size="large" :loading="redeeming" @click="$emit('confirmRedeem')">
|
<div class="action-row">
|
||||||
确认兑换
|
<el-button
|
||||||
</el-button>
|
size="large"
|
||||||
|
plain
|
||||||
|
:loading="rebindingRole"
|
||||||
|
:disabled="redeeming"
|
||||||
|
@click="$emit('rebindRole')"
|
||||||
|
>
|
||||||
|
换绑角色
|
||||||
|
</el-button>
|
||||||
|
<el-button
|
||||||
|
type="primary"
|
||||||
|
size="large"
|
||||||
|
:loading="redeeming"
|
||||||
|
:disabled="rebindingRole"
|
||||||
|
@click="$emit('confirmRedeem')"
|
||||||
|
>
|
||||||
|
确认兑换
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -120,6 +139,17 @@ defineEmits<{
|
|||||||
color: #c2410c;
|
color: #c2410c;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.action-row {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 12px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-row :deep(.el-button) {
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: 720px) {
|
@media (max-width: 720px) {
|
||||||
.content-card {
|
.content-card {
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
@@ -128,5 +158,14 @@ defineEmits<{
|
|||||||
.info-grid {
|
.info-grid {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.action-row {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-row :deep(.el-button) {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
+196
-184
@@ -1,380 +1,390 @@
|
|||||||
import { computed, onBeforeUnmount, onMounted, ref } from "vue";
|
import { computed, onBeforeUnmount, onMounted, ref } from 'vue'
|
||||||
import QRCode from "qrcode";
|
import QRCode from 'qrcode'
|
||||||
import { ElMessageBox } from "element-plus";
|
import { ElMessageBox } from 'element-plus'
|
||||||
|
|
||||||
import { showError, showSuccess } from "@/lib/feedback";
|
import { showError, showSuccess } from '@/lib/feedback'
|
||||||
import {
|
import {
|
||||||
hasKuaishouCloudRedeemResultStatus,
|
hasKuaishouCloudRedeemResultStatus,
|
||||||
isClaimInactiveTaskStatus,
|
isClaimInactiveTaskStatus,
|
||||||
isKuaishouCloudCompletedStatus,
|
isKuaishouCloudCompletedStatus,
|
||||||
isKuaishouCloudRoleConfirmedStatus,
|
isKuaishouCloudRoleConfirmedStatus,
|
||||||
} from "@/domain/task-status";
|
} from '@/domain/task-status'
|
||||||
import { formatAdminDateTime } from "@/utils/admin-time";
|
import { formatAdminDateTime } from '@/utils/admin-time'
|
||||||
import {
|
import {
|
||||||
confirmKuaishouCloudClaimRole,
|
confirmKuaishouCloudClaimRole,
|
||||||
fetchClaimDetail,
|
fetchClaimDetail,
|
||||||
|
rebindKuaishouCloudClaimRole,
|
||||||
redeemKuaishouCloudClaim,
|
redeemKuaishouCloudClaim,
|
||||||
verifyKuaishouCloudClaimTicket,
|
verifyKuaishouCloudClaimTicket,
|
||||||
} from "@/services/claim";
|
} from '@/services/claim'
|
||||||
import type { ClaimDetailData } from "@/types/claim";
|
import type { ClaimDetailData } from '@/types/claim'
|
||||||
|
|
||||||
const BINDING_PREPARE_POLL_MS = 5_000;
|
const BINDING_PREPARE_POLL_MS = 5_000
|
||||||
const ROLE_FAST_POLL_MS = 10_000;
|
const ROLE_FAST_POLL_MS = 10_000
|
||||||
const ROLE_SLOW_POLL_MS = 30_000;
|
const ROLE_SLOW_POLL_MS = 30_000
|
||||||
const ROLE_IDLE_POLL_MS = 60_000;
|
const ROLE_IDLE_POLL_MS = 60_000
|
||||||
const ROLE_FAST_WINDOW_MS = 3 * 60_000;
|
const ROLE_FAST_WINDOW_MS = 3 * 60_000
|
||||||
const ROLE_IDLE_WINDOW_MS = 10 * 60_000;
|
const ROLE_IDLE_WINDOW_MS = 10 * 60_000
|
||||||
|
|
||||||
export function useKuaishouCloudClaim(token: () => string) {
|
export function useKuaishouCloudClaim(token: () => string) {
|
||||||
const loading = ref(true);
|
const loading = ref(true)
|
||||||
const submitting = ref(false);
|
const submitting = ref(false)
|
||||||
const refreshingRole = ref(false);
|
const refreshingRole = ref(false)
|
||||||
const confirmingRole = ref(false);
|
const confirmingRole = ref(false)
|
||||||
const redeeming = ref(false);
|
const rebindingRole = ref(false)
|
||||||
const errorMessage = ref("");
|
const redeeming = ref(false)
|
||||||
const detail = ref<ClaimDetailData | null>(null);
|
const errorMessage = ref('')
|
||||||
const ticketCode = ref("");
|
const detail = ref<ClaimDetailData | null>(null)
|
||||||
const qrCodeDataUrl = ref("");
|
const ticketCode = ref('')
|
||||||
let pollTimer = 0;
|
const qrCodeDataUrl = ref('')
|
||||||
let rolePollBaselineAt = 0;
|
let pollTimer = 0
|
||||||
let rolePollBaselineKey = "";
|
let rolePollBaselineAt = 0
|
||||||
|
let rolePollBaselineKey = ''
|
||||||
|
|
||||||
// ── derived data ────────────────────────────────────────
|
// ── derived data ────────────────────────────────────────
|
||||||
|
|
||||||
const flow = computed(() => detail.value?.kuaishouCloudFulfillment || null);
|
const flow = computed(() => detail.value?.kuaishouCloudFulfillment || null)
|
||||||
const order = computed(() => detail.value?.order || null);
|
const order = computed(() => detail.value?.order || null)
|
||||||
const orderItem = computed(() => detail.value?.orderItem || null);
|
const orderItem = computed(() => detail.value?.orderItem || null)
|
||||||
const product = computed(() => detail.value?.product || null);
|
const product = computed(() => detail.value?.product || null)
|
||||||
const task = computed(() => detail.value?.task || null);
|
const task = computed(() => detail.value?.task || null)
|
||||||
|
|
||||||
// ── computed status flags ───────────────────────────────
|
// ── computed status flags ───────────────────────────────
|
||||||
|
|
||||||
const roleName = computed(
|
const roleName = computed(() => flow.value?.role.name || flow.value?.binding.roleName || '')
|
||||||
() => flow.value?.role.name || flow.value?.binding.roleName || ""
|
const roleId = computed(() => flow.value?.role.rid || flow.value?.binding.roleId || '')
|
||||||
);
|
const isTicketVerified = computed(() => flow.value?.ticket.status === 'verified')
|
||||||
const roleId = computed(
|
|
||||||
() => flow.value?.role.rid || flow.value?.binding.roleId || ""
|
|
||||||
);
|
|
||||||
const isTicketVerified = computed(
|
|
||||||
() => flow.value?.ticket.status === "verified"
|
|
||||||
);
|
|
||||||
|
|
||||||
const isBindUrlExpired = computed(() => {
|
const isBindUrlExpired = computed(() => {
|
||||||
const expiresAt = String(flow.value?.binding.bindExpiresAt || "").trim();
|
const expiresAt = String(flow.value?.binding.bindExpiresAt || '').trim()
|
||||||
if (!expiresAt) {
|
if (!expiresAt) {
|
||||||
return false;
|
return false
|
||||||
}
|
}
|
||||||
const expiresTime = Date.parse(expiresAt);
|
const expiresTime = Date.parse(expiresAt)
|
||||||
return Number.isFinite(expiresTime) && expiresTime <= Date.now();
|
return Number.isFinite(expiresTime) && expiresTime <= Date.now()
|
||||||
});
|
})
|
||||||
|
|
||||||
const isBindingPrepared = computed(
|
const isBindingPrepared = computed(
|
||||||
() =>
|
() =>
|
||||||
flow.value?.binding.prepareStatus === "ready" &&
|
flow.value?.binding.prepareStatus === 'ready' &&
|
||||||
Boolean(String(flow.value?.binding.bindUrl || "").trim()) &&
|
Boolean(String(flow.value?.binding.bindUrl || '').trim()) &&
|
||||||
!isBindUrlExpired.value
|
!isBindUrlExpired.value,
|
||||||
);
|
)
|
||||||
|
|
||||||
const isBindingPreparing = computed(
|
const isBindingPreparing = computed(() => flow.value?.binding.prepareStatus === 'pending')
|
||||||
() => flow.value?.binding.prepareStatus === "pending"
|
const canEnterBindingStep = computed(() => isTicketVerified.value)
|
||||||
);
|
const isRoleReady = computed(() => Boolean(roleName.value || roleId.value))
|
||||||
const canEnterBindingStep = computed(() => isTicketVerified.value);
|
const isRoleConfirmed = computed(() => isKuaishouCloudRoleConfirmedStatus(task.value?.status))
|
||||||
const isRoleReady = computed(() => Boolean(roleName.value || roleId.value));
|
|
||||||
const isRoleConfirmed = computed(
|
|
||||||
() => isKuaishouCloudRoleConfirmedStatus(task.value?.status)
|
|
||||||
);
|
|
||||||
const isDispatched = computed(
|
const isDispatched = computed(
|
||||||
() => String(flow.value?.dispatch.status || "").trim() === "success"
|
() => String(flow.value?.dispatch.status || '').trim() === 'success',
|
||||||
);
|
)
|
||||||
|
|
||||||
const isCompleted = computed(
|
const isCompleted = computed(
|
||||||
() =>
|
() =>
|
||||||
isKuaishouCloudCompletedStatus(task.value?.status) ||
|
isKuaishouCloudCompletedStatus(task.value?.status) ||
|
||||||
String(flow.value?.consume.status || "").trim() === "success"
|
String(flow.value?.consume.status || '').trim() === 'success',
|
||||||
);
|
)
|
||||||
|
|
||||||
const hasRedeemResult = computed(() => {
|
const hasRedeemResult = computed(() => {
|
||||||
return isDispatched.value || hasKuaishouCloudRedeemResultStatus(task.value?.status);
|
return isDispatched.value || hasKuaishouCloudRedeemResultStatus(task.value?.status)
|
||||||
});
|
})
|
||||||
|
|
||||||
const canSubmitTicket = computed(
|
const canSubmitTicket = computed(
|
||||||
() =>
|
() => !isClaimInactiveTaskStatus(task.value?.status) && !submitting.value,
|
||||||
!isClaimInactiveTaskStatus(task.value?.status) && !submitting.value
|
)
|
||||||
);
|
|
||||||
|
|
||||||
const currentStep = computed(() => {
|
const currentStep = computed(() => {
|
||||||
if (hasRedeemResult.value) {
|
if (hasRedeemResult.value) {
|
||||||
return 4;
|
return 4
|
||||||
}
|
}
|
||||||
if (isRoleConfirmed.value) {
|
if (isRoleConfirmed.value) {
|
||||||
return 3;
|
return 3
|
||||||
}
|
}
|
||||||
if (canEnterBindingStep.value) {
|
if (canEnterBindingStep.value) {
|
||||||
return 2;
|
return 2
|
||||||
}
|
}
|
||||||
return 1;
|
return 1
|
||||||
});
|
})
|
||||||
|
|
||||||
const progressText = computed(() => {
|
const progressText = computed(() => {
|
||||||
if (hasRedeemResult.value) {
|
if (hasRedeemResult.value) {
|
||||||
return "兑换结果已生成";
|
return '兑换结果已生成'
|
||||||
}
|
}
|
||||||
if (isRoleConfirmed.value) {
|
if (isRoleConfirmed.value) {
|
||||||
return "角色已确认,等待兑换";
|
return '角色已确认,等待兑换'
|
||||||
}
|
}
|
||||||
if (isTicketVerified.value) {
|
if (isTicketVerified.value) {
|
||||||
return isBindingPrepared.value
|
return isBindingPrepared.value ? '请完成扫码绑定' : '绑定链接刷新中,请稍候'
|
||||||
? "请完成扫码绑定"
|
|
||||||
: "绑定链接刷新中,请稍候";
|
|
||||||
}
|
}
|
||||||
return "等待提交核销码";
|
return '等待提交核销码'
|
||||||
});
|
})
|
||||||
|
|
||||||
const resultTitle = computed(() => {
|
const resultTitle = computed(() => {
|
||||||
if (isCompleted.value) {
|
if (isCompleted.value) {
|
||||||
return "兑换成功";
|
return '兑换成功'
|
||||||
}
|
}
|
||||||
if (isDispatched.value) {
|
if (isDispatched.value) {
|
||||||
return "兑换请求已提交";
|
return '兑换请求已提交'
|
||||||
}
|
}
|
||||||
return "结果已记录";
|
return '结果已记录'
|
||||||
});
|
})
|
||||||
|
|
||||||
const resultDescription = computed(() => {
|
const resultDescription = computed(() => {
|
||||||
if (isCompleted.value) {
|
if (isCompleted.value) {
|
||||||
return "当前兑换流程已经完成。发货、退号和核销结果会保存在后台任务界面。";
|
return '当前兑换流程已经完成。发货、退号和核销结果会保存在后台任务界面。'
|
||||||
}
|
}
|
||||||
return "你的兑换请求已经提交。发货、退号和核销结果会由系统保存在后台任务界面,无需在此页面等待。";
|
return '你的兑换请求已经提交。发货、退号和核销结果会由系统保存在后台任务界面,无需在此页面等待。'
|
||||||
});
|
})
|
||||||
|
|
||||||
// ── actions ─────────────────────────────────────────────
|
// ── actions ─────────────────────────────────────────────
|
||||||
|
|
||||||
async function generateQRCode(url: string) {
|
async function generateQRCode(url: string) {
|
||||||
if (!url) {
|
if (!url) {
|
||||||
qrCodeDataUrl.value = "";
|
qrCodeDataUrl.value = ''
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
qrCodeDataUrl.value = await QRCode.toDataURL(url, {
|
qrCodeDataUrl.value = await QRCode.toDataURL(url, {
|
||||||
width: 280,
|
width: 280,
|
||||||
margin: 2,
|
margin: 2,
|
||||||
color: {
|
color: {
|
||||||
dark: "#0f172a",
|
dark: '#0f172a',
|
||||||
light: "#ffffff",
|
light: '#ffffff',
|
||||||
},
|
},
|
||||||
});
|
})
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
qrCodeDataUrl.value = "";
|
qrCodeDataUrl.value = ''
|
||||||
console.error("生成二维码失败:", error);
|
console.error('生成二维码失败:', error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function applyDetail(nextDetail: ClaimDetailData) {
|
async function applyDetail(nextDetail: ClaimDetailData) {
|
||||||
detail.value = nextDetail;
|
detail.value = nextDetail
|
||||||
if (!ticketCode.value && nextDetail.kuaishouCloudFulfillment?.ticket.code) {
|
if (!ticketCode.value && nextDetail.kuaishouCloudFulfillment?.ticket.code) {
|
||||||
ticketCode.value = nextDetail.kuaishouCloudFulfillment.ticket.code;
|
ticketCode.value = nextDetail.kuaishouCloudFulfillment.ticket.code
|
||||||
}
|
}
|
||||||
await generateQRCode(
|
await generateQRCode(String(nextDetail.kuaishouCloudFulfillment?.binding.bindUrl || '').trim())
|
||||||
String(nextDetail.kuaishouCloudFulfillment?.binding.bindUrl || "").trim()
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadDetail(options: { silent?: boolean } = {}) {
|
async function loadDetail(options: { silent?: boolean } = {}) {
|
||||||
if (!options.silent) {
|
if (!options.silent) {
|
||||||
loading.value = true;
|
loading.value = true
|
||||||
}
|
}
|
||||||
errorMessage.value = "";
|
errorMessage.value = ''
|
||||||
try {
|
try {
|
||||||
const response = await fetchClaimDetail(token());
|
const response = await fetchClaimDetail(token())
|
||||||
await applyDetail(response.data);
|
await applyDetail(response.data)
|
||||||
syncPolling();
|
syncPolling()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
errorMessage.value =
|
errorMessage.value = error instanceof Error ? error.message : '读取领取信息失败'
|
||||||
error instanceof Error ? error.message : "读取领取信息失败";
|
stopPolling()
|
||||||
stopPolling();
|
|
||||||
} finally {
|
} finally {
|
||||||
if (!options.silent) {
|
if (!options.silent) {
|
||||||
loading.value = false;
|
loading.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function submitTicket() {
|
async function submitTicket() {
|
||||||
const normalizedTicketCode = ticketCode.value.trim();
|
const normalizedTicketCode = ticketCode.value.trim()
|
||||||
if (!normalizedTicketCode) {
|
if (!normalizedTicketCode) {
|
||||||
showError("请输入核销码");
|
showError('请输入核销码')
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
submitting.value = true;
|
submitting.value = true
|
||||||
try {
|
try {
|
||||||
const response = await verifyKuaishouCloudClaimTicket(token(), {
|
const response = await verifyKuaishouCloudClaimTicket(token(), {
|
||||||
ticketCode: normalizedTicketCode,
|
ticketCode: normalizedTicketCode,
|
||||||
});
|
})
|
||||||
await applyDetail(response.data);
|
await applyDetail(response.data)
|
||||||
showSuccess("核销码验证通过,系统已开始准备绑定资源");
|
showSuccess('核销码验证通过,系统已开始准备绑定资源')
|
||||||
syncPolling();
|
syncPolling()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
showError(error instanceof Error ? error.message : "核销码验证失败");
|
showError(error instanceof Error ? error.message : '核销码验证失败')
|
||||||
} finally {
|
} finally {
|
||||||
submitting.value = false;
|
submitting.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function refreshRole() {
|
async function refreshRole() {
|
||||||
refreshingRole.value = true;
|
refreshingRole.value = true
|
||||||
try {
|
try {
|
||||||
await loadDetail({ silent: true });
|
await loadDetail({ silent: true })
|
||||||
if (isRoleReady.value) {
|
if (isRoleReady.value) {
|
||||||
showSuccess("角色信息已刷新");
|
showSuccess('角色信息已刷新')
|
||||||
} else {
|
} else {
|
||||||
showError(
|
showError(
|
||||||
flow.value?.role.errorMessage ||
|
flow.value?.role.errorMessage || '暂时还没有识别到角色信息,请完成绑定后稍等片刻再试',
|
||||||
"暂时还没有识别到角色信息,请完成绑定后稍等片刻再试"
|
)
|
||||||
);
|
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
refreshingRole.value = false;
|
refreshingRole.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function confirmRole() {
|
async function confirmRole() {
|
||||||
confirmingRole.value = true;
|
confirmingRole.value = true
|
||||||
try {
|
try {
|
||||||
const response = await confirmKuaishouCloudClaimRole(token());
|
const response = await confirmKuaishouCloudClaimRole(token())
|
||||||
await applyDetail(response.data);
|
await applyDetail(response.data)
|
||||||
showSuccess("角色已确认,进入下一步");
|
showSuccess('角色已确认,进入下一步')
|
||||||
syncPolling();
|
syncPolling()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
showError(error instanceof Error ? error.message : "确认角色失败");
|
showError(error instanceof Error ? error.message : '确认角色失败')
|
||||||
} finally {
|
} finally {
|
||||||
confirmingRole.value = false;
|
confirmingRole.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function confirmRedeem() {
|
async function confirmRedeem() {
|
||||||
try {
|
try {
|
||||||
await ElMessageBox.confirm(
|
await ElMessageBox.confirm(
|
||||||
"兑换后不可取消,也不可退货。请确认角色和商品信息完全正确。",
|
'兑换后不可取消,也不可退货。请确认角色和商品信息完全正确。',
|
||||||
"确认兑换",
|
'确认兑换',
|
||||||
{
|
{
|
||||||
confirmButtonText: "确认兑换",
|
confirmButtonText: '确认兑换',
|
||||||
cancelButtonText: "取消",
|
cancelButtonText: '取消',
|
||||||
type: "warning",
|
type: 'warning',
|
||||||
center: true,
|
center: true,
|
||||||
}
|
},
|
||||||
);
|
)
|
||||||
} catch {
|
} catch {
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
redeeming.value = true;
|
redeeming.value = true
|
||||||
try {
|
try {
|
||||||
const response = await redeemKuaishouCloudClaim(token());
|
const response = await redeemKuaishouCloudClaim(token())
|
||||||
await applyDetail(response.data);
|
await applyDetail(response.data)
|
||||||
showSuccess("兑换请求已提交");
|
showSuccess('兑换请求已提交')
|
||||||
syncPolling();
|
syncPolling()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
showError(error instanceof Error ? error.message : "兑换失败");
|
showError(error instanceof Error ? error.message : '兑换失败')
|
||||||
} finally {
|
} finally {
|
||||||
redeeming.value = false;
|
redeeming.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function rebindRole() {
|
||||||
|
try {
|
||||||
|
await ElMessageBox.confirm(
|
||||||
|
'换绑后当前绑定链接会失效,系统会退还当前虚拟号并生成新的绑定二维码。核销码和领取商品不会改变,但需要重新扫码绑定角色。',
|
||||||
|
'确认换绑角色',
|
||||||
|
{
|
||||||
|
confirmButtonText: '确认换绑',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
type: 'warning',
|
||||||
|
center: true,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
} catch {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
rebindingRole.value = true
|
||||||
|
try {
|
||||||
|
const response = await rebindKuaishouCloudClaimRole(token())
|
||||||
|
await applyDetail(response.data)
|
||||||
|
showSuccess('新的绑定二维码已生成,请重新绑定角色')
|
||||||
|
syncPolling()
|
||||||
|
} catch (error) {
|
||||||
|
showError(error instanceof Error ? error.message : '换绑角色失败')
|
||||||
|
} finally {
|
||||||
|
rebindingRole.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function openBindUrl(useCurrentPage = false) {
|
function openBindUrl(useCurrentPage = false) {
|
||||||
const bindUrl = String(flow.value?.binding.bindUrl || "").trim();
|
const bindUrl = String(flow.value?.binding.bindUrl || '').trim()
|
||||||
if (!bindUrl) {
|
if (!bindUrl) {
|
||||||
showError("绑定链接还没准备好,请稍后刷新");
|
showError('绑定链接还没准备好,请稍后刷新')
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
if (useCurrentPage) {
|
if (useCurrentPage) {
|
||||||
window.location.assign(bindUrl);
|
window.location.assign(bindUrl)
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
window.open(bindUrl, "_blank", "noopener,noreferrer");
|
window.open(bindUrl, '_blank', 'noopener,noreferrer')
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── polling ─────────────────────────────────────────────
|
// ── polling ─────────────────────────────────────────────
|
||||||
|
|
||||||
function stopPolling() {
|
function stopPolling() {
|
||||||
if (pollTimer) {
|
if (pollTimer) {
|
||||||
window.clearTimeout(pollTimer);
|
window.clearTimeout(pollTimer)
|
||||||
pollTimer = 0;
|
pollTimer = 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function syncPolling() {
|
function syncPolling() {
|
||||||
const delay = resolveNextPollDelay();
|
const delay = resolveNextPollDelay()
|
||||||
if (!delay) {
|
if (!delay) {
|
||||||
stopPolling();
|
stopPolling()
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
stopPolling();
|
stopPolling()
|
||||||
pollTimer = window.setTimeout(() => {
|
pollTimer = window.setTimeout(() => {
|
||||||
pollTimer = 0;
|
pollTimer = 0
|
||||||
void loadDetail({ silent: true });
|
void loadDetail({ silent: true })
|
||||||
}, delay);
|
}, delay)
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolveNextPollDelay() {
|
function resolveNextPollDelay() {
|
||||||
if (
|
if (!flow.value || hasRedeemResult.value || isClaimInactiveTaskStatus(task.value?.status)) {
|
||||||
!flow.value ||
|
return 0
|
||||||
hasRedeemResult.value ||
|
|
||||||
isClaimInactiveTaskStatus(task.value?.status)
|
|
||||||
) {
|
|
||||||
return 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (currentStep.value === 2) {
|
if (currentStep.value === 2) {
|
||||||
if (!isBindingPrepared.value) {
|
if (!isBindingPrepared.value) {
|
||||||
return BINDING_PREPARE_POLL_MS;
|
return BINDING_PREPARE_POLL_MS
|
||||||
}
|
}
|
||||||
|
|
||||||
return resolveRolePollDelay();
|
return resolveRolePollDelay()
|
||||||
}
|
}
|
||||||
|
|
||||||
if (currentStep.value === 3) {
|
if (currentStep.value === 3) {
|
||||||
return 0;
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
return 0;
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolveRolePollDelay() {
|
function resolveRolePollDelay() {
|
||||||
const nextRoleKey = [roleName.value, roleId.value].join("::");
|
const nextRoleKey = [roleName.value, roleId.value].join('::')
|
||||||
const now = Date.now();
|
const now = Date.now()
|
||||||
|
|
||||||
if (nextRoleKey !== rolePollBaselineKey) {
|
if (nextRoleKey !== rolePollBaselineKey) {
|
||||||
rolePollBaselineKey = nextRoleKey;
|
rolePollBaselineKey = nextRoleKey
|
||||||
rolePollBaselineAt = now;
|
rolePollBaselineAt = now
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!rolePollBaselineAt) {
|
if (!rolePollBaselineAt) {
|
||||||
rolePollBaselineAt = now;
|
rolePollBaselineAt = now
|
||||||
}
|
}
|
||||||
|
|
||||||
const elapsedMs = now - rolePollBaselineAt;
|
const elapsedMs = now - rolePollBaselineAt
|
||||||
if (elapsedMs < ROLE_FAST_WINDOW_MS) {
|
if (elapsedMs < ROLE_FAST_WINDOW_MS) {
|
||||||
return ROLE_FAST_POLL_MS;
|
return ROLE_FAST_POLL_MS
|
||||||
}
|
}
|
||||||
|
|
||||||
if (elapsedMs < ROLE_IDLE_WINDOW_MS) {
|
if (elapsedMs < ROLE_IDLE_WINDOW_MS) {
|
||||||
return ROLE_SLOW_POLL_MS;
|
return ROLE_SLOW_POLL_MS
|
||||||
}
|
}
|
||||||
|
|
||||||
return ROLE_IDLE_POLL_MS;
|
return ROLE_IDLE_POLL_MS
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── lifecycle ───────────────────────────────────────────
|
// ── lifecycle ───────────────────────────────────────────
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
void loadDetail();
|
void loadDetail()
|
||||||
});
|
})
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
stopPolling();
|
stopPolling()
|
||||||
});
|
})
|
||||||
|
|
||||||
return {
|
return {
|
||||||
// state
|
// state
|
||||||
@@ -382,6 +392,7 @@ export function useKuaishouCloudClaim(token: () => string) {
|
|||||||
submitting,
|
submitting,
|
||||||
refreshingRole,
|
refreshingRole,
|
||||||
confirmingRole,
|
confirmingRole,
|
||||||
|
rebindingRole,
|
||||||
redeeming,
|
redeeming,
|
||||||
errorMessage,
|
errorMessage,
|
||||||
detail,
|
detail,
|
||||||
@@ -416,9 +427,10 @@ export function useKuaishouCloudClaim(token: () => string) {
|
|||||||
submitTicket,
|
submitTicket,
|
||||||
refreshRole,
|
refreshRole,
|
||||||
confirmRole,
|
confirmRole,
|
||||||
|
rebindRole,
|
||||||
confirmRedeem,
|
confirmRedeem,
|
||||||
openBindUrl,
|
openBindUrl,
|
||||||
// utility
|
// utility
|
||||||
formatAdminDateTime,
|
formatAdminDateTime,
|
||||||
};
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user