优化领取结果和CloudTentacles平台兑换购买记录
This commit is contained in:
@@ -305,6 +305,10 @@ export function mapClaimKuaishouCloudFulfillment(task: TaskRow, order: OrderRow)
|
|||||||
dispatch: {
|
dispatch: {
|
||||||
status: String(dispatch.status || 'pending').trim() || 'pending',
|
status: String(dispatch.status || 'pending').trim() || 'pending',
|
||||||
dispatchAt: dispatch.dispatchAt || null,
|
dispatchAt: dispatch.dispatchAt || null,
|
||||||
|
failedAt: dispatch.failedAt || null,
|
||||||
|
failedStage: String(dispatch.failedStage || '').trim(),
|
||||||
|
errorCode: String(dispatch.errorCode || '').trim(),
|
||||||
|
errorMessage: String(dispatch.errorMessage || '').trim(),
|
||||||
note: String(dispatch.note || '').trim(),
|
note: String(dispatch.note || '').trim(),
|
||||||
},
|
},
|
||||||
returnNumber: {
|
returnNumber: {
|
||||||
|
|||||||
@@ -549,8 +549,17 @@ export async function redeemKuaishouCloudClaim(token: unknown) {
|
|||||||
})
|
})
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const latestTask = await getTaskById(lockedTask.id)
|
const latestTask = await getTaskById(lockedTask.id)
|
||||||
if (latestTask && normalizeTaskStatus(latestTask.task_status) !== TASK_STATUS.REDEEMING) {
|
const latestStatus = latestTask ? normalizeTaskStatus(latestTask.task_status) : ''
|
||||||
return getKuaishouCloudClaimDetail(token)
|
if (latestTask && latestStatus !== TASK_STATUS.REDEEMING) {
|
||||||
|
if (
|
||||||
|
latestStatus === TASK_STATUS.DISPATCHED_PENDING_RETURN ||
|
||||||
|
latestStatus === TASK_STATUS.COMPLETED ||
|
||||||
|
latestStatus === TASK_STATUS.REDEEMED
|
||||||
|
) {
|
||||||
|
return getKuaishouCloudClaimDetail(token)
|
||||||
|
}
|
||||||
|
|
||||||
|
throw error
|
||||||
}
|
}
|
||||||
|
|
||||||
const message = error instanceof Error ? error.message : '兑换请求提交失败,请联系客服处理'
|
const message = error instanceof Error ? error.message : '兑换请求提交失败,请联系客服处理'
|
||||||
|
|||||||
@@ -128,15 +128,25 @@ export async function dispatchKuaishouCloudFulfillmentTask(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const { dispatchResults, stockResult } = await withCloudSkuDispatchLocks(
|
let dispatchResults: DispatchResultItem[];
|
||||||
resolveCloudSkuDispatchLockKeys(cloudContext.resolvedSourceKey, deliveryItems),
|
let stockResult: DispatchStockResult;
|
||||||
() => prepareStockAndDispatch({
|
try {
|
||||||
task,
|
({ dispatchResults, stockResult } = await withCloudSkuDispatchLocks(
|
||||||
flow: syncedFlow,
|
resolveCloudSkuDispatchLockKeys(cloudContext.resolvedSourceKey, deliveryItems),
|
||||||
cloudContext,
|
() => prepareStockAndDispatch({
|
||||||
deliveryItems,
|
task,
|
||||||
})
|
flow: syncedFlow,
|
||||||
);
|
cloudContext,
|
||||||
|
deliveryItems,
|
||||||
|
})
|
||||||
|
));
|
||||||
|
} catch (error) {
|
||||||
|
await markKuaishouCloudDispatchFailed(task, syncedTaskContext, syncedFlow, error, {
|
||||||
|
actor,
|
||||||
|
source: options.source || "system",
|
||||||
|
});
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
const firstDispatchResult: DispatchResultItem = dispatchResults[0] || {
|
const firstDispatchResult: DispatchResultItem = dispatchResults[0] || {
|
||||||
cloudSkuId: syncedFlow.binding.skuId,
|
cloudSkuId: syncedFlow.binding.skuId,
|
||||||
cloudSkuName: syncedFlow.binding.skuName,
|
cloudSkuName: syncedFlow.binding.skuName,
|
||||||
@@ -248,6 +258,72 @@ export async function dispatchKuaishouCloudFulfillmentTask(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function markKuaishouCloudDispatchFailed(
|
||||||
|
task: TaskRow,
|
||||||
|
taskContext: JsonObject,
|
||||||
|
flow: JsonObject,
|
||||||
|
error: unknown,
|
||||||
|
options: JsonObject = {}
|
||||||
|
) {
|
||||||
|
const now = nowIso();
|
||||||
|
const errorMessage = resolveErrorMessage(error);
|
||||||
|
const errorCode = resolveErrorCode(error) || "kuaishou_cloud_dispatch_failed";
|
||||||
|
const failureContext = resolveDispatchFailureContext(error);
|
||||||
|
const stockResult = isPlainObject(failureContext.stockResult)
|
||||||
|
? (failureContext.stockResult as Partial<DispatchStockResult>)
|
||||||
|
: null;
|
||||||
|
const nextContext = {
|
||||||
|
...taskContext,
|
||||||
|
kuaishouCloudFulfillment: {
|
||||||
|
...flow,
|
||||||
|
dispatch: {
|
||||||
|
...flow.dispatch,
|
||||||
|
status: "failed",
|
||||||
|
failedAt: now,
|
||||||
|
failedStage: String(failureContext.stage || "").trim(),
|
||||||
|
errorCode,
|
||||||
|
errorMessage,
|
||||||
|
note: buildDispatchFailedMessage(errorMessage),
|
||||||
|
},
|
||||||
|
purchase: {
|
||||||
|
...flow.purchase,
|
||||||
|
...(stockResult
|
||||||
|
? {
|
||||||
|
usedKnapsack: stockResult.usedKnapsack,
|
||||||
|
purchaseTriggered: stockResult.purchaseTriggered,
|
||||||
|
assetBefore: stockResult.assetBefore,
|
||||||
|
assetAfter: stockResult.assetAfter,
|
||||||
|
items: stockResult.items,
|
||||||
|
}
|
||||||
|
: {}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
await updateTask(task.id, {
|
||||||
|
task_status: TASK_STATUS.MANUAL_REVIEW,
|
||||||
|
user_action_status: "not_required",
|
||||||
|
last_error: errorMessage,
|
||||||
|
result_code: errorCode,
|
||||||
|
result_message: buildDispatchFailedMessage(errorMessage),
|
||||||
|
context_json: JSON.stringify(nextContext),
|
||||||
|
updated_at: now,
|
||||||
|
});
|
||||||
|
|
||||||
|
await createTaskEvent(
|
||||||
|
task.id,
|
||||||
|
"kuaishou_cloud_dispatch_failed",
|
||||||
|
{
|
||||||
|
source: String(options.source || "system").trim() || "system",
|
||||||
|
actor: options.actor,
|
||||||
|
errorCode,
|
||||||
|
errorMessage,
|
||||||
|
failureContext,
|
||||||
|
},
|
||||||
|
now
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
async function prepareStockAndDispatch({
|
async function prepareStockAndDispatch({
|
||||||
task,
|
task,
|
||||||
flow,
|
flow,
|
||||||
@@ -316,11 +392,62 @@ async function prepareStockAndDispatch({
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (const item of missingItems) {
|
for (const item of missingItems) {
|
||||||
await buyCloudtentaclesSku({
|
await createTaskEvent(
|
||||||
...cloudContext,
|
task.id,
|
||||||
id: item.cloudSkuId,
|
"cloudtentacles_sku_buy_started",
|
||||||
count: item.purchasedCount,
|
{
|
||||||
});
|
source: "kuaishou_cloud_dispatch",
|
||||||
|
cloudSkuId: item.cloudSkuId,
|
||||||
|
cloudSkuName: item.cloudSkuName,
|
||||||
|
count: item.purchasedCount,
|
||||||
|
assetBefore,
|
||||||
|
},
|
||||||
|
nowIso()
|
||||||
|
);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const buyResult = await buyCloudtentaclesSku({
|
||||||
|
...cloudContext,
|
||||||
|
id: item.cloudSkuId,
|
||||||
|
count: item.purchasedCount,
|
||||||
|
});
|
||||||
|
await createTaskEvent(
|
||||||
|
task.id,
|
||||||
|
"cloudtentacles_sku_buy_succeeded",
|
||||||
|
{
|
||||||
|
source: "kuaishou_cloud_dispatch",
|
||||||
|
cloudSkuId: item.cloudSkuId,
|
||||||
|
cloudSkuName: item.cloudSkuName,
|
||||||
|
count: item.purchasedCount,
|
||||||
|
responseMessage: buyResult.responseMessage,
|
||||||
|
},
|
||||||
|
nowIso()
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
await createTaskEvent(
|
||||||
|
task.id,
|
||||||
|
"cloudtentacles_sku_buy_failed",
|
||||||
|
{
|
||||||
|
source: "kuaishou_cloud_dispatch",
|
||||||
|
cloudSkuId: item.cloudSkuId,
|
||||||
|
cloudSkuName: item.cloudSkuName,
|
||||||
|
count: item.purchasedCount,
|
||||||
|
errorCode: resolveErrorCode(error),
|
||||||
|
errorMessage: resolveErrorMessage(error),
|
||||||
|
},
|
||||||
|
nowIso()
|
||||||
|
);
|
||||||
|
throw enrichCloudtentaclesDispatchError(error, {
|
||||||
|
stage: "purchase",
|
||||||
|
stockResult: buildCurrentStockResult({
|
||||||
|
stockItems,
|
||||||
|
purchaseTriggered,
|
||||||
|
assetBefore,
|
||||||
|
assetAfter,
|
||||||
|
}),
|
||||||
|
item,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
purchaseTriggered = true;
|
purchaseTriggered = true;
|
||||||
@@ -331,12 +458,78 @@ async function prepareStockAndDispatch({
|
|||||||
const dispatchResults: DispatchResultItem[] = [];
|
const dispatchResults: DispatchResultItem[] = [];
|
||||||
for (const item of deliveryItems) {
|
for (const item of deliveryItems) {
|
||||||
for (let index = 0; index < item.quantity; index += 1) {
|
for (let index = 0; index < item.quantity; index += 1) {
|
||||||
const dispatchResult = await useCloudtentaclesSku({
|
await createTaskEvent(
|
||||||
...cloudContext,
|
task.id,
|
||||||
id: item.cloudSkuId,
|
"cloudtentacles_sku_use_started",
|
||||||
virtualNumberId: flow.binding.vnId,
|
{
|
||||||
phone: flow.binding.vnPhone,
|
source: "kuaishou_cloud_dispatch",
|
||||||
});
|
cloudSkuId: item.cloudSkuId,
|
||||||
|
cloudSkuName: item.cloudSkuName,
|
||||||
|
unitIndex: index + 1,
|
||||||
|
quantity: item.quantity,
|
||||||
|
vnId: flow.binding.vnId,
|
||||||
|
vnPhoneMasked: maskPhone(flow.binding.vnPhone),
|
||||||
|
},
|
||||||
|
nowIso()
|
||||||
|
);
|
||||||
|
|
||||||
|
let dispatchResult: Awaited<ReturnType<typeof useCloudtentaclesSku>>;
|
||||||
|
try {
|
||||||
|
dispatchResult = await useCloudtentaclesSku({
|
||||||
|
...cloudContext,
|
||||||
|
id: item.cloudSkuId,
|
||||||
|
virtualNumberId: flow.binding.vnId,
|
||||||
|
phone: flow.binding.vnPhone,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
await createTaskEvent(
|
||||||
|
task.id,
|
||||||
|
"cloudtentacles_sku_use_failed",
|
||||||
|
{
|
||||||
|
source: "kuaishou_cloud_dispatch",
|
||||||
|
cloudSkuId: item.cloudSkuId,
|
||||||
|
cloudSkuName: item.cloudSkuName,
|
||||||
|
unitIndex: index + 1,
|
||||||
|
quantity: item.quantity,
|
||||||
|
vnId: flow.binding.vnId,
|
||||||
|
vnPhoneMasked: maskPhone(flow.binding.vnPhone),
|
||||||
|
errorCode: resolveErrorCode(error),
|
||||||
|
errorMessage: resolveErrorMessage(error),
|
||||||
|
},
|
||||||
|
nowIso()
|
||||||
|
);
|
||||||
|
throw enrichCloudtentaclesDispatchError(error, {
|
||||||
|
stage: "dispatch",
|
||||||
|
stockResult: buildCurrentStockResult({
|
||||||
|
stockItems,
|
||||||
|
purchaseTriggered,
|
||||||
|
assetBefore,
|
||||||
|
assetAfter,
|
||||||
|
}),
|
||||||
|
item,
|
||||||
|
unitIndex: index + 1,
|
||||||
|
vnId: flow.binding.vnId,
|
||||||
|
vnPhoneMasked: maskPhone(flow.binding.vnPhone),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await createTaskEvent(
|
||||||
|
task.id,
|
||||||
|
"cloudtentacles_sku_use_succeeded",
|
||||||
|
{
|
||||||
|
source: "kuaishou_cloud_dispatch",
|
||||||
|
cloudSkuId: item.cloudSkuId,
|
||||||
|
cloudSkuName: item.cloudSkuName,
|
||||||
|
unitIndex: index + 1,
|
||||||
|
quantity: item.quantity,
|
||||||
|
vnId: flow.binding.vnId,
|
||||||
|
vnPhoneMasked: maskPhone(flow.binding.vnPhone),
|
||||||
|
sendType: Number(dispatchResult.sendType || 0) || 0,
|
||||||
|
note: String(dispatchResult.note || "").trim(),
|
||||||
|
responseMessage: String(dispatchResult.responseMessage || "").trim(),
|
||||||
|
},
|
||||||
|
nowIso()
|
||||||
|
);
|
||||||
dispatchResults.push({
|
dispatchResults.push({
|
||||||
cloudSkuId: item.cloudSkuId,
|
cloudSkuId: item.cloudSkuId,
|
||||||
cloudSkuName: item.cloudSkuName,
|
cloudSkuName: item.cloudSkuName,
|
||||||
@@ -361,6 +554,82 @@ async function prepareStockAndDispatch({
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function buildCurrentStockResult({
|
||||||
|
stockItems,
|
||||||
|
purchaseTriggered,
|
||||||
|
assetBefore,
|
||||||
|
assetAfter,
|
||||||
|
}: {
|
||||||
|
stockItems: DispatchStockItem[];
|
||||||
|
purchaseTriggered: boolean;
|
||||||
|
assetBefore: number;
|
||||||
|
assetAfter: number;
|
||||||
|
}): DispatchStockResult {
|
||||||
|
return {
|
||||||
|
usedKnapsack: stockItems.every((item) => item.purchasedCount <= 0),
|
||||||
|
purchaseTriggered,
|
||||||
|
assetBefore,
|
||||||
|
assetAfter,
|
||||||
|
items: stockItems,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function enrichCloudtentaclesDispatchError(error: unknown, context: JsonObject) {
|
||||||
|
const currentError = error instanceof Error
|
||||||
|
? (error as Error & { context?: unknown })
|
||||||
|
: createHttpError(resolveErrorMessage(error), {
|
||||||
|
statusCode: 500,
|
||||||
|
errorCode: "kuaishou_cloud_dispatch_failed",
|
||||||
|
}) as Error & { context?: unknown };
|
||||||
|
const currentContext = isPlainObject(currentError.context) ? currentError.context : {};
|
||||||
|
currentError.context = {
|
||||||
|
...currentContext,
|
||||||
|
dispatchFailure: {
|
||||||
|
...(isPlainObject((currentContext as JsonObject).dispatchFailure)
|
||||||
|
? (currentContext as JsonObject).dispatchFailure
|
||||||
|
: {}),
|
||||||
|
...context,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
return currentError;
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveDispatchFailureContext(error: unknown): JsonObject {
|
||||||
|
if (!error || typeof error !== "object") {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
const context = (error as { context?: unknown }).context;
|
||||||
|
if (!isPlainObject(context)) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
const dispatchFailure = context.dispatchFailure;
|
||||||
|
return isPlainObject(dispatchFailure) ? dispatchFailure : {};
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveErrorCode(error: unknown) {
|
||||||
|
if (!error || typeof error !== "object") {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
return String((error as { errorCode?: unknown }).errorCode || "").trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveErrorMessage(error: unknown) {
|
||||||
|
return error instanceof Error ? error.message : String(error || "CloudTentacles 发货失败");
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildDispatchFailedMessage(message: unknown) {
|
||||||
|
const reason = String(message || "").trim() || "未知错误";
|
||||||
|
return `CloudTentacles 发货失败:${reason}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isPlainObject(value: unknown): value is JsonObject {
|
||||||
|
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
||||||
|
}
|
||||||
|
|
||||||
export function buildDispatchStockItems(
|
export function buildDispatchStockItems(
|
||||||
deliveryItems: DispatchDeliveryItem[],
|
deliveryItems: DispatchDeliveryItem[],
|
||||||
{ skuItems = [], knapsackItems = [] }: { skuItems?: JsonObject[]; knapsackItems?: JsonObject[] } = {}
|
{ skuItems = [], knapsackItems = [] }: { skuItems?: JsonObject[]; knapsackItems?: JsonObject[] } = {}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { createHttpError } from '../../../utils/http.js'
|
import { createHttpError, type HttpErrorLike } from '../../../utils/http.js'
|
||||||
import { cloudtentaclesRequest } from './http-client.js'
|
import { cloudtentaclesRequest } from './http-client.js'
|
||||||
import { resolveCloudtentaclesConfig } from './helpers.js'
|
import { resolveCloudtentaclesConfig } from './helpers.js'
|
||||||
|
|
||||||
@@ -86,6 +86,13 @@ export async function buyCloudtentaclesSku(payload: JsonObject = {}) {
|
|||||||
},
|
},
|
||||||
businessErrorStatusCode: 401,
|
businessErrorStatusCode: 401,
|
||||||
businessErrorCode: 'cloudtentacles_sku_buy_failed',
|
businessErrorCode: 'cloudtentacles_sku_buy_failed',
|
||||||
|
context: {
|
||||||
|
operation: 'sku_buy',
|
||||||
|
skuId,
|
||||||
|
count,
|
||||||
|
sourceKey: payload.sourceKey,
|
||||||
|
accountLabel: payload.accountLabel,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -113,17 +120,14 @@ export async function useCloudtentaclesSku(payload: JsonObject = {}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const config = resolveCloudtentaclesConfig(payload)
|
const config = resolveCloudtentaclesConfig(payload)
|
||||||
const result = await cloudtentaclesRequest(config.skuUsePath, {
|
const result = await useCloudtentaclesSkuRequest({
|
||||||
...config,
|
...config,
|
||||||
method: 'POST',
|
|
||||||
token,
|
token,
|
||||||
body: {
|
skuId,
|
||||||
id: skuId,
|
virtualNumberId,
|
||||||
virtual_number_id: virtualNumberId,
|
phone,
|
||||||
phone,
|
sourceKey: payload.sourceKey,
|
||||||
},
|
accountLabel: payload.accountLabel,
|
||||||
businessErrorStatusCode: 401,
|
|
||||||
businessErrorCode: 'cloudtentacles_sku_use_failed',
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const responseData = isPlainObject(result.payload?.data) ? result.payload.data : {}
|
const responseData = isPlainObject(result.payload?.data) ? result.payload.data : {}
|
||||||
@@ -140,6 +144,53 @@ export async function useCloudtentaclesSku(payload: JsonObject = {}) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function useCloudtentaclesSkuRequest(payload: JsonObject) {
|
||||||
|
try {
|
||||||
|
return await cloudtentaclesRequest(payload.skuUsePath, {
|
||||||
|
...payload,
|
||||||
|
method: 'POST',
|
||||||
|
token: payload.token,
|
||||||
|
body: {
|
||||||
|
id: payload.skuId,
|
||||||
|
virtual_number_id: payload.virtualNumberId,
|
||||||
|
phone: payload.phone,
|
||||||
|
},
|
||||||
|
businessErrorStatusCode: 401,
|
||||||
|
businessErrorCode: 'cloudtentacles_sku_use_failed',
|
||||||
|
context: {
|
||||||
|
operation: 'sku_use',
|
||||||
|
skuId: payload.skuId,
|
||||||
|
virtualNumberId: payload.virtualNumberId,
|
||||||
|
phoneMasked: maskPhone(payload.phone),
|
||||||
|
sourceKey: payload.sourceKey,
|
||||||
|
accountLabel: payload.accountLabel,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
if (isCloudtentaclesSkuUseLimitMessage(error instanceof Error ? error.message : '')) {
|
||||||
|
const currentError = error as Error & HttpErrorLike
|
||||||
|
currentError.statusCode = 409
|
||||||
|
currentError.errorCode = 'cloudtentacles_sku_use_limit_reached'
|
||||||
|
}
|
||||||
|
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function isCloudtentaclesSkuUseLimitMessage(message: unknown) {
|
||||||
|
const normalized = String(message || '').trim()
|
||||||
|
return normalized.includes('最大领取次数') || normalized.includes('领取次数')
|
||||||
|
}
|
||||||
|
|
||||||
|
function maskPhone(value: unknown) {
|
||||||
|
const text = String(value || '').trim()
|
||||||
|
if (text.length <= 4) {
|
||||||
|
return text ? '****' : ''
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${text.slice(0, 3)}****${text.slice(-4)}`
|
||||||
|
}
|
||||||
|
|
||||||
function mapCategoryItem(item: unknown) {
|
function mapCategoryItem(item: unknown) {
|
||||||
const source = isPlainObject(item) ? item : {}
|
const source = isPlainObject(item) ? item : {}
|
||||||
|
|
||||||
|
|||||||
@@ -67,11 +67,30 @@ export async function cloudtentaclesRequest(pathname: unknown, options: JsonObje
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
logWarn('[cloudtentacles/http]', '请求 HTTP 失败', {
|
||||||
|
method,
|
||||||
|
pathname: normalizedPathname,
|
||||||
|
status: response.status,
|
||||||
|
attempt: attempt + 1,
|
||||||
|
sourceKey: options.sourceKey,
|
||||||
|
accountLabel: options.accountLabel,
|
||||||
|
context: options.context,
|
||||||
|
upstreamPayload: payload,
|
||||||
|
rawText: truncateText(rawText),
|
||||||
|
})
|
||||||
|
|
||||||
throw createHttpError(`cloudtentacles 请求失败,HTTP ${response.status}`, {
|
throw createHttpError(`cloudtentacles 请求失败,HTTP ${response.status}`, {
|
||||||
statusCode: isCloudtentaclesHttpRateLimited(response.status) ? 429 : 502,
|
statusCode: isCloudtentaclesHttpRateLimited(response.status) ? 429 : 502,
|
||||||
errorCode: isCloudtentaclesHttpRateLimited(response.status)
|
errorCode: isCloudtentaclesHttpRateLimited(response.status)
|
||||||
? 'cloudtentacles_rate_limited'
|
? 'cloudtentacles_rate_limited'
|
||||||
: 'cloudtentacles_http_error',
|
: 'cloudtentacles_http_error',
|
||||||
|
context: {
|
||||||
|
method,
|
||||||
|
pathname: normalizedPathname,
|
||||||
|
upstreamStatus: response.status,
|
||||||
|
upstreamPayload: payload,
|
||||||
|
requestContext: options.context,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -106,9 +125,32 @@ export async function cloudtentaclesRequest(pathname: unknown, options: JsonObje
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
logWarn('[cloudtentacles/http]', '请求业务失败', {
|
||||||
|
method,
|
||||||
|
pathname: normalizedPathname,
|
||||||
|
status: response.status,
|
||||||
|
attempt: attempt + 1,
|
||||||
|
errorCode,
|
||||||
|
upstreamCode: payload?.code,
|
||||||
|
upstreamMessage: message,
|
||||||
|
sourceKey: options.sourceKey,
|
||||||
|
accountLabel: options.accountLabel,
|
||||||
|
context: options.context,
|
||||||
|
upstreamPayload: payload,
|
||||||
|
})
|
||||||
|
|
||||||
throw createHttpError(message, {
|
throw createHttpError(message, {
|
||||||
statusCode,
|
statusCode,
|
||||||
errorCode,
|
errorCode,
|
||||||
|
context: {
|
||||||
|
method,
|
||||||
|
pathname: normalizedPathname,
|
||||||
|
upstreamStatus: response.status,
|
||||||
|
upstreamCode: payload?.code,
|
||||||
|
upstreamMessage: message,
|
||||||
|
upstreamPayload: payload,
|
||||||
|
requestContext: options.context,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -157,6 +199,15 @@ function isCloudtentaclesHttpRateLimited(status: unknown) {
|
|||||||
return Number(status) === 429
|
return Number(status) === 429
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function truncateText(value: unknown, maxLength = 1000) {
|
||||||
|
const text = String(value || '')
|
||||||
|
if (text.length <= maxLength) {
|
||||||
|
return text
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${text.slice(0, maxLength)}...`
|
||||||
|
}
|
||||||
|
|
||||||
async function executeCloudtentaclesRequest({
|
async function executeCloudtentaclesRequest({
|
||||||
url,
|
url,
|
||||||
method,
|
method,
|
||||||
|
|||||||
@@ -106,6 +106,10 @@ export interface ClaimKuaishouCloudFlowInfo {
|
|||||||
dispatch: {
|
dispatch: {
|
||||||
status: string
|
status: string
|
||||||
dispatchAt: string | null
|
dispatchAt: string | null
|
||||||
|
failedAt: string | null
|
||||||
|
failedStage: string
|
||||||
|
errorCode: string
|
||||||
|
errorMessage: string
|
||||||
note: string
|
note: string
|
||||||
}
|
}
|
||||||
returnNumber: {
|
returnNumber: {
|
||||||
|
|||||||
@@ -78,6 +78,7 @@ const claim = useKuaishouCloudClaim(() => props.token)
|
|||||||
v-else
|
v-else
|
||||||
:result-title="claim.resultTitle.value"
|
:result-title="claim.resultTitle.value"
|
||||||
:result-description="claim.resultDescription.value"
|
:result-description="claim.resultDescription.value"
|
||||||
|
:result-variant="claim.resultVariant.value"
|
||||||
:flow="claim.flow.value"
|
:flow="claim.flow.value"
|
||||||
:order="claim.order.value"
|
:order="claim.order.value"
|
||||||
:product="claim.product.value"
|
:product="claim.product.value"
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { CircleCheck } from '@element-plus/icons-vue'
|
import { CircleCheck, WarningFilled } from '@element-plus/icons-vue'
|
||||||
|
|
||||||
import ClaimProductItems from './ClaimProductItems.vue'
|
import ClaimProductItems from './ClaimProductItems.vue'
|
||||||
|
|
||||||
@@ -8,6 +8,7 @@ import type { ClaimKuaishouCloudFlowInfo, ClaimOrderInfo, ClaimProductInfo } fro
|
|||||||
defineProps<{
|
defineProps<{
|
||||||
resultTitle: string
|
resultTitle: string
|
||||||
resultDescription: string
|
resultDescription: string
|
||||||
|
resultVariant: 'success' | 'warning' | 'info'
|
||||||
flow: ClaimKuaishouCloudFlowInfo
|
flow: ClaimKuaishouCloudFlowInfo
|
||||||
order: ClaimOrderInfo | null
|
order: ClaimOrderInfo | null
|
||||||
product: ClaimProductInfo | null
|
product: ClaimProductInfo | null
|
||||||
@@ -20,7 +21,10 @@ defineProps<{
|
|||||||
<template>
|
<template>
|
||||||
<section class="content-card">
|
<section class="content-card">
|
||||||
<div class="result-header">
|
<div class="result-header">
|
||||||
<el-icon class="large-icon success"><CircleCheck /></el-icon>
|
<el-icon :class="['large-icon', resultVariant]">
|
||||||
|
<WarningFilled v-if="resultVariant === 'warning'" />
|
||||||
|
<CircleCheck v-else />
|
||||||
|
</el-icon>
|
||||||
<div>
|
<div>
|
||||||
<h2>{{ resultTitle }}</h2>
|
<h2>{{ resultTitle }}</h2>
|
||||||
<p class="muted">{{ resultDescription }}</p>
|
<p class="muted">{{ resultDescription }}</p>
|
||||||
@@ -56,9 +60,13 @@ defineProps<{
|
|||||||
|
|
||||||
<ClaimProductItems :product="product" />
|
<ClaimProductItems :product="product" />
|
||||||
|
|
||||||
<div class="status-banner success">
|
<div :class="['status-banner', resultVariant]">
|
||||||
<el-icon><CircleCheck /></el-icon>
|
<el-icon>
|
||||||
<span>客户侧操作已经完成,后续履约结果请以后台任务界面为准。</span>
|
<WarningFilled v-if="resultVariant === 'warning'" />
|
||||||
|
<CircleCheck v-else />
|
||||||
|
</el-icon>
|
||||||
|
<span v-if="resultVariant === 'warning'">当前兑换未完成,客服会根据后台任务记录继续处理。</span>
|
||||||
|
<span v-else>客户侧操作已经完成,后续履约结果请以后台任务界面为准。</span>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
@@ -103,6 +111,14 @@ defineProps<{
|
|||||||
color: #16a34a;
|
color: #16a34a;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.large-icon.warning {
|
||||||
|
color: #d97706;
|
||||||
|
}
|
||||||
|
|
||||||
|
.large-icon.info {
|
||||||
|
color: #2563eb;
|
||||||
|
}
|
||||||
|
|
||||||
.info-grid {
|
.info-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
@@ -143,6 +159,16 @@ defineProps<{
|
|||||||
color: #166534;
|
color: #166534;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.status-banner.warning {
|
||||||
|
background: #fffbeb;
|
||||||
|
color: #92400e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-banner.info {
|
||||||
|
background: #eff6ff;
|
||||||
|
color: #1d4ed8;
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: 720px) {
|
@media (max-width: 720px) {
|
||||||
.content-card {
|
.content-card {
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
|
|||||||
@@ -4,10 +4,12 @@ import { ElMessageBox } from 'element-plus'
|
|||||||
|
|
||||||
import { showError, showSuccess } from '@/lib/feedback'
|
import { showError, showSuccess } from '@/lib/feedback'
|
||||||
import {
|
import {
|
||||||
|
TASK_STATUS,
|
||||||
hasKuaishouCloudRedeemResultStatus,
|
hasKuaishouCloudRedeemResultStatus,
|
||||||
isClaimInactiveTaskStatus,
|
isClaimInactiveTaskStatus,
|
||||||
isKuaishouCloudCompletedStatus,
|
isKuaishouCloudCompletedStatus,
|
||||||
isKuaishouCloudRoleConfirmedStatus,
|
isKuaishouCloudRoleConfirmedStatus,
|
||||||
|
normalizeTaskStatus,
|
||||||
} from '@/domain/task-status'
|
} from '@/domain/task-status'
|
||||||
import { formatAdminDateTime } from '@/utils/admin-time'
|
import { formatAdminDateTime } from '@/utils/admin-time'
|
||||||
import {
|
import {
|
||||||
@@ -78,12 +80,16 @@ export function useKuaishouCloudClaim(token: () => string) {
|
|||||||
const isDispatched = computed(
|
const isDispatched = computed(
|
||||||
() => String(flow.value?.dispatch.status || '').trim() === 'success',
|
() => String(flow.value?.dispatch.status || '').trim() === 'success',
|
||||||
)
|
)
|
||||||
|
const isRedeemFailed = computed(() => {
|
||||||
|
const status = normalizeTaskStatus(task.value?.status)
|
||||||
|
return (
|
||||||
|
status === TASK_STATUS.MANUAL_REVIEW ||
|
||||||
|
status === TASK_STATUS.FAILED ||
|
||||||
|
String(flow.value?.dispatch.status || '').trim() === 'failed'
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
const isCompleted = computed(
|
const isCompleted = computed(() => isKuaishouCloudCompletedStatus(task.value?.status))
|
||||||
() =>
|
|
||||||
isKuaishouCloudCompletedStatus(task.value?.status) ||
|
|
||||||
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)
|
||||||
@@ -120,6 +126,9 @@ export function useKuaishouCloudClaim(token: () => string) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const resultTitle = computed(() => {
|
const resultTitle = computed(() => {
|
||||||
|
if (isRedeemFailed.value) {
|
||||||
|
return '兑换遇到问题'
|
||||||
|
}
|
||||||
if (isCompleted.value) {
|
if (isCompleted.value) {
|
||||||
return '兑换成功'
|
return '兑换成功'
|
||||||
}
|
}
|
||||||
@@ -130,12 +139,31 @@ export function useKuaishouCloudClaim(token: () => string) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const resultDescription = computed(() => {
|
const resultDescription = computed(() => {
|
||||||
|
if (isRedeemFailed.value) {
|
||||||
|
const message = String(
|
||||||
|
task.value?.lastError ||
|
||||||
|
flow.value?.dispatch.errorMessage ||
|
||||||
|
detail.value?.result?.resultMessage ||
|
||||||
|
'',
|
||||||
|
).trim()
|
||||||
|
return message || '当前兑换流程需要客服处理,后续结果请以后台任务界面为准。'
|
||||||
|
}
|
||||||
if (isCompleted.value) {
|
if (isCompleted.value) {
|
||||||
return '当前兑换流程已经完成。发货、退号和核销结果会保存在后台任务界面。'
|
return '当前兑换流程已经完成。发货、退号和核销结果会保存在后台任务界面。'
|
||||||
}
|
}
|
||||||
return '你的兑换请求已经提交。发货、退号和核销结果会由系统保存在后台任务界面,无需在此页面等待。'
|
return '你的兑换请求已经提交。发货、退号和核销结果会由系统保存在后台任务界面,无需在此页面等待。'
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const resultVariant = computed(() => {
|
||||||
|
if (isRedeemFailed.value) {
|
||||||
|
return 'warning'
|
||||||
|
}
|
||||||
|
if (isCompleted.value || isDispatched.value) {
|
||||||
|
return 'success'
|
||||||
|
}
|
||||||
|
return 'info'
|
||||||
|
})
|
||||||
|
|
||||||
// ── actions ─────────────────────────────────────────────
|
// ── actions ─────────────────────────────────────────────
|
||||||
|
|
||||||
async function generateQRCode(url: string) {
|
async function generateQRCode(url: string) {
|
||||||
@@ -415,6 +443,7 @@ export function useKuaishouCloudClaim(token: () => string) {
|
|||||||
isRoleReady,
|
isRoleReady,
|
||||||
isRoleConfirmed,
|
isRoleConfirmed,
|
||||||
isDispatched,
|
isDispatched,
|
||||||
|
isRedeemFailed,
|
||||||
isCompleted,
|
isCompleted,
|
||||||
hasRedeemResult,
|
hasRedeemResult,
|
||||||
canSubmitTicket,
|
canSubmitTicket,
|
||||||
@@ -422,6 +451,7 @@ export function useKuaishouCloudClaim(token: () => string) {
|
|||||||
progressText,
|
progressText,
|
||||||
resultTitle,
|
resultTitle,
|
||||||
resultDescription,
|
resultDescription,
|
||||||
|
resultVariant,
|
||||||
// actions
|
// actions
|
||||||
loadDetail,
|
loadDetail,
|
||||||
submitTicket,
|
submitTicket,
|
||||||
|
|||||||
Reference in New Issue
Block a user