履约配置优化
This commit is contained in:
@@ -1,11 +1,13 @@
|
|||||||
{
|
{
|
||||||
"syncFromCreatedAt": "2026-05-02T10:53:28.550Z",
|
"syncFromCreatedAt": "2026-05-02T10:53:28.550Z",
|
||||||
"lastRunStartedAt": "2026-05-03T12:57:26.689Z",
|
"lastRunStartedAt": "2026-05-03T15:01:47.533Z",
|
||||||
"lastRunFinishedAt": "2026-05-03T12:57:27.309Z",
|
"lastRunFinishedAt": "2026-05-03T15:01:48.438Z",
|
||||||
"lastRunStatus": "success",
|
"lastRunStatus": "success",
|
||||||
"lastErrorMessage": "",
|
"lastErrorMessage": "",
|
||||||
"fetchedCount": 40,
|
"fetchedCount": 40,
|
||||||
"syncedCount": 1,
|
"syncedCount": 0,
|
||||||
"ignoredCount": 39,
|
"ignoredCount": 40,
|
||||||
"lastOrderCreatedAt": "2026-05-03T12:57:20.000Z"
|
"lastOrderCreatedAt": "2026-05-03T15:01:23.000Z",
|
||||||
|
"watchMode": "normal",
|
||||||
|
"watchOrders": []
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
dispatchAdminTaskKuaishouCloudFulfillment,
|
dispatchAdminTaskKuaishouCloudFulfillment,
|
||||||
markAdminTaskManualReview,
|
markAdminTaskManualReview,
|
||||||
prepareAdminTaskKuaishouCloudFulfillment,
|
prepareAdminTaskKuaishouCloudFulfillment,
|
||||||
|
refreshAdminTaskKuaishouCloudRoleInfo,
|
||||||
regenerateAdminTaskClaimLink,
|
regenerateAdminTaskClaimLink,
|
||||||
redeemAdminTaskAssisted,
|
redeemAdminTaskAssisted,
|
||||||
releaseAdminTaskInventoryBinding,
|
releaseAdminTaskInventoryBinding,
|
||||||
@@ -299,6 +300,31 @@ router.post('/tasks/:taskId/kuaishou-cloud/prepare', requireAdminRoles(['admin',
|
|||||||
},
|
},
|
||||||
))
|
))
|
||||||
|
|
||||||
|
router.post('/tasks/:taskId/kuaishou-cloud/refresh-role-info', requireAdminRoles(['admin', 'operator', 'support']), createJsonHandler(
|
||||||
|
(req) => refreshAdminTaskKuaishouCloudRoleInfo(
|
||||||
|
(/** @type {AdminTaskRouteParams} */ (req.params)).taskId,
|
||||||
|
/** @type {AdminRouteAdminSession | null} */ (req.adminSession),
|
||||||
|
),
|
||||||
|
{
|
||||||
|
successMessage: '角色信息已刷新',
|
||||||
|
errorMessage: '刷新角色信息失败',
|
||||||
|
scope: '[admin/tasks/:taskId/kuaishou-cloud/refresh-role-info]',
|
||||||
|
audit: (req, data) => {
|
||||||
|
const result = /** @type {AdminTaskActionResponse} */ (data)
|
||||||
|
return {
|
||||||
|
action: 'task_kuaishou_cloud_role_info_refreshed',
|
||||||
|
targetType: 'task',
|
||||||
|
targetId: String((/** @type {AdminTaskRouteParams} */ (req.params)).taskId),
|
||||||
|
data: {
|
||||||
|
taskId: result.task.taskId,
|
||||||
|
taskNo: result.task.taskNo,
|
||||||
|
status: result.task.status,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
))
|
||||||
|
|
||||||
router.post('/tasks/:taskId/kuaishou-cloud/dispatch', requireAdminRoles(['admin', 'operator']), createJsonHandler(
|
router.post('/tasks/:taskId/kuaishou-cloud/dispatch', requireAdminRoles(['admin', 'operator']), createJsonHandler(
|
||||||
(req) => dispatchAdminTaskKuaishouCloudFulfillment(
|
(req) => dispatchAdminTaskKuaishouCloudFulfillment(
|
||||||
(/** @type {AdminTaskRouteParams} */ (req.params)).taskId,
|
(/** @type {AdminTaskRouteParams} */ (req.params)).taskId,
|
||||||
|
|||||||
@@ -281,6 +281,16 @@ export function getAdminKhhaoSourceConfig() {
|
|||||||
fetchedCount: Number(syncState.fetchedCount || 0),
|
fetchedCount: Number(syncState.fetchedCount || 0),
|
||||||
syncedCount: Number(syncState.syncedCount || 0),
|
syncedCount: Number(syncState.syncedCount || 0),
|
||||||
ignoredCount: Number(syncState.ignoredCount || 0),
|
ignoredCount: Number(syncState.ignoredCount || 0),
|
||||||
|
watchMode: String(syncState.watchMode || 'normal').trim() || 'normal',
|
||||||
|
watchOrders: Array.isArray(syncState.watchOrders) ? syncState.watchOrders.map((item) => ({
|
||||||
|
platformOrderId: String(item?.platformOrderId || '').trim(),
|
||||||
|
shopId: String(item?.shopId || '').trim(),
|
||||||
|
shopName: String(item?.shopName || '').trim(),
|
||||||
|
itemTitle: String(item?.itemTitle || '').trim(),
|
||||||
|
payStatus: String(item?.payStatus || '').trim(),
|
||||||
|
detectedAt: String(item?.detectedAt || '').trim(),
|
||||||
|
lastSeenAt: String(item?.lastSeenAt || '').trim(),
|
||||||
|
})) : [],
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -358,6 +368,16 @@ export function updateAdminKhhaoSourceConfig(payload = /** @type {AdminKhhaoSour
|
|||||||
fetchedCount: Number(syncState.fetchedCount || 0),
|
fetchedCount: Number(syncState.fetchedCount || 0),
|
||||||
syncedCount: Number(syncState.syncedCount || 0),
|
syncedCount: Number(syncState.syncedCount || 0),
|
||||||
ignoredCount: Number(syncState.ignoredCount || 0),
|
ignoredCount: Number(syncState.ignoredCount || 0),
|
||||||
|
watchMode: String(syncState.watchMode || 'normal').trim() || 'normal',
|
||||||
|
watchOrders: Array.isArray(syncState.watchOrders) ? syncState.watchOrders.map((item) => ({
|
||||||
|
platformOrderId: String(item?.platformOrderId || '').trim(),
|
||||||
|
shopId: String(item?.shopId || '').trim(),
|
||||||
|
shopName: String(item?.shopName || '').trim(),
|
||||||
|
itemTitle: String(item?.itemTitle || '').trim(),
|
||||||
|
payStatus: String(item?.payStatus || '').trim(),
|
||||||
|
detectedAt: String(item?.detectedAt || '').trim(),
|
||||||
|
lastSeenAt: String(item?.lastSeenAt || '').trim(),
|
||||||
|
})) : [],
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -814,6 +834,7 @@ export function getAdminKuaishouCloudFulfillmentConfig() {
|
|||||||
autoReturnNumberAfterDispatch: item.autoReturnNumberAfterDispatch === true,
|
autoReturnNumberAfterDispatch: item.autoReturnNumberAfterDispatch === true,
|
||||||
autoConsumeAfterDispatch: item.autoConsumeAfterDispatch === true,
|
autoConsumeAfterDispatch: item.autoConsumeAfterDispatch === true,
|
||||||
kuaishouConsumeShopId: String(item.kuaishouConsumeShopId || '').trim(),
|
kuaishouConsumeShopId: String(item.kuaishouConsumeShopId || '').trim(),
|
||||||
|
kuaishouConsumeShopName: String(item.kuaishouConsumeShopName || '').trim(),
|
||||||
notes: String(item.notes || '').trim(),
|
notes: String(item.notes || '').trim(),
|
||||||
})),
|
})),
|
||||||
},
|
},
|
||||||
@@ -857,6 +878,7 @@ export async function updateAdminKuaishouCloudFulfillmentConfig(
|
|||||||
autoReturnNumberAfterDispatch: item.autoReturnNumberAfterDispatch === true,
|
autoReturnNumberAfterDispatch: item.autoReturnNumberAfterDispatch === true,
|
||||||
autoConsumeAfterDispatch: item.autoConsumeAfterDispatch === true,
|
autoConsumeAfterDispatch: item.autoConsumeAfterDispatch === true,
|
||||||
kuaishouConsumeShopId: String(item.kuaishouConsumeShopId || '').trim(),
|
kuaishouConsumeShopId: String(item.kuaishouConsumeShopId || '').trim(),
|
||||||
|
kuaishouConsumeShopName: String(item.kuaishouConsumeShopName || '').trim(),
|
||||||
notes: String(item.notes || '').trim(),
|
notes: String(item.notes || '').trim(),
|
||||||
})),
|
})),
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -297,6 +297,9 @@ export async function getAdminTaskDetail(taskId, session = null) {
|
|||||||
&& String(kuaishouCloudFulfillment?.dispatch.status || 'pending').trim() === 'pending'
|
&& String(kuaishouCloudFulfillment?.dispatch.status || 'pending').trim() === 'pending'
|
||||||
&& String(kuaishouCloudFulfillment?.returnNumber.status || 'pending').trim() === 'pending'
|
&& String(kuaishouCloudFulfillment?.returnNumber.status || 'pending').trim() === 'pending'
|
||||||
&& ['pending_binding_prepare', 'manual_review', 'failed'].includes(String(task.task_status || '').trim()),
|
&& ['pending_binding_prepare', 'manual_review', 'failed'].includes(String(task.task_status || '').trim()),
|
||||||
|
canRefreshKuaishouCloudRoleInfo: viewerContext.canOperateAssistedTask
|
||||||
|
&& String(task.executor_key || '').trim() === 'kuaishou_ct_assisted'
|
||||||
|
&& Number(kuaishouCloudFulfillment?.binding.vnId || 0) > 0,
|
||||||
canDispatchKuaishouCloudFulfillment: viewerContext.canManageTaskLifecycle
|
canDispatchKuaishouCloudFulfillment: viewerContext.canManageTaskLifecycle
|
||||||
&& String(task.executor_key || '').trim() === 'kuaishou_ct_assisted'
|
&& String(task.executor_key || '').trim() === 'kuaishou_ct_assisted'
|
||||||
&& ['waiting_binding'].includes(String(task.task_status || '').trim()),
|
&& ['waiting_binding'].includes(String(task.task_status || '').trim()),
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ export function mapKuaishouCloudFulfillmentContext(value) {
|
|||||||
|
|
||||||
const ticket = value.ticket && typeof value.ticket === 'object' ? value.ticket : {}
|
const ticket = value.ticket && typeof value.ticket === 'object' ? value.ticket : {}
|
||||||
const binding = value.binding && typeof value.binding === 'object' ? value.binding : {}
|
const binding = value.binding && typeof value.binding === 'object' ? value.binding : {}
|
||||||
|
const role = value.role && typeof value.role === 'object' ? value.role : {}
|
||||||
const purchase = value.purchase && typeof value.purchase === 'object' ? value.purchase : {}
|
const purchase = value.purchase && typeof value.purchase === 'object' ? value.purchase : {}
|
||||||
const dispatch = value.dispatch && typeof value.dispatch === 'object' ? value.dispatch : {}
|
const dispatch = value.dispatch && typeof value.dispatch === 'object' ? value.dispatch : {}
|
||||||
const returnNumber = value.returnNumber && typeof value.returnNumber === 'object' ? value.returnNumber : {}
|
const returnNumber = value.returnNumber && typeof value.returnNumber === 'object' ? value.returnNumber : {}
|
||||||
@@ -61,6 +62,14 @@ export function mapKuaishouCloudFulfillmentContext(value) {
|
|||||||
bindUrl: String(binding.bindUrl || '').trim(),
|
bindUrl: String(binding.bindUrl || '').trim(),
|
||||||
bindPreparedAt: binding.bindPreparedAt || null,
|
bindPreparedAt: binding.bindPreparedAt || null,
|
||||||
},
|
},
|
||||||
|
role: {
|
||||||
|
status: String(role.status || 'pending').trim() || 'pending',
|
||||||
|
name: String(role.name || '').trim(),
|
||||||
|
rid: String(role.rid || '').trim(),
|
||||||
|
refreshedAt: role.refreshedAt || null,
|
||||||
|
errorMessage: String(role.errorMessage || '').trim(),
|
||||||
|
rawInfo: role.rawInfo && typeof role.rawInfo === 'object' ? role.rawInfo : null,
|
||||||
|
},
|
||||||
purchase: {
|
purchase: {
|
||||||
autoBuyEnabled: purchase.autoBuyEnabled !== false,
|
autoBuyEnabled: purchase.autoBuyEnabled !== false,
|
||||||
minAssetReserve: Number(purchase.minAssetReserve || 0) || 0,
|
minAssetReserve: Number(purchase.minAssetReserve || 0) || 0,
|
||||||
@@ -84,6 +93,7 @@ export function mapKuaishouCloudFulfillmentContext(value) {
|
|||||||
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(),
|
||||||
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(),
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ import {
|
|||||||
import {
|
import {
|
||||||
appointCloudtentaclesVirtualNumber,
|
appointCloudtentaclesVirtualNumber,
|
||||||
backCloudtentaclesVirtualNumber,
|
backCloudtentaclesVirtualNumber,
|
||||||
|
getCloudtentaclesBindInfo,
|
||||||
fetchCloudtentaclesVirtualNumberCode,
|
fetchCloudtentaclesVirtualNumberCode,
|
||||||
generateCloudtentaclesLoginCode,
|
generateCloudtentaclesLoginCode,
|
||||||
getCloudtentaclesBindUrl,
|
getCloudtentaclesBindUrl,
|
||||||
@@ -973,6 +974,85 @@ export async function dispatchAdminTaskKuaishouCloudFulfillment(taskId, payload
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @returns {Promise<AdminTaskActionResponse>} */
|
||||||
|
/** @param {AdminEntityIdInput} taskId */
|
||||||
|
/** @param {AdminViewerSessionInput | null} [session] */
|
||||||
|
export async function refreshAdminTaskKuaishouCloudRoleInfo(taskId, session = null) {
|
||||||
|
const task = await getRequiredTask(taskId)
|
||||||
|
const now = nowIso()
|
||||||
|
const viewerContext = createAdminViewerContext(session)
|
||||||
|
|
||||||
|
if (!viewerContext.canOperateAssistedTask) {
|
||||||
|
throw createHttpError('当前账号没有权限刷新角色信息', {
|
||||||
|
statusCode: 403,
|
||||||
|
errorCode: 'admin_task_refresh_kuaishou_cloud_role_forbidden',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isKuaishouCloudTask(task)) {
|
||||||
|
throw createHttpError('当前任务不是快手 cloud 履约任务', {
|
||||||
|
statusCode: 409,
|
||||||
|
errorCode: 'admin_task_not_kuaishou_cloud',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const taskContext = parseTaskContext(task)
|
||||||
|
const flow = normalizeKuaishouCloudFlow(taskContext.kuaishouCloudFulfillment)
|
||||||
|
if (!flow.binding.vnId || !flow.binding.vnKey) {
|
||||||
|
throw createHttpError('当前任务还没有可查询的绑定角色信息,请先准备绑定资源', {
|
||||||
|
statusCode: 409,
|
||||||
|
errorCode: 'admin_task_kuaishou_cloud_missing_bind_info_context',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const cloudContext = resolvePersistedCloudtentaclesContext()
|
||||||
|
const bindInfoResult = await getCloudtentaclesBindInfo({
|
||||||
|
...cloudContext,
|
||||||
|
key: flow.binding.vnKey,
|
||||||
|
id: flow.binding.vnId,
|
||||||
|
})
|
||||||
|
|
||||||
|
const bindInfo = normalizeKuaishouCloudRoleInfo(bindInfoResult.bindInfo)
|
||||||
|
const nextContext = {
|
||||||
|
...taskContext,
|
||||||
|
kuaishouCloudFulfillment: {
|
||||||
|
...flow,
|
||||||
|
role: {
|
||||||
|
status: bindInfo.name || bindInfo.rid ? 'ready' : 'pending',
|
||||||
|
name: bindInfo.name,
|
||||||
|
rid: bindInfo.rid,
|
||||||
|
refreshedAt: now,
|
||||||
|
errorMessage: bindInfo.name || bindInfo.rid ? '' : '当前还没有查询到角色信息,请让客户完成绑定后再刷新',
|
||||||
|
rawInfo: bindInfo.rawInfo,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const updatedTask = await updateTask(task.id, {
|
||||||
|
role_id: bindInfo.rid || task.role_id || '',
|
||||||
|
role_name: bindInfo.name || task.role_name || '',
|
||||||
|
context_json: JSON.stringify(nextContext),
|
||||||
|
updated_at: now,
|
||||||
|
})
|
||||||
|
|
||||||
|
await createTaskEvent(task.id, 'kuaishou_cloud_role_info_refreshed', {
|
||||||
|
roleName: bindInfo.name,
|
||||||
|
roleId: bindInfo.rid,
|
||||||
|
vnId: flow.binding.vnId,
|
||||||
|
refreshedBy: session
|
||||||
|
? {
|
||||||
|
userId: Number(session.userId || 0) || 0,
|
||||||
|
username: String(session.username || '').trim(),
|
||||||
|
role: String(session.role || '').trim(),
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
}, now)
|
||||||
|
|
||||||
|
return {
|
||||||
|
task: mapTaskActionPayload(updatedTask),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/** @returns {Promise<AdminTaskActionResponse>} */
|
/** @returns {Promise<AdminTaskActionResponse>} */
|
||||||
/** @param {AdminEntityIdInput} taskId */
|
/** @param {AdminEntityIdInput} taskId */
|
||||||
/** @param {AdminViewerSessionInput | null} [session] */
|
/** @param {AdminViewerSessionInput | null} [session] */
|
||||||
@@ -1085,6 +1165,7 @@ export async function returnNumberAdminTaskKuaishouCloudFulfillment(taskId, sess
|
|||||||
...flow.consume,
|
...flow.consume,
|
||||||
status: consumeStatus,
|
status: consumeStatus,
|
||||||
shopId: shopId || flow.consume.shopId,
|
shopId: shopId || flow.consume.shopId,
|
||||||
|
shopName: String(flow.consume.shopName || order?.shop_name || '').trim(),
|
||||||
autoConsumeEnabled: flow.consume.autoConsumeEnabled === true,
|
autoConsumeEnabled: flow.consume.autoConsumeEnabled === true,
|
||||||
consumedAt,
|
consumedAt,
|
||||||
errorMessage: consumeErrorMessage,
|
errorMessage: consumeErrorMessage,
|
||||||
@@ -1202,6 +1283,7 @@ function resolvePersistedCloudtentaclesContext() {
|
|||||||
function normalizeKuaishouCloudFlow(value) {
|
function normalizeKuaishouCloudFlow(value) {
|
||||||
const source = value && typeof value === 'object' ? value : {}
|
const source = value && typeof value === 'object' ? value : {}
|
||||||
const binding = source.binding && typeof source.binding === 'object' ? source.binding : {}
|
const binding = source.binding && typeof source.binding === 'object' ? source.binding : {}
|
||||||
|
const role = source.role && typeof source.role === 'object' ? source.role : {}
|
||||||
const purchase = source.purchase && typeof source.purchase === 'object' ? source.purchase : {}
|
const purchase = source.purchase && typeof source.purchase === 'object' ? source.purchase : {}
|
||||||
const dispatch = source.dispatch && typeof source.dispatch === 'object' ? source.dispatch : {}
|
const dispatch = source.dispatch && typeof source.dispatch === 'object' ? source.dispatch : {}
|
||||||
const returnNumber = source.returnNumber && typeof source.returnNumber === 'object' ? source.returnNumber : {}
|
const returnNumber = source.returnNumber && typeof source.returnNumber === 'object' ? source.returnNumber : {}
|
||||||
@@ -1235,6 +1317,14 @@ function normalizeKuaishouCloudFlow(value) {
|
|||||||
bindUrl: String(binding.bindUrl || '').trim(),
|
bindUrl: String(binding.bindUrl || '').trim(),
|
||||||
bindPreparedAt: binding.bindPreparedAt || null,
|
bindPreparedAt: binding.bindPreparedAt || null,
|
||||||
},
|
},
|
||||||
|
role: {
|
||||||
|
status: String(role.status || 'pending').trim() || 'pending',
|
||||||
|
name: String(role.name || '').trim(),
|
||||||
|
rid: String(role.rid || '').trim(),
|
||||||
|
refreshedAt: role.refreshedAt || null,
|
||||||
|
errorMessage: String(role.errorMessage || '').trim(),
|
||||||
|
rawInfo: role.rawInfo && typeof role.rawInfo === 'object' ? role.rawInfo : null,
|
||||||
|
},
|
||||||
purchase: {
|
purchase: {
|
||||||
autoBuyEnabled: purchase.autoBuyEnabled !== false,
|
autoBuyEnabled: purchase.autoBuyEnabled !== false,
|
||||||
minAssetReserve: Number(purchase.minAssetReserve || 0) || 0,
|
minAssetReserve: Number(purchase.minAssetReserve || 0) || 0,
|
||||||
@@ -1260,6 +1350,7 @@ function normalizeKuaishouCloudFlow(value) {
|
|||||||
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(),
|
||||||
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(),
|
||||||
@@ -1267,6 +1358,16 @@ function normalizeKuaishouCloudFlow(value) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizeKuaishouCloudRoleInfo(value) {
|
||||||
|
const rawInfo = value && typeof value === 'object' ? value : null
|
||||||
|
|
||||||
|
return {
|
||||||
|
name: String(rawInfo?.name || rawInfo?.roleName || rawInfo?.nickname || '').trim(),
|
||||||
|
rid: String(rawInfo?.rid || rawInfo?.roleId || rawInfo?.uid || '').trim(),
|
||||||
|
rawInfo,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function resolveKuaishouCloudBindingResources(flow, { skuItems = [], knapsackItems = [] } = {}) {
|
function resolveKuaishouCloudBindingResources(flow, { skuItems = [], knapsackItems = [] } = {}) {
|
||||||
const normalizedSkuItems = Array.isArray(skuItems) ? skuItems.filter(isCloudSkuLikeItem) : []
|
const normalizedSkuItems = Array.isArray(skuItems) ? skuItems.filter(isCloudSkuLikeItem) : []
|
||||||
const normalizedKnapsackItems = Array.isArray(knapsackItems) ? knapsackItems.filter(isCloudSkuLikeItem) : []
|
const normalizedKnapsackItems = Array.isArray(knapsackItems) ? knapsackItems.filter(isCloudSkuLikeItem) : []
|
||||||
|
|||||||
@@ -918,6 +918,8 @@ function mapClaimKuaishouCloudFulfillment(task, order) {
|
|||||||
},
|
},
|
||||||
consume: {
|
consume: {
|
||||||
status: String(consume.status || 'pending').trim() || 'pending',
|
status: String(consume.status || 'pending').trim() || 'pending',
|
||||||
|
shopId: String(consume.shopId || '').trim(),
|
||||||
|
shopName: String(consume.shopName || '').trim(),
|
||||||
consumedAt: consume.consumedAt || null,
|
consumedAt: consume.consumedAt || null,
|
||||||
errorMessage: String(consume.errorMessage || '').trim(),
|
errorMessage: String(consume.errorMessage || '').trim(),
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -161,6 +161,7 @@ function normalizeKuaishouCloudFlow(value) {
|
|||||||
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(),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -116,6 +116,14 @@ export async function syncDeliveryTasksForOrder(order, orderItems) {
|
|||||||
bindUrl: '',
|
bindUrl: '',
|
||||||
bindPreparedAt: null,
|
bindPreparedAt: null,
|
||||||
},
|
},
|
||||||
|
role: {
|
||||||
|
status: 'pending',
|
||||||
|
name: '',
|
||||||
|
rid: '',
|
||||||
|
refreshedAt: null,
|
||||||
|
errorMessage: '',
|
||||||
|
rawInfo: null,
|
||||||
|
},
|
||||||
purchase: {
|
purchase: {
|
||||||
autoBuyEnabled: fulfillmentConfig.cloudtentacles?.autoBuyEnabled !== false,
|
autoBuyEnabled: fulfillmentConfig.cloudtentacles?.autoBuyEnabled !== false,
|
||||||
minAssetReserve: Number(fulfillmentConfig.cloudtentacles?.minAssetReserve || 0) || 0,
|
minAssetReserve: Number(fulfillmentConfig.cloudtentacles?.minAssetReserve || 0) || 0,
|
||||||
@@ -141,6 +149,7 @@ export async function syncDeliveryTasksForOrder(order, orderItems) {
|
|||||||
consume: {
|
consume: {
|
||||||
status: 'pending',
|
status: 'pending',
|
||||||
shopId: String(fulfillmentConfig.kuaishouConsume?.shopId || kuaishouShopConfig.shopId || '').trim(),
|
shopId: String(fulfillmentConfig.kuaishouConsume?.shopId || kuaishouShopConfig.shopId || '').trim(),
|
||||||
|
shopName: String(fulfillmentConfig.kuaishouConsume?.shopName || kuaishouShopConfig.kshopName || order.shop_name || '').trim(),
|
||||||
autoConsumeEnabled: fulfillmentConfig.kuaishouConsume?.autoConsumeAfterDispatch === true,
|
autoConsumeEnabled: fulfillmentConfig.kuaishouConsume?.autoConsumeAfterDispatch === true,
|
||||||
consumedAt: null,
|
consumedAt: null,
|
||||||
errorMessage: '',
|
errorMessage: '',
|
||||||
|
|||||||
@@ -52,6 +52,7 @@ export function mapKuaishouCloudFulfillmentItemsToBindings(config = {}) {
|
|||||||
},
|
},
|
||||||
kuaishouConsume: {
|
kuaishouConsume: {
|
||||||
shopId: String(item.kuaishouConsumeShopId || '').trim(),
|
shopId: String(item.kuaishouConsumeShopId || '').trim(),
|
||||||
|
shopName: String(item.kuaishouConsumeShopName || '').trim(),
|
||||||
autoConsumeAfterDispatch: item.autoConsumeAfterDispatch === true,
|
autoConsumeAfterDispatch: item.autoConsumeAfterDispatch === true,
|
||||||
},
|
},
|
||||||
notes: String(item.notes || '').trim(),
|
notes: String(item.notes || '').trim(),
|
||||||
@@ -137,6 +138,7 @@ function normalizeKuaishouCloudFulfillmentItem(rawValue) {
|
|||||||
autoReturnNumberAfterDispatch: rawValue.autoReturnNumberAfterDispatch === true,
|
autoReturnNumberAfterDispatch: rawValue.autoReturnNumberAfterDispatch === true,
|
||||||
autoConsumeAfterDispatch: rawValue.autoConsumeAfterDispatch === true,
|
autoConsumeAfterDispatch: rawValue.autoConsumeAfterDispatch === true,
|
||||||
kuaishouConsumeShopId: String(rawValue.kuaishouConsumeShopId || '').trim(),
|
kuaishouConsumeShopId: String(rawValue.kuaishouConsumeShopId || '').trim(),
|
||||||
|
kuaishouConsumeShopName: String(rawValue.kuaishouConsumeShopName || '').trim(),
|
||||||
notes: String(rawValue.notes || '').trim(),
|
notes: String(rawValue.notes || '').trim(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ export function resolveCloudtentaclesConfig(overrides = {}) {
|
|||||||
vnVerifCodePath: '/public/vn_verif_code',
|
vnVerifCodePath: '/public/vn_verif_code',
|
||||||
vnVerifyLoginCodePath: '/vn/verify_login_code',
|
vnVerifyLoginCodePath: '/vn/verify_login_code',
|
||||||
vnBindUrlPath: '/vn/bind_url',
|
vnBindUrlPath: '/vn/bind_url',
|
||||||
|
vnBindInfoPath: '/vn/bind_info',
|
||||||
vnBackPath: '/vn/back',
|
vnBackPath: '/vn/back',
|
||||||
publicKeyPem: '',
|
publicKeyPem: '',
|
||||||
clientSource: 'ct-client',
|
clientSource: 'ct-client',
|
||||||
@@ -57,6 +58,7 @@ export function resolveCloudtentaclesConfig(overrides = {}) {
|
|||||||
'/vn/verify_login_code',
|
'/vn/verify_login_code',
|
||||||
),
|
),
|
||||||
vnBindUrlPath: normalizePath(overrides.vnBindUrlPath || baseConfig.vnBindUrlPath, '/vn/bind_url'),
|
vnBindUrlPath: normalizePath(overrides.vnBindUrlPath || baseConfig.vnBindUrlPath, '/vn/bind_url'),
|
||||||
|
vnBindInfoPath: normalizePath(overrides.vnBindInfoPath || baseConfig.vnBindInfoPath, '/vn/bind_info'),
|
||||||
vnBackPath: normalizePath(overrides.vnBackPath || baseConfig.vnBackPath, '/vn/back'),
|
vnBackPath: normalizePath(overrides.vnBackPath || baseConfig.vnBackPath, '/vn/back'),
|
||||||
publicKeyPem: normalizePem(overrides.publicKeyPem || baseConfig.publicKeyPem),
|
publicKeyPem: normalizePem(overrides.publicKeyPem || baseConfig.publicKeyPem),
|
||||||
clientSource: String(overrides.clientSource || baseConfig.clientSource || 'ct-client').trim() || 'ct-client',
|
clientSource: String(overrides.clientSource || baseConfig.clientSource || 'ct-client').trim() || 'ct-client',
|
||||||
|
|||||||
@@ -159,6 +159,35 @@ export async function getCloudtentaclesBindUrl(payload = {}) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getCloudtentaclesBindInfo(payload = {}) {
|
||||||
|
const token = requireToken(payload.token, 'cloudtentacles 获取绑定信息缺少 token', 'cloudtentacles_vn_bind_info_missing_token')
|
||||||
|
const key = requireKey(payload.key, 'cloudtentacles 获取绑定信息缺少 key', 'cloudtentacles_vn_bind_info_missing_key')
|
||||||
|
const id = requireId(payload.id, 'cloudtentacles 获取绑定信息缺少 id', 'cloudtentacles_vn_bind_info_missing_id')
|
||||||
|
const config = resolveCloudtentaclesConfig(payload)
|
||||||
|
|
||||||
|
const result = await cloudtentaclesRequest(config.vnBindInfoPath, {
|
||||||
|
...config,
|
||||||
|
method: 'POST',
|
||||||
|
token,
|
||||||
|
body: [{ id, key }],
|
||||||
|
businessErrorStatusCode: 401,
|
||||||
|
businessErrorCode: 'cloudtentacles_vn_bind_info_failed',
|
||||||
|
})
|
||||||
|
|
||||||
|
const items = Array.isArray(result.payload?.data) ? result.payload.data : []
|
||||||
|
const matchedItem = items.find((item) => Number(item?.id || 0) === id) || items[0] || {}
|
||||||
|
const bindInfo = parseBindInfo(matchedItem?.bind_info)
|
||||||
|
|
||||||
|
return {
|
||||||
|
baseUrl: config.baseUrl,
|
||||||
|
key,
|
||||||
|
id,
|
||||||
|
bindInfo,
|
||||||
|
rawItem: isPlainObject(matchedItem) ? matchedItem : {},
|
||||||
|
raw: result.payload ?? null,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export async function backCloudtentaclesVirtualNumber(payload = {}) {
|
export async function backCloudtentaclesVirtualNumber(payload = {}) {
|
||||||
const token = requireToken(payload.token, 'cloudtentacles 退还号码缺少 token', 'cloudtentacles_vn_back_missing_token')
|
const token = requireToken(payload.token, 'cloudtentacles 退还号码缺少 token', 'cloudtentacles_vn_back_missing_token')
|
||||||
const key = requireKey(payload.key, 'cloudtentacles 退还号码缺少 key', 'cloudtentacles_vn_back_missing_key')
|
const key = requireKey(payload.key, 'cloudtentacles 退还号码缺少 key', 'cloudtentacles_vn_back_missing_key')
|
||||||
@@ -185,14 +214,14 @@ export async function backCloudtentaclesVirtualNumber(payload = {}) {
|
|||||||
|
|
||||||
function mapVirtualNumberItem(item) {
|
function mapVirtualNumberItem(item) {
|
||||||
const source = isPlainObject(item) ? item : {}
|
const source = isPlainObject(item) ? item : {}
|
||||||
const bindInfoText = String(source.bind_info || '').trim()
|
const bindInfo = parseBindInfo(source.bind_info)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: Number(source.id || 0),
|
id: Number(source.id || 0),
|
||||||
phone: String(source.phone || '').trim(),
|
phone: String(source.phone || '').trim(),
|
||||||
status: Number(source.status || 0),
|
status: Number(source.status || 0),
|
||||||
countTime: Number(source.count_time || 0),
|
countTime: Number(source.count_time || 0),
|
||||||
bindInfo: tryParseJson(bindInfoText) || bindInfoText || null,
|
bindInfo,
|
||||||
raw: source,
|
raw: source,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -241,6 +270,11 @@ function tryParseJson(text) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function parseBindInfo(value) {
|
||||||
|
const bindInfoText = String(value || '').trim()
|
||||||
|
return tryParseJson(bindInfoText) || bindInfoText || null
|
||||||
|
}
|
||||||
|
|
||||||
function isPlainObject(value) {
|
function isPlainObject(value) {
|
||||||
return Boolean(value) && typeof value === 'object' && !Array.isArray(value)
|
return Boolean(value) && typeof value === 'object' && !Array.isArray(value)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ import { nowIso } from '../../../utils/time.js'
|
|||||||
|
|
||||||
let timer = null
|
let timer = null
|
||||||
let running = false
|
let running = false
|
||||||
|
const BOOST_INTERVAL_MIN_MS = 3_000
|
||||||
|
const BOOST_INTERVAL_MAX_MS = 5_000
|
||||||
|
|
||||||
export function startKhhaoAutoSyncLoop() {
|
export function startKhhaoAutoSyncLoop() {
|
||||||
scheduleNextRun(5_000)
|
scheduleNextRun(5_000)
|
||||||
@@ -22,6 +24,7 @@ export function stopKhhaoAutoSyncLoop() {
|
|||||||
|
|
||||||
async function runKhhaoAutoSyncCycle() {
|
async function runKhhaoAutoSyncCycle() {
|
||||||
const source = getKhhaoSourceConfig()
|
const source = getKhhaoSourceConfig()
|
||||||
|
const currentState = getKhhaoSyncState()
|
||||||
|
|
||||||
if (!source.enabled || !source.autoSync?.enabled) {
|
if (!source.enabled || !source.autoSync?.enabled) {
|
||||||
scheduleNextRun(resolveIntervalMs(source.autoSync?.intervalMinutes))
|
scheduleNextRun(resolveIntervalMs(source.autoSync?.intervalMinutes))
|
||||||
@@ -30,7 +33,7 @@ async function runKhhaoAutoSyncCycle() {
|
|||||||
|
|
||||||
if (running) {
|
if (running) {
|
||||||
logWarn('[khhao/auto-sync]', '检测到上一次自动同步仍在运行,本轮跳过')
|
logWarn('[khhao/auto-sync]', '检测到上一次自动同步仍在运行,本轮跳过')
|
||||||
scheduleNextRun(resolveIntervalMs(source.autoSync?.intervalMinutes))
|
scheduleNextRun(resolveNextDelayMs(source, currentState))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -63,6 +66,8 @@ async function runKhhaoAutoSyncCycle() {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const latestCreatedAt = resolveLatestOrderCreatedAt(result.results)
|
const latestCreatedAt = resolveLatestOrderCreatedAt(result.results)
|
||||||
|
const nextWatchOrders = reconcileKhhaoWatchOrders(baselineState.watchOrders, result.results, startedAt)
|
||||||
|
const watchMode = nextWatchOrders.length > 0 ? 'boosted' : 'normal'
|
||||||
|
|
||||||
saveKhhaoSyncState({
|
saveKhhaoSyncState({
|
||||||
...getKhhaoSyncState(),
|
...getKhhaoSyncState(),
|
||||||
@@ -75,6 +80,8 @@ async function runKhhaoAutoSyncCycle() {
|
|||||||
syncedCount: result.syncedCount,
|
syncedCount: result.syncedCount,
|
||||||
ignoredCount: result.ignoredCount,
|
ignoredCount: result.ignoredCount,
|
||||||
lastOrderCreatedAt: latestCreatedAt,
|
lastOrderCreatedAt: latestCreatedAt,
|
||||||
|
watchMode,
|
||||||
|
watchOrders: nextWatchOrders,
|
||||||
})
|
})
|
||||||
|
|
||||||
logInfo('[khhao/auto-sync]', 'khhao 自动同步完成', {
|
logInfo('[khhao/auto-sync]', 'khhao 自动同步完成', {
|
||||||
@@ -82,6 +89,13 @@ async function runKhhaoAutoSyncCycle() {
|
|||||||
syncedCount: result.syncedCount,
|
syncedCount: result.syncedCount,
|
||||||
ignoredCount: result.ignoredCount,
|
ignoredCount: result.ignoredCount,
|
||||||
syncFromCreatedAt: baselineState.syncFromCreatedAt,
|
syncFromCreatedAt: baselineState.syncFromCreatedAt,
|
||||||
|
watchMode,
|
||||||
|
watchOrderCount: nextWatchOrders.length,
|
||||||
|
watchOrders: nextWatchOrders.map((item) => ({
|
||||||
|
platformOrderId: item.platformOrderId,
|
||||||
|
shopName: item.shopName,
|
||||||
|
payStatus: item.payStatus,
|
||||||
|
})),
|
||||||
})
|
})
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const message = error instanceof Error ? error.message : String(error || 'khhao 自动同步失败')
|
const message = error instanceof Error ? error.message : String(error || 'khhao 自动同步失败')
|
||||||
@@ -96,7 +110,7 @@ async function runKhhaoAutoSyncCycle() {
|
|||||||
logError('[khhao/auto-sync]', 'khhao 自动同步失败', error)
|
logError('[khhao/auto-sync]', 'khhao 自动同步失败', error)
|
||||||
} finally {
|
} finally {
|
||||||
running = false
|
running = false
|
||||||
scheduleNextRun(resolveIntervalMs(source.autoSync?.intervalMinutes))
|
scheduleNextRun(resolveNextDelayMs(source, getKhhaoSyncState()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -113,6 +127,80 @@ function resolveIntervalMs(value) {
|
|||||||
return normalized * 60 * 1000
|
return normalized * 60 * 1000
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function resolveNextDelayMs(source, syncState) {
|
||||||
|
if (hasActiveKhhaoWatchOrders(syncState?.watchOrders)) {
|
||||||
|
return resolveBoostIntervalMs()
|
||||||
|
}
|
||||||
|
|
||||||
|
return resolveIntervalMs(source?.autoSync?.intervalMinutes)
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveBoostIntervalMs() {
|
||||||
|
const delta = BOOST_INTERVAL_MAX_MS - BOOST_INTERVAL_MIN_MS
|
||||||
|
return BOOST_INTERVAL_MIN_MS + Math.floor(Math.random() * (delta + 1))
|
||||||
|
}
|
||||||
|
|
||||||
|
export function reconcileKhhaoWatchOrders(currentWatchOrders = [], results = [], now = nowIso()) {
|
||||||
|
const nextMap = new Map(normalizeKhhaoWatchOrders(currentWatchOrders).map((item) => [item.platformOrderId, item]))
|
||||||
|
|
||||||
|
for (const item of Array.isArray(results) ? results : []) {
|
||||||
|
const platformOrderId = String(item?.platformOrderId || '').trim()
|
||||||
|
if (!platformOrderId || item?.ignored) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
const payStatus = normalizePayStatus(item?.payStatus)
|
||||||
|
if (payStatus === 'paid') {
|
||||||
|
nextMap.delete(platformOrderId)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if (payStatus !== 'unpaid') {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
const previous = nextMap.get(platformOrderId)
|
||||||
|
nextMap.set(platformOrderId, {
|
||||||
|
platformOrderId,
|
||||||
|
shopId: String(item?.shopId || previous?.shopId || '').trim(),
|
||||||
|
shopName: String(item?.shopName || previous?.shopName || '').trim(),
|
||||||
|
itemTitle: String(item?.itemTitle || previous?.itemTitle || '').trim(),
|
||||||
|
payStatus,
|
||||||
|
detectedAt: previous?.detectedAt || now,
|
||||||
|
lastSeenAt: now,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return [...nextMap.values()]
|
||||||
|
}
|
||||||
|
|
||||||
|
export function hasActiveKhhaoWatchOrders(watchOrders = []) {
|
||||||
|
return normalizeKhhaoWatchOrders(watchOrders).length > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeKhhaoWatchOrders(value) {
|
||||||
|
return (Array.isArray(value) ? value : [])
|
||||||
|
.map((item) => ({
|
||||||
|
platformOrderId: String(item?.platformOrderId || '').trim(),
|
||||||
|
shopId: String(item?.shopId || '').trim(),
|
||||||
|
shopName: String(item?.shopName || '').trim(),
|
||||||
|
itemTitle: String(item?.itemTitle || '').trim(),
|
||||||
|
payStatus: normalizePayStatus(item?.payStatus),
|
||||||
|
detectedAt: String(item?.detectedAt || '').trim(),
|
||||||
|
lastSeenAt: String(item?.lastSeenAt || '').trim(),
|
||||||
|
}))
|
||||||
|
.filter((item) => item.platformOrderId && item.payStatus === 'unpaid')
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizePayStatus(value) {
|
||||||
|
const normalized = String(value || '').trim().toLowerCase()
|
||||||
|
if (normalized === 'paid') {
|
||||||
|
return 'paid'
|
||||||
|
}
|
||||||
|
|
||||||
|
return normalized === 'unpaid' ? 'unpaid' : 'unknown'
|
||||||
|
}
|
||||||
|
|
||||||
function resolveLatestOrderCreatedAt(results = []) {
|
function resolveLatestOrderCreatedAt(results = []) {
|
||||||
let latestText = ''
|
let latestText = ''
|
||||||
let latestTime = 0
|
let latestTime = 0
|
||||||
|
|||||||
@@ -0,0 +1,79 @@
|
|||||||
|
import test from 'node:test'
|
||||||
|
import assert from 'node:assert/strict'
|
||||||
|
|
||||||
|
import {
|
||||||
|
hasActiveKhhaoWatchOrders,
|
||||||
|
reconcileKhhaoWatchOrders,
|
||||||
|
} from './auto-sync-service.js'
|
||||||
|
|
||||||
|
test('reconcileKhhaoWatchOrders adds new unpaid synced order into watch list', () => {
|
||||||
|
const now = '2026-05-03T12:00:00.000Z'
|
||||||
|
const next = reconcileKhhaoWatchOrders([], [
|
||||||
|
{
|
||||||
|
platformOrderId: '2612300323173226',
|
||||||
|
shopId: '4269276762',
|
||||||
|
shopName: '稚嫩游戏交易店',
|
||||||
|
itemTitle: '暗影哥特',
|
||||||
|
payStatus: 'unpaid',
|
||||||
|
ignored: false,
|
||||||
|
},
|
||||||
|
], now)
|
||||||
|
|
||||||
|
assert.equal(next.length, 1)
|
||||||
|
assert.deepEqual(next[0], {
|
||||||
|
platformOrderId: '2612300323173226',
|
||||||
|
shopId: '4269276762',
|
||||||
|
shopName: '稚嫩游戏交易店',
|
||||||
|
itemTitle: '暗影哥特',
|
||||||
|
payStatus: 'unpaid',
|
||||||
|
detectedAt: now,
|
||||||
|
lastSeenAt: now,
|
||||||
|
})
|
||||||
|
assert.equal(hasActiveKhhaoWatchOrders(next), true)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('reconcileKhhaoWatchOrders removes watched order after payment is detected', () => {
|
||||||
|
const current = [
|
||||||
|
{
|
||||||
|
platformOrderId: '2612300323173226',
|
||||||
|
shopId: '4269276762',
|
||||||
|
shopName: '稚嫩游戏交易店',
|
||||||
|
itemTitle: '暗影哥特',
|
||||||
|
payStatus: 'unpaid',
|
||||||
|
detectedAt: '2026-05-03T12:00:00.000Z',
|
||||||
|
lastSeenAt: '2026-05-03T12:00:00.000Z',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
const next = reconcileKhhaoWatchOrders(current, [
|
||||||
|
{
|
||||||
|
platformOrderId: '2612300323173226',
|
||||||
|
shopId: '4269276762',
|
||||||
|
shopName: '稚嫩游戏交易店',
|
||||||
|
itemTitle: '暗影哥特',
|
||||||
|
payStatus: 'paid',
|
||||||
|
ignored: false,
|
||||||
|
},
|
||||||
|
], '2026-05-03T12:00:04.000Z')
|
||||||
|
|
||||||
|
assert.deepEqual(next, [])
|
||||||
|
assert.equal(hasActiveKhhaoWatchOrders(next), false)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('reconcileKhhaoWatchOrders ignores ignored and unknown-pay-status rows', () => {
|
||||||
|
const now = '2026-05-03T12:00:00.000Z'
|
||||||
|
const next = reconcileKhhaoWatchOrders([], [
|
||||||
|
{
|
||||||
|
platformOrderId: 'ignored-order',
|
||||||
|
payStatus: 'unpaid',
|
||||||
|
ignored: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
platformOrderId: 'unknown-order',
|
||||||
|
payStatus: 'unknown',
|
||||||
|
ignored: false,
|
||||||
|
},
|
||||||
|
], now)
|
||||||
|
|
||||||
|
assert.deepEqual(next, [])
|
||||||
|
})
|
||||||
@@ -1,7 +1,12 @@
|
|||||||
// @ts-check
|
// @ts-check
|
||||||
|
|
||||||
import { upsertOrderFromSource } from '../../order/order-service.js'
|
import { upsertOrderFromSource } from '../../order/order-service.js'
|
||||||
import { mapKhhaoOrderPreview, mapKhhaoOrderToSourceEvent } from './order-mapper-service.js'
|
import {
|
||||||
|
mapKhhaoOrderPreview,
|
||||||
|
mapKhhaoOrderToSourceEvent,
|
||||||
|
resolveKhhaoOrderStatus,
|
||||||
|
resolveKhhaoPayStatus,
|
||||||
|
} from './order-mapper-service.js'
|
||||||
import { queryKhhaoOrderList } from './order-query-service.js'
|
import { queryKhhaoOrderList } from './order-query-service.js'
|
||||||
import { loginKhhaoSession } from './session-service.js'
|
import { loginKhhaoSession } from './session-service.js'
|
||||||
|
|
||||||
@@ -57,8 +62,11 @@ export async function syncKhhaoOrders(payload = {}) {
|
|||||||
provider: 'khhao',
|
provider: 'khhao',
|
||||||
platform: preview.platform || 'unknown',
|
platform: preview.platform || 'unknown',
|
||||||
shopId: preview.shopId,
|
shopId: preview.shopId,
|
||||||
|
shopName: preview.shopName,
|
||||||
skuCode: preview.skuCode,
|
skuCode: preview.skuCode,
|
||||||
itemTitle: preview.itemTitle,
|
itemTitle: preview.itemTitle,
|
||||||
|
orderStatus: resolvePreviewOrderStatus(preview),
|
||||||
|
payStatus: resolvePreviewPayStatus(preview),
|
||||||
rawOrderCreatedAt: preview.orderCreatedAt,
|
rawOrderCreatedAt: preview.orderCreatedAt,
|
||||||
ignored: true,
|
ignored: true,
|
||||||
ignoreReason: 'before_sync_baseline',
|
ignoreReason: 'before_sync_baseline',
|
||||||
@@ -77,8 +85,11 @@ export async function syncKhhaoOrders(payload = {}) {
|
|||||||
provider: sourceEvent.provider,
|
provider: sourceEvent.provider,
|
||||||
platform: sourceEvent.platform,
|
platform: sourceEvent.platform,
|
||||||
shopId: sourceEvent.shopId,
|
shopId: sourceEvent.shopId,
|
||||||
|
shopName: sourceEvent.shopName,
|
||||||
skuCode: preview.skuCode,
|
skuCode: preview.skuCode,
|
||||||
itemTitle: preview.itemTitle,
|
itemTitle: preview.itemTitle,
|
||||||
|
orderStatus: sourceEvent.orderStatus,
|
||||||
|
payStatus: sourceEvent.payStatus,
|
||||||
rawOrderCreatedAt: preview.orderCreatedAt,
|
rawOrderCreatedAt: preview.orderCreatedAt,
|
||||||
ignored: Boolean(upsertResult.ignored),
|
ignored: Boolean(upsertResult.ignored),
|
||||||
ignoreReason: String(upsertResult.ignoreReason || '').trim(),
|
ignoreReason: String(upsertResult.ignoreReason || '').trim(),
|
||||||
@@ -129,3 +140,11 @@ function parseKhhaoOrderCreatedAt(value) {
|
|||||||
const isoText = text.replace(' ', 'T')
|
const isoText = text.replace(' ', 'T')
|
||||||
return Date.parse(`${isoText}+08:00`)
|
return Date.parse(`${isoText}+08:00`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function resolvePreviewOrderStatus(preview) {
|
||||||
|
return resolveKhhaoOrderStatus(preview?.status)
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolvePreviewPayStatus(preview) {
|
||||||
|
return resolveKhhaoPayStatus(preview?.status)
|
||||||
|
}
|
||||||
|
|||||||
@@ -58,6 +58,8 @@ function createDefaultKhhaoSyncState() {
|
|||||||
syncedCount: 0,
|
syncedCount: 0,
|
||||||
ignoredCount: 0,
|
ignoredCount: 0,
|
||||||
lastOrderCreatedAt: '',
|
lastOrderCreatedAt: '',
|
||||||
|
watchMode: 'normal',
|
||||||
|
watchOrders: [],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -74,6 +76,8 @@ function normalizeKhhaoSyncState(rawValue) {
|
|||||||
syncedCount: normalizeNonNegativeInteger(source.syncedCount),
|
syncedCount: normalizeNonNegativeInteger(source.syncedCount),
|
||||||
ignoredCount: normalizeNonNegativeInteger(source.ignoredCount),
|
ignoredCount: normalizeNonNegativeInteger(source.ignoredCount),
|
||||||
lastOrderCreatedAt: String(source.lastOrderCreatedAt || '').trim(),
|
lastOrderCreatedAt: String(source.lastOrderCreatedAt || '').trim(),
|
||||||
|
watchMode: normalizeWatchMode(source.watchMode),
|
||||||
|
watchOrders: normalizeWatchOrders(source.watchOrders),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -87,6 +91,39 @@ function normalizeNonNegativeInteger(value) {
|
|||||||
return Number.isInteger(parsed) && parsed >= 0 ? parsed : 0
|
return Number.isInteger(parsed) && parsed >= 0 ? parsed : 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizeWatchMode(value) {
|
||||||
|
return String(value || '').trim() === 'boosted' ? 'boosted' : 'normal'
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeWatchOrders(value) {
|
||||||
|
return (Array.isArray(value) ? value : [])
|
||||||
|
.map((item) => normalizeWatchOrder(item))
|
||||||
|
.filter((item) => item.platformOrderId)
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeWatchOrder(value) {
|
||||||
|
const source = isPlainObject(value) ? value : {}
|
||||||
|
|
||||||
|
return {
|
||||||
|
platformOrderId: String(source.platformOrderId || '').trim(),
|
||||||
|
shopId: String(source.shopId || '').trim(),
|
||||||
|
shopName: String(source.shopName || '').trim(),
|
||||||
|
itemTitle: String(source.itemTitle || '').trim(),
|
||||||
|
payStatus: normalizeWatchPayStatus(source.payStatus),
|
||||||
|
detectedAt: String(source.detectedAt || '').trim(),
|
||||||
|
lastSeenAt: String(source.lastSeenAt || '').trim(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeWatchPayStatus(value) {
|
||||||
|
const normalized = String(value || '').trim().toLowerCase()
|
||||||
|
if (normalized === 'paid') {
|
||||||
|
return 'paid'
|
||||||
|
}
|
||||||
|
|
||||||
|
return normalized === 'unpaid' ? 'unpaid' : 'unknown'
|
||||||
|
}
|
||||||
|
|
||||||
function isPlainObject(value) {
|
function isPlainObject(value) {
|
||||||
return Object.prototype.toString.call(value) === '[object Object]'
|
return Object.prototype.toString.call(value) === '[object Object]'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -90,6 +90,7 @@ export {}
|
|||||||
* autoReturnNumberAfterDispatch?: boolean
|
* autoReturnNumberAfterDispatch?: boolean
|
||||||
* autoConsumeAfterDispatch?: boolean
|
* autoConsumeAfterDispatch?: boolean
|
||||||
* kuaishouConsumeShopId?: string
|
* kuaishouConsumeShopId?: string
|
||||||
|
* kuaishouConsumeShopName?: string
|
||||||
* notes?: string
|
* notes?: string
|
||||||
* }} AdminKuaishouCloudFulfillmentItemInput
|
* }} AdminKuaishouCloudFulfillmentItemInput
|
||||||
*/
|
*/
|
||||||
|
|||||||
Vendored
+2
@@ -22,6 +22,8 @@ declare module 'vue' {
|
|||||||
ElFormItem: typeof import('element-plus/es')['ElFormItem']
|
ElFormItem: typeof import('element-plus/es')['ElFormItem']
|
||||||
ElInput: typeof import('element-plus/es')['ElInput']
|
ElInput: typeof import('element-plus/es')['ElInput']
|
||||||
ElInputNumber: typeof import('element-plus/es')['ElInputNumber']
|
ElInputNumber: typeof import('element-plus/es')['ElInputNumber']
|
||||||
|
ElTabPane: typeof import('element-plus/es')['ElTabPane']
|
||||||
|
ElTabs: typeof import('element-plus/es')['ElTabs']
|
||||||
RouterLink: typeof import('vue-router')['RouterLink']
|
RouterLink: typeof import('vue-router')['RouterLink']
|
||||||
RouterView: typeof import('vue-router')['RouterView']
|
RouterView: typeof import('vue-router')['RouterView']
|
||||||
TencentAuthCard: typeof import('./components/tencent/TencentAuthCard.vue')['default']
|
TencentAuthCard: typeof import('./components/tencent/TencentAuthCard.vue')['default']
|
||||||
|
|||||||
@@ -91,12 +91,12 @@ const router = createRouter({
|
|||||||
{
|
{
|
||||||
path: 'platform-fulfillment',
|
path: 'platform-fulfillment',
|
||||||
meta: { allowedRoles: ['admin'] },
|
meta: { allowedRoles: ['admin'] },
|
||||||
component: () => import('@/views/admin/AdminFulfillmentBindingsView.vue'),
|
component: () => import('@/views/admin/AdminFulfillmentConfigHubView.vue'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'platform-kuaishou-cloud-fulfillment',
|
path: 'platform-kuaishou-cloud-fulfillment',
|
||||||
meta: { allowedRoles: ['admin'] },
|
meta: { allowedRoles: ['admin'] },
|
||||||
component: () => import('@/views/admin/AdminKuaishouCloudFulfillmentView.vue'),
|
redirect: '/admin/platform-fulfillment?tab=kuaishou-cloud',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'webhook-events/:eventId',
|
path: 'webhook-events/:eventId',
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import type {
|
|||||||
AdminAuditLogItem,
|
AdminAuditLogItem,
|
||||||
AdminCloudtentaclesLoginTestResult,
|
AdminCloudtentaclesLoginTestResult,
|
||||||
AdminCloudtentaclesPersistedSession,
|
AdminCloudtentaclesPersistedSession,
|
||||||
|
AdminCloudtentaclesSkuListResult,
|
||||||
AdminCloudtentaclesSendSmsResult,
|
AdminCloudtentaclesSendSmsResult,
|
||||||
AdminCloudtentaclesSourceConfig,
|
AdminCloudtentaclesSourceConfig,
|
||||||
AdminCloudtentaclesValidateSessionResult,
|
AdminCloudtentaclesValidateSessionResult,
|
||||||
@@ -272,7 +273,7 @@ export function fetchAdminCloudtentaclesSkuList(payload: {
|
|||||||
deviceId?: string
|
deviceId?: string
|
||||||
deviceType?: number
|
deviceType?: number
|
||||||
}) {
|
}) {
|
||||||
return apiPost<Record<string, unknown>>('/api/v1/admin/platform-config/cloudtentacles/sku/list', payload)
|
return apiPost<AdminCloudtentaclesSkuListResult>('/api/v1/admin/platform-config/cloudtentacles/sku/list', payload)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function buyAdminCloudtentaclesSku(payload: {
|
export function buyAdminCloudtentaclesSku(payload: {
|
||||||
@@ -504,6 +505,10 @@ export function prepareAdminTaskKuaishouCloudFulfillment(taskId: number | string
|
|||||||
return apiPost<AdminTaskActionResponse>(`/api/v1/admin/tasks/${taskId}/kuaishou-cloud/prepare`, {})
|
return apiPost<AdminTaskActionResponse>(`/api/v1/admin/tasks/${taskId}/kuaishou-cloud/prepare`, {})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function refreshAdminTaskKuaishouCloudRoleInfo(taskId: number | string) {
|
||||||
|
return apiPost<AdminTaskActionResponse>(`/api/v1/admin/tasks/${taskId}/kuaishou-cloud/refresh-role-info`, {})
|
||||||
|
}
|
||||||
|
|
||||||
export function dispatchAdminTaskKuaishouCloudFulfillment(
|
export function dispatchAdminTaskKuaishouCloudFulfillment(
|
||||||
taskId: number | string,
|
taskId: number | string,
|
||||||
payload: {
|
payload: {
|
||||||
|
|||||||
@@ -204,6 +204,16 @@ export interface AdminKhhaoSyncState {
|
|||||||
fetchedCount: number
|
fetchedCount: number
|
||||||
syncedCount: number
|
syncedCount: number
|
||||||
ignoredCount: number
|
ignoredCount: number
|
||||||
|
watchMode: string
|
||||||
|
watchOrders: Array<{
|
||||||
|
platformOrderId: string
|
||||||
|
shopId: string
|
||||||
|
shopName: string
|
||||||
|
itemTitle: string
|
||||||
|
payStatus: string
|
||||||
|
detectedAt: string
|
||||||
|
lastSeenAt: string
|
||||||
|
}>
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AdminKhhaoOrderPreview {
|
export interface AdminKhhaoOrderPreview {
|
||||||
@@ -319,6 +329,28 @@ export interface AdminCloudtentaclesValidateSessionResult {
|
|||||||
asset: number
|
asset: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface AdminCloudtentaclesSkuItem {
|
||||||
|
id: number
|
||||||
|
categoriesId: number
|
||||||
|
name: string
|
||||||
|
description: string
|
||||||
|
image: string
|
||||||
|
inventory: number
|
||||||
|
price: number
|
||||||
|
listingTime: string
|
||||||
|
delistingTime: string
|
||||||
|
buyLimitMin: number
|
||||||
|
buyLimitMax: number
|
||||||
|
raw: Record<string, unknown>
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AdminCloudtentaclesSkuListResult {
|
||||||
|
baseUrl: string
|
||||||
|
itemCount: number
|
||||||
|
items: AdminCloudtentaclesSkuItem[]
|
||||||
|
rawItems: Record<string, unknown>[]
|
||||||
|
}
|
||||||
|
|
||||||
export interface AdminFulfillmentBindingConfigItem {
|
export interface AdminFulfillmentBindingConfigItem {
|
||||||
provider: string
|
provider: string
|
||||||
platform: string
|
platform: string
|
||||||
@@ -361,6 +393,7 @@ export interface AdminKuaishouCloudFulfillmentItem {
|
|||||||
autoReturnNumberAfterDispatch: boolean
|
autoReturnNumberAfterDispatch: boolean
|
||||||
autoConsumeAfterDispatch: boolean
|
autoConsumeAfterDispatch: boolean
|
||||||
kuaishouConsumeShopId: string
|
kuaishouConsumeShopId: string
|
||||||
|
kuaishouConsumeShopName: string
|
||||||
notes: string
|
notes: string
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -532,6 +565,7 @@ export interface AdminTaskOperations {
|
|||||||
canMarkManualReview: boolean
|
canMarkManualReview: boolean
|
||||||
canCompleteManualDispatch: boolean
|
canCompleteManualDispatch: boolean
|
||||||
canPrepareKuaishouCloudFulfillment: boolean
|
canPrepareKuaishouCloudFulfillment: boolean
|
||||||
|
canRefreshKuaishouCloudRoleInfo: boolean
|
||||||
canDispatchKuaishouCloudFulfillment: boolean
|
canDispatchKuaishouCloudFulfillment: boolean
|
||||||
canReturnKuaishouCloudFulfillment: boolean
|
canReturnKuaishouCloudFulfillment: boolean
|
||||||
canSupportConfirmRole: boolean
|
canSupportConfirmRole: boolean
|
||||||
@@ -713,6 +747,14 @@ export interface AdminTaskDetail {
|
|||||||
bindUrl: string
|
bindUrl: string
|
||||||
bindPreparedAt: string | null
|
bindPreparedAt: string | null
|
||||||
}
|
}
|
||||||
|
role: {
|
||||||
|
status: string
|
||||||
|
name: string
|
||||||
|
rid: string
|
||||||
|
refreshedAt: string | null
|
||||||
|
errorMessage: string
|
||||||
|
rawInfo: Record<string, unknown> | null
|
||||||
|
}
|
||||||
purchase: {
|
purchase: {
|
||||||
autoBuyEnabled: boolean
|
autoBuyEnabled: boolean
|
||||||
minAssetReserve: number
|
minAssetReserve: number
|
||||||
@@ -736,6 +778,7 @@ export interface AdminTaskDetail {
|
|||||||
consume: {
|
consume: {
|
||||||
status: string
|
status: string
|
||||||
shopId: string
|
shopId: string
|
||||||
|
shopName: string
|
||||||
autoConsumeEnabled: boolean
|
autoConsumeEnabled: boolean
|
||||||
consumedAt: string | null
|
consumedAt: string | null
|
||||||
errorMessage: string
|
errorMessage: string
|
||||||
|
|||||||
@@ -4,9 +4,7 @@ import { computed, nextTick, onMounted, reactive, ref } from 'vue'
|
|||||||
import { showSuccess } from '@/lib/feedback'
|
import { showSuccess } from '@/lib/feedback'
|
||||||
import {
|
import {
|
||||||
fetchAdminFulfillmentBindingConfigs,
|
fetchAdminFulfillmentBindingConfigs,
|
||||||
fetchAdminKhhaoSourceConfig,
|
|
||||||
fetchAdminKuaishouEticketSourceConfig,
|
fetchAdminKuaishouEticketSourceConfig,
|
||||||
queryAdminKhhaoOrders,
|
|
||||||
lookupAdminFulfillmentBindingOrder,
|
lookupAdminFulfillmentBindingOrder,
|
||||||
saveAdminFulfillmentBindingConfigs,
|
saveAdminFulfillmentBindingConfigs,
|
||||||
} from '@/services/admin'
|
} from '@/services/admin'
|
||||||
@@ -14,7 +12,6 @@ import type {
|
|||||||
AdminFulfillmentBindingConfigItem,
|
AdminFulfillmentBindingConfigItem,
|
||||||
AdminFulfillmentLookupItem,
|
AdminFulfillmentLookupItem,
|
||||||
AdminFulfillmentLookupResult,
|
AdminFulfillmentLookupResult,
|
||||||
AdminKhhaoOrderPreview,
|
|
||||||
AdminKuaishouEticketShopConfigItem,
|
AdminKuaishouEticketShopConfigItem,
|
||||||
AdminObservedProductItem,
|
AdminObservedProductItem,
|
||||||
} from '@/types/admin'
|
} from '@/types/admin'
|
||||||
@@ -93,18 +90,6 @@ const lookupForm = reactive({
|
|||||||
shopId: '',
|
shopId: '',
|
||||||
platformOrderId: '',
|
platformOrderId: '',
|
||||||
})
|
})
|
||||||
const khhaoLookupLoading = ref(false)
|
|
||||||
const khhaoLookupErrorMessage = ref('')
|
|
||||||
const khhaoLookupResults = ref<AdminKhhaoOrderPreview[]>([])
|
|
||||||
const importedKhhaoOrderIds = ref<string[]>([])
|
|
||||||
const khhaoLookupForm = reactive({
|
|
||||||
baseUrl: 'https://admin.khhao.com',
|
|
||||||
username: '',
|
|
||||||
password: '',
|
|
||||||
page: 1,
|
|
||||||
limit: 10,
|
|
||||||
maxCaptchaAttempts: 3,
|
|
||||||
})
|
|
||||||
|
|
||||||
const ruleMetrics = computed(() => {
|
const ruleMetrics = computed(() => {
|
||||||
const completedCount = bindings.value.filter(isEditableBindingComplete).length
|
const completedCount = bindings.value.filter(isEditableBindingComplete).length
|
||||||
@@ -288,9 +273,8 @@ async function loadConfigs() {
|
|||||||
errorMessage.value = ''
|
errorMessage.value = ''
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const [response, khhaoSourceResponse, kuaishouSourceResponse] = await Promise.all([
|
const [response, kuaishouSourceResponse] = await Promise.all([
|
||||||
fetchAdminFulfillmentBindingConfigs(),
|
fetchAdminFulfillmentBindingConfigs(),
|
||||||
fetchAdminKhhaoSourceConfig(),
|
|
||||||
fetchAdminKuaishouEticketSourceConfig(),
|
fetchAdminKuaishouEticketSourceConfig(),
|
||||||
])
|
])
|
||||||
filePath.value = response.data.filePath
|
filePath.value = response.data.filePath
|
||||||
@@ -299,10 +283,6 @@ async function loadConfigs() {
|
|||||||
bindings.value = nextBindings
|
bindings.value = nextBindings
|
||||||
collapsedBindingIds.value = buildCollapsedIds(nextBindings)
|
collapsedBindingIds.value = buildCollapsedIds(nextBindings)
|
||||||
observedProducts.value = response.data.observedProducts
|
observedProducts.value = response.data.observedProducts
|
||||||
khhaoLookupForm.baseUrl = khhaoSourceResponse.data.source.baseUrl || 'https://admin.khhao.com'
|
|
||||||
khhaoLookupForm.username = khhaoSourceResponse.data.source.username || ''
|
|
||||||
khhaoLookupForm.password = khhaoSourceResponse.data.source.password || ''
|
|
||||||
khhaoLookupForm.maxCaptchaAttempts = khhaoSourceResponse.data.source.maxCaptchaAttempts || 3
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
errorMessage.value = error instanceof Error ? error.message : '读取履约配置失败'
|
errorMessage.value = error instanceof Error ? error.message : '读取履约配置失败'
|
||||||
} finally {
|
} finally {
|
||||||
@@ -366,29 +346,6 @@ function isLookupProductImported(lineId: string) {
|
|||||||
return importedLookupLineIds.value.includes(lineId)
|
return importedLookupLineIds.value.includes(lineId)
|
||||||
}
|
}
|
||||||
|
|
||||||
function importKhhaoProduct(item: AdminKhhaoOrderPreview) {
|
|
||||||
importObservedProduct({
|
|
||||||
provider: item.provider || 'khhao',
|
|
||||||
platform: item.platform || 'unknown',
|
|
||||||
shopId: isKhhaoKuaishouBinding({ provider: item.provider || 'khhao', platform: item.platform || 'unknown' })
|
|
||||||
? (item.kuaishouShopId || resolveKuaishouShopByName(item.shopName)?.shopId || '')
|
|
||||||
: item.shopId,
|
|
||||||
shopName: item.shopName,
|
|
||||||
khhaoShopId: item.khhaoShopId || item.shopId,
|
|
||||||
externalSkuCode: item.skuCode,
|
|
||||||
externalItemId: item.itemId,
|
|
||||||
externalSkuName: item.itemTitle,
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!importedKhhaoOrderIds.value.includes(item.platformOrderId)) {
|
|
||||||
importedKhhaoOrderIds.value = [...importedKhhaoOrderIds.value, item.platformOrderId]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function isKhhaoProductImported(platformOrderId: string) {
|
|
||||||
return importedKhhaoOrderIds.value.includes(platformOrderId)
|
|
||||||
}
|
|
||||||
|
|
||||||
function normalizeBindingForSave(item: EditableBinding): SaveBindingPayload {
|
function normalizeBindingForSave(item: EditableBinding): SaveBindingPayload {
|
||||||
syncBindingShopSelection(item)
|
syncBindingShopSelection(item)
|
||||||
|
|
||||||
@@ -615,34 +572,6 @@ async function lookupOrderProducts() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function lookupKhhaoProducts() {
|
|
||||||
if (!khhaoLookupForm.username.trim() || !khhaoLookupForm.password.trim()) {
|
|
||||||
khhaoLookupErrorMessage.value = '请先填写 khhao 账号和密码'
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
khhaoLookupLoading.value = true
|
|
||||||
khhaoLookupErrorMessage.value = ''
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await queryAdminKhhaoOrders({
|
|
||||||
baseUrl: khhaoLookupForm.baseUrl.trim() || 'https://admin.khhao.com',
|
|
||||||
username: khhaoLookupForm.username.trim(),
|
|
||||||
password: khhaoLookupForm.password.trim(),
|
|
||||||
page: Number(khhaoLookupForm.page || 1),
|
|
||||||
limit: Number(khhaoLookupForm.limit || 10),
|
|
||||||
maxCaptchaAttempts: Number(khhaoLookupForm.maxCaptchaAttempts || 3),
|
|
||||||
})
|
|
||||||
khhaoLookupResults.value = response.data.items
|
|
||||||
importedKhhaoOrderIds.value = []
|
|
||||||
} catch (error) {
|
|
||||||
khhaoLookupResults.value = []
|
|
||||||
khhaoLookupErrorMessage.value = error instanceof Error ? error.message : 'khhao 订单查询失败'
|
|
||||||
} finally {
|
|
||||||
khhaoLookupLoading.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(loadConfigs)
|
onMounted(loadConfigs)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -938,113 +867,6 @@ onMounted(loadConfigs)
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="table-card table-card--dense">
|
|
||||||
<div class="section-title-row section-title-row--tight">
|
|
||||||
<div>
|
|
||||||
<h3>khhao 订单取样导入</h3>
|
|
||||||
<p>先用 khhao 账号查订单样本,再直接把商品导入成规则草稿,适合首批建规则。</p>
|
|
||||||
</div>
|
|
||||||
<span class="section-note-chip">khhao / kuaishou</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="khhao-toolbar">
|
|
||||||
<label class="field-block field-wide">
|
|
||||||
<span>Base URL</span>
|
|
||||||
<input v-model="khhaoLookupForm.baseUrl" class="text-input" placeholder="https://admin.khhao.com" />
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<label class="field-block">
|
|
||||||
<span>账号</span>
|
|
||||||
<input v-model="khhaoLookupForm.username" class="text-input" placeholder="khhao 登录账号" />
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<label class="field-block">
|
|
||||||
<span>密码</span>
|
|
||||||
<input v-model="khhaoLookupForm.password" class="text-input" type="password" placeholder="khhao 登录密码" />
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<label class="field-block">
|
|
||||||
<span>页码</span>
|
|
||||||
<input v-model.number="khhaoLookupForm.page" class="text-input" type="number" min="1" />
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<label class="field-block">
|
|
||||||
<span>每页条数</span>
|
|
||||||
<input v-model.number="khhaoLookupForm.limit" class="text-input" type="number" min="1" max="50" />
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<label class="field-block">
|
|
||||||
<span>验证码重试</span>
|
|
||||||
<input v-model.number="khhaoLookupForm.maxCaptchaAttempts" class="text-input" type="number" min="1" max="5" />
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<div class="field-block field-block--action">
|
|
||||||
<span>操作</span>
|
|
||||||
<el-button :loading="khhaoLookupLoading" round type="primary" @click="lookupKhhaoProducts">查询 khhao 订单</el-button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<p class="toolbar-hint">这里直接读取 khhao 订单列表,不写库,只用于导入规则草稿。</p>
|
|
||||||
<p v-if="khhaoLookupErrorMessage" class="error-copy lookup-error">{{ khhaoLookupErrorMessage }}</p>
|
|
||||||
<div v-else-if="khhaoLookupLoading" class="empty-inline">正在查询 khhao 订单…</div>
|
|
||||||
|
|
||||||
<table v-else class="data-table">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>订单</th>
|
|
||||||
<th>店铺</th>
|
|
||||||
<th>商品</th>
|
|
||||||
<th>金额 / 状态</th>
|
|
||||||
<th>操作</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<tr v-for="item in khhaoLookupResults" :key="`${item.platformOrderId}:${item.skuCode}:${item.itemId}`">
|
|
||||||
<td>
|
|
||||||
<div class="cell-stack">
|
|
||||||
<strong>{{ item.platformOrderId }}</strong>
|
|
||||||
<span class="cell-subtle">{{ item.platformLabel || item.platform || '-' }}</span>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<div class="cell-stack">
|
|
||||||
<strong>{{ item.shopName || item.shopId || '-' }}</strong>
|
|
||||||
<span class="cell-subtle">khhao ID: {{ item.khhaoShopId || item.shopId || '-' }}</span>
|
|
||||||
<span v-if="item.kuaishouShopId" class="cell-subtle">快手 ID: {{ item.kuaishouShopId }}</span>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<div class="cell-stack">
|
|
||||||
<strong>{{ item.itemTitle || '-' }}</strong>
|
|
||||||
<span class="cell-subtle">SKU: {{ item.skuCode || '-' }}</span>
|
|
||||||
<span class="cell-subtle">ItemId: {{ item.itemId || '-' }}</span>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<div class="cell-stack">
|
|
||||||
<strong>{{ (item.totalAmountFen / 100).toFixed(2) }}</strong>
|
|
||||||
<span class="cell-subtle">{{ item.statusLabel || item.status || '-' }}</span>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<el-button
|
|
||||||
v-if="!isKhhaoProductImported(item.platformOrderId)"
|
|
||||||
link
|
|
||||||
type="primary"
|
|
||||||
@click="importKhhaoProduct(item)"
|
|
||||||
>
|
|
||||||
导入到规则
|
|
||||||
</el-button>
|
|
||||||
<span v-else class="cell-subtle">已导入草稿</span>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr v-if="khhaoLookupResults.length === 0">
|
|
||||||
<td colspan="5" class="empty-inline">还没有 khhao 查询结果。</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="table-card table-card--dense">
|
<section class="table-card table-card--dense">
|
||||||
<div class="section-title-row section-title-row--tight">
|
<div class="section-title-row section-title-row--tight">
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@@ -0,0 +1,135 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue'
|
||||||
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
|
|
||||||
|
import AdminFulfillmentBindingsView from '@/views/admin/AdminFulfillmentBindingsView.vue'
|
||||||
|
import AdminKuaishouCloudFulfillmentView from '@/views/admin/AdminKuaishouCloudFulfillmentView.vue'
|
||||||
|
|
||||||
|
type FulfillmentTabKey = 'legacy' | 'kuaishou-cloud'
|
||||||
|
|
||||||
|
const route = useRoute()
|
||||||
|
const router = useRouter()
|
||||||
|
const TAB_OPTIONS: Array<{ key: FulfillmentTabKey; label: string; desc: string }> = [
|
||||||
|
{
|
||||||
|
key: 'legacy',
|
||||||
|
label: '旧履约规则',
|
||||||
|
desc: '这里维护原有统一履约绑定,支持腾讯领取、人工发货,以及旧任务匹配逻辑。',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'kuaishou-cloud',
|
||||||
|
label: '快手 Cloud 新履约',
|
||||||
|
desc: '这里维护快手下单后走 cloudtentacles 绑定、发货、退号的新流程,不会覆盖旧规则。',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
const activeTab = computed<FulfillmentTabKey>({
|
||||||
|
get() {
|
||||||
|
const value = String(route.query.tab || '').trim()
|
||||||
|
return value === 'kuaishou-cloud' ? 'kuaishou-cloud' : 'legacy'
|
||||||
|
},
|
||||||
|
set(value) {
|
||||||
|
const next = value === 'kuaishou-cloud' ? 'kuaishou-cloud' : 'legacy'
|
||||||
|
void router.replace({
|
||||||
|
path: '/admin/platform-fulfillment',
|
||||||
|
query: next === 'legacy' ? {} : { tab: next },
|
||||||
|
})
|
||||||
|
},
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<section class="hub-panel">
|
||||||
|
<header class="hub-hero">
|
||||||
|
<div>
|
||||||
|
<h1>履约配置中心</h1>
|
||||||
|
<p>旧规则和快手 Cloud 新流程都放在这里维护,不再拆成两个侧边栏入口。</p>
|
||||||
|
</div>
|
||||||
|
<div class="hero-tags">
|
||||||
|
<span>一个入口</span>
|
||||||
|
<span>两套配置并存</span>
|
||||||
|
<span>互不覆盖</span>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<section class="hub-card">
|
||||||
|
<div class="tab-copy">
|
||||||
|
<h3>{{ TAB_OPTIONS.find((item) => item.key === activeTab)?.label }}</h3>
|
||||||
|
<p>{{ TAB_OPTIONS.find((item) => item.key === activeTab)?.desc }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-tabs v-model="activeTab" class="hub-tabs">
|
||||||
|
<el-tab-pane label="旧履约规则" name="legacy">
|
||||||
|
<AdminFulfillmentBindingsView />
|
||||||
|
</el-tab-pane>
|
||||||
|
<el-tab-pane label="快手 Cloud 新履约" name="kuaishou-cloud">
|
||||||
|
<AdminKuaishouCloudFulfillmentView />
|
||||||
|
</el-tab-pane>
|
||||||
|
</el-tabs>
|
||||||
|
</section>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.hub-panel {
|
||||||
|
display: grid;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hub-hero,
|
||||||
|
.hub-card {
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 24px;
|
||||||
|
border: 1px solid rgba(86, 108, 138, 0.12);
|
||||||
|
background: rgba(255, 255, 255, 0.94);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hub-hero {
|
||||||
|
display: grid;
|
||||||
|
gap: 14px;
|
||||||
|
background:
|
||||||
|
radial-gradient(circle at top right, rgba(26, 127, 255, 0.1), transparent 26%),
|
||||||
|
linear-gradient(135deg, rgba(255,255,255,.98), rgba(240,247,255,.96));
|
||||||
|
}
|
||||||
|
|
||||||
|
.hub-hero h1,
|
||||||
|
.tab-copy h3 {
|
||||||
|
margin: 0;
|
||||||
|
color: #17324d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hub-hero p,
|
||||||
|
.tab-copy p {
|
||||||
|
margin: 8px 0 0;
|
||||||
|
color: #64748b;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-tags {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-tags span {
|
||||||
|
padding: 8px 12px;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: rgba(23, 92, 211, 0.08);
|
||||||
|
color: #175cd3;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hub-card {
|
||||||
|
display: grid;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-copy {
|
||||||
|
display: grid;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.hub-tabs .el-tabs__content) {
|
||||||
|
padding-top: 12px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -40,7 +40,6 @@ const navItems = computed(() => {
|
|||||||
baseItems.splice(1, 0, { to: '/admin/users', label: '用户' })
|
baseItems.splice(1, 0, { to: '/admin/users', label: '用户' })
|
||||||
baseItems.push({ to: '/admin/platform-shops', label: '平台配置' })
|
baseItems.push({ to: '/admin/platform-shops', label: '平台配置' })
|
||||||
baseItems.push({ to: '/admin/platform-fulfillment', label: '履约配置' })
|
baseItems.push({ to: '/admin/platform-fulfillment', label: '履约配置' })
|
||||||
baseItems.push({ to: '/admin/platform-kuaishou-cloud-fulfillment', label: '新履约配置' })
|
|
||||||
baseItems.push({ to: '/admin/audit-logs', label: '审计' })
|
baseItems.push({ to: '/admin/audit-logs', label: '审计' })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1533,6 +1533,10 @@ onMounted(loadConfigs)
|
|||||||
<span class="meta-label">自动同步状态</span>
|
<span class="meta-label">自动同步状态</span>
|
||||||
<span>{{ khhaoAutoSyncState.lastRunStatus || 'idle' }}</span>
|
<span>{{ khhaoAutoSyncState.lastRunStatus || 'idle' }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="meta-line">
|
||||||
|
<span class="meta-label">当前节奏</span>
|
||||||
|
<span>{{ khhaoAutoSyncState.watchMode === 'boosted' ? '提频观察中(3-5 秒)' : '常规同步' }}</span>
|
||||||
|
</div>
|
||||||
<div class="meta-line">
|
<div class="meta-line">
|
||||||
<span class="meta-label">同步基线</span>
|
<span class="meta-label">同步基线</span>
|
||||||
<span>{{ formatAdminDateTime(khhaoAutoSyncState.syncFromCreatedAt) }}</span>
|
<span>{{ formatAdminDateTime(khhaoAutoSyncState.syncFromCreatedAt) }}</span>
|
||||||
@@ -1549,6 +1553,10 @@ onMounted(loadConfigs)
|
|||||||
<span class="meta-label">最近结果</span>
|
<span class="meta-label">最近结果</span>
|
||||||
<span>拉取 {{ khhaoAutoSyncState.fetchedCount }} 条,成功 {{ khhaoAutoSyncState.syncedCount }} 条,忽略 {{ khhaoAutoSyncState.ignoredCount }} 条</span>
|
<span>拉取 {{ khhaoAutoSyncState.fetchedCount }} 条,成功 {{ khhaoAutoSyncState.syncedCount }} 条,忽略 {{ khhaoAutoSyncState.ignoredCount }} 条</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div v-if="khhaoAutoSyncState.watchOrders.length > 0" class="meta-line">
|
||||||
|
<span class="meta-label">观察订单</span>
|
||||||
|
<span>{{ khhaoAutoSyncState.watchOrders.map((item) => `${item.shopName || item.shopId || '-'} / ${item.platformOrderId}`).join(';') }}</span>
|
||||||
|
</div>
|
||||||
<div v-if="khhaoAutoSyncState.lastErrorMessage" class="meta-line">
|
<div v-if="khhaoAutoSyncState.lastErrorMessage" class="meta-line">
|
||||||
<span class="meta-label">最近错误</span>
|
<span class="meta-label">最近错误</span>
|
||||||
<span class="error-copy">{{ khhaoAutoSyncState.lastErrorMessage }}</span>
|
<span class="error-copy">{{ khhaoAutoSyncState.lastErrorMessage }}</span>
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import {
|
|||||||
fetchAdminTaskDetail,
|
fetchAdminTaskDetail,
|
||||||
markAdminTaskManualReview,
|
markAdminTaskManualReview,
|
||||||
prepareAdminTaskKuaishouCloudFulfillment,
|
prepareAdminTaskKuaishouCloudFulfillment,
|
||||||
|
refreshAdminTaskKuaishouCloudRoleInfo,
|
||||||
regenerateAdminTaskClaimLink,
|
regenerateAdminTaskClaimLink,
|
||||||
releaseAdminTaskInventoryBinding,
|
releaseAdminTaskInventoryBinding,
|
||||||
returnNumberAdminTaskKuaishouCloudFulfillment,
|
returnNumberAdminTaskKuaishouCloudFulfillment,
|
||||||
@@ -40,7 +41,109 @@ const canManageTaskLifecycle = computed(() => hasAdminRole('operator'))
|
|||||||
const canCloseTasks = computed(() => hasAdminRole('support'))
|
const canCloseTasks = computed(() => hasAdminRole('support'))
|
||||||
const canViewSensitiveTaskData = computed(() => Boolean(detail.value?.operations.canViewSensitiveTaskData))
|
const canViewSensitiveTaskData = computed(() => Boolean(detail.value?.operations.canViewSensitiveTaskData))
|
||||||
const kuaishouCloudFlow = computed(() => detail.value?.kuaishouCloudFulfillment || null)
|
const kuaishouCloudFlow = computed(() => detail.value?.kuaishouCloudFulfillment || null)
|
||||||
|
const kuaishouCloudRole = computed(() => kuaishouCloudFlow.value?.role || null)
|
||||||
|
const kuaishouCloudCollapsePanels = ref(['claim', 'ticket', 'cloud'])
|
||||||
const isKuaishouCloudTask = computed(() => detail.value?.task.executorKey === 'kuaishou_ct_assisted')
|
const isKuaishouCloudTask = computed(() => detail.value?.task.executorKey === 'kuaishou_ct_assisted')
|
||||||
|
const summaryCards = computed(() => {
|
||||||
|
if (!detail.value) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
label: '任务状态',
|
||||||
|
value: detail.value.task.status || '-',
|
||||||
|
tone: 'blue',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '履约状态',
|
||||||
|
value: detail.value.task.deliveryStatus || '-',
|
||||||
|
tone: 'green',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '当前角色',
|
||||||
|
value: kuaishouCloudRole.value?.name || detail.value.task.roleName || '-',
|
||||||
|
tone: 'amber',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '角色 ID',
|
||||||
|
value: kuaishouCloudRole.value?.rid || detail.value.task.roleId || '-',
|
||||||
|
tone: 'slate',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
})
|
||||||
|
const kuaishouRoleInfoEntries = computed(() => {
|
||||||
|
const rawInfo = kuaishouCloudRole.value?.rawInfo
|
||||||
|
if (!rawInfo) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
return Object.entries(rawInfo)
|
||||||
|
.filter(([, value]) => value !== null && value !== undefined && String(value).trim() !== '')
|
||||||
|
.map(([key, value]) => ({
|
||||||
|
key,
|
||||||
|
label: formatKuaishouRoleInfoLabel(key),
|
||||||
|
value: typeof value === 'object' ? JSON.stringify(value) : String(value),
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
const kuaishouCloudChecklist = computed(() => {
|
||||||
|
const flow = kuaishouCloudFlow.value
|
||||||
|
const role = kuaishouCloudRole.value
|
||||||
|
if (!flow) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
key: 'ticket',
|
||||||
|
label: '核销码校验',
|
||||||
|
status: flow.ticket.status || 'pending',
|
||||||
|
detail: flow.ticket.verifiedAt
|
||||||
|
? `已于 ${formatAdminDateTime(flow.ticket.verifiedAt)} 校验`
|
||||||
|
: '等待客户提交并校验核销码',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'bind',
|
||||||
|
label: '绑定资源',
|
||||||
|
status: flow.binding.prepareStatus || 'pending',
|
||||||
|
detail: flow.binding.bindPreparedAt
|
||||||
|
? `绑定资源已准备,VN ${flow.binding.vnId || '-'}`
|
||||||
|
: '等待准备 Cloud 绑定资源',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'role',
|
||||||
|
label: '角色识别',
|
||||||
|
status: role?.status || 'pending',
|
||||||
|
detail: role?.name || role?.rid
|
||||||
|
? `${role?.name || '-'} / ${role?.rid || '-'}`
|
||||||
|
: '客户绑定后刷新角色信息',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'dispatch',
|
||||||
|
label: '发货执行',
|
||||||
|
status: flow.dispatch.status || 'pending',
|
||||||
|
detail: flow.dispatch.dispatchAt
|
||||||
|
? `已于 ${formatAdminDateTime(flow.dispatch.dispatchAt)} 发货`
|
||||||
|
: '等待客服确认绑定并发货',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'return',
|
||||||
|
label: '退还号码',
|
||||||
|
status: flow.returnNumber.status || 'pending',
|
||||||
|
detail: flow.returnNumber.returnedAt
|
||||||
|
? `已于 ${formatAdminDateTime(flow.returnNumber.returnedAt)} 退号`
|
||||||
|
: '发货后执行退还号码',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'consume',
|
||||||
|
label: '快手核销',
|
||||||
|
status: flow.consume.status || 'pending',
|
||||||
|
detail: flow.consume.consumedAt
|
||||||
|
? `已于 ${formatAdminDateTime(flow.consume.consumedAt)} 完成核销`
|
||||||
|
: (flow.consume.errorMessage || '等待退号后核销收口'),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
})
|
||||||
const claimUrl = computed(() => {
|
const claimUrl = computed(() => {
|
||||||
const tokenStatus = String(detail.value?.claimToken?.status || '').trim()
|
const tokenStatus = String(detail.value?.claimToken?.status || '').trim()
|
||||||
const taskStatus = String(detail.value?.task.status || '').trim()
|
const taskStatus = String(detail.value?.task.status || '').trim()
|
||||||
@@ -184,6 +287,18 @@ async function submitKuaishouCloudDispatch() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function submitKuaishouCloudRefreshRoleInfo() {
|
||||||
|
if (!detail.value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
await runAction(
|
||||||
|
() => refreshAdminTaskKuaishouCloudRoleInfo(detail.value!.task.taskId),
|
||||||
|
'角色信息已刷新',
|
||||||
|
`确认刷新任务 ${detail.value.task.taskNo} 当前云绑定角色信息吗?系统会重新查询 cloudtentacles 的绑定结果。`,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
async function submitKuaishouCloudReturnNumber() {
|
async function submitKuaishouCloudReturnNumber() {
|
||||||
if (!detail.value) {
|
if (!detail.value) {
|
||||||
return
|
return
|
||||||
@@ -321,6 +436,19 @@ function formatAutoDeliveryReason(value: string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function formatKuaishouRoleInfoLabel(value: string) {
|
||||||
|
const normalized = String(value || '').trim()
|
||||||
|
|
||||||
|
switch (normalized) {
|
||||||
|
case 'name':
|
||||||
|
return '角色名称'
|
||||||
|
case 'rid':
|
||||||
|
return '角色 ID'
|
||||||
|
default:
|
||||||
|
return normalized || '-'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function resolveTaskAutoDeliveryStatus(taskDetail: AdminTaskDetail) {
|
function resolveTaskAutoDeliveryStatus(taskDetail: AdminTaskDetail) {
|
||||||
if (taskDetail.task.agisoAutoDelivery?.status) {
|
if (taskDetail.task.agisoAutoDelivery?.status) {
|
||||||
return taskDetail.task.agisoAutoDelivery.status
|
return taskDetail.task.agisoAutoDelivery.status
|
||||||
@@ -362,7 +490,42 @@ onBeforeUnmount(clearScreenshotPreview)
|
|||||||
<div v-if="loading" class="empty-block">任务详情加载中</div>
|
<div v-if="loading" class="empty-block">任务详情加载中</div>
|
||||||
|
|
||||||
<template v-else-if="detail">
|
<template v-else-if="detail">
|
||||||
<section class="info-card">
|
<section v-if="isKuaishouCloudTask && kuaishouCloudFlow" class="hero-card">
|
||||||
|
<div class="hero-copy">
|
||||||
|
<span class="hero-eyebrow">{{ detail.task.executorKey || 'task' }}</span>
|
||||||
|
<h2>{{ detail.task.taskNo }}</h2>
|
||||||
|
<p>
|
||||||
|
订单 {{ detail.order?.platformOrderId || '-' }} ·
|
||||||
|
商品 {{ detail.orderItem?.skuName || detail.orderItem?.skuCode || '-' }} ·
|
||||||
|
店铺 {{ detail.order?.shopName || detail.order?.shopId || '-' }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="hero-meta-grid">
|
||||||
|
<article
|
||||||
|
v-for="card in summaryCards"
|
||||||
|
:key="card.label"
|
||||||
|
:class="['hero-metric', `hero-metric--${card.tone}`]"
|
||||||
|
>
|
||||||
|
<span>{{ card.label }}</span>
|
||||||
|
<strong>{{ card.value }}</strong>
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
|
<div class="hero-inline">
|
||||||
|
<span>系统绑定 <AdminStatusTag :status="detail.task.systemBindingStatus" /></span>
|
||||||
|
<span>完整绑定 <AdminStatusTag :status="detail.task.userBindingStatus" /></span>
|
||||||
|
<span v-if="detail.task.lastError">最近异常 {{ detail.task.lastError }}</span>
|
||||||
|
</div>
|
||||||
|
<div v-if="claimUrl" class="hero-link-row">
|
||||||
|
<span>客户领取页</span>
|
||||||
|
<a class="claim-link" :href="claimUrl" target="_blank" rel="noreferrer">{{ claimUrl }}</a>
|
||||||
|
</div>
|
||||||
|
<div v-else-if="claimLinkInvalid" class="hero-link-row">
|
||||||
|
<span>客户领取页</span>
|
||||||
|
<strong>已失效</strong>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section v-else class="info-card">
|
||||||
<h2>{{ detail.task.taskNo }}</h2>
|
<h2>{{ detail.task.taskNo }}</h2>
|
||||||
<p>状态:<AdminStatusTag :status="detail.task.status" /> · 订单:{{ detail.order?.platformOrderId || '-' }}</p>
|
<p>状态:<AdminStatusTag :status="detail.task.status" /> · 订单:{{ detail.order?.platformOrderId || '-' }}</p>
|
||||||
<p>执行器:{{ detail.task.executorKey || '-' }} · 履约:<AdminStatusTag :status="detail.task.deliveryStatus" /></p>
|
<p>执行器:{{ detail.task.executorKey || '-' }} · 履约:<AdminStatusTag :status="detail.task.deliveryStatus" /></p>
|
||||||
@@ -489,8 +652,8 @@ onBeforeUnmount(clearScreenshotPreview)
|
|||||||
</el-button>
|
</el-button>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="table-card">
|
<section v-if="!isKuaishouCloudTask" class="table-card">
|
||||||
<h3>任务信息</h3>
|
<h3>任务信息</h3>
|
||||||
<table class="data-table">
|
<table class="data-table">
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr><th>商品</th><td>{{ detail.orderItem?.skuName || '-' }}</td></tr>
|
<tr><th>商品</th><td>{{ detail.orderItem?.skuName || '-' }}</td></tr>
|
||||||
@@ -521,15 +684,23 @@ onBeforeUnmount(clearScreenshotPreview)
|
|||||||
<tr><th>截图路径</th><td>{{ detail.task.screenshotPath || '-' }}</td></tr>
|
<tr><th>截图路径</th><td>{{ detail.task.screenshotPath || '-' }}</td></tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section v-if="isKuaishouCloudTask && kuaishouCloudFlow" class="table-card flow-card">
|
<section v-if="isKuaishouCloudTask && kuaishouCloudFlow" class="table-card flow-card">
|
||||||
<div class="flow-card-head">
|
<div class="flow-card-head">
|
||||||
<div>
|
<div>
|
||||||
<h3>快手 Cloud 履约面板</h3>
|
<h3>快手 Cloud 履约面板</h3>
|
||||||
<p class="section-copy">这里集中处理绑定链接、客户卡券号、发货动作与退号收口。</p>
|
<p class="section-copy">这是一套独立的快手 Cloud 任务面板,优先展示角色信息与当前履约推进点,次级明细可按需展开。</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="flow-head-actions">
|
<div class="flow-head-actions">
|
||||||
|
<el-button
|
||||||
|
v-if="detail.operations.canRefreshKuaishouCloudRoleInfo"
|
||||||
|
:loading="actionLoading"
|
||||||
|
round
|
||||||
|
@click="submitKuaishouCloudRefreshRoleInfo"
|
||||||
|
>
|
||||||
|
刷新角色信息
|
||||||
|
</el-button>
|
||||||
<el-button
|
<el-button
|
||||||
v-if="kuaishouCloudFlow.binding.bindUrl"
|
v-if="kuaishouCloudFlow.binding.bindUrl"
|
||||||
round
|
round
|
||||||
@@ -576,80 +747,126 @@ onBeforeUnmount(clearScreenshotPreview)
|
|||||||
</article>
|
</article>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flow-grid">
|
<div class="kuaishou-focus-grid">
|
||||||
<article class="flow-block">
|
<article class="flow-block flow-block--spotlight">
|
||||||
<h4>客户绑定</h4>
|
<div class="spotlight-head">
|
||||||
<p>客户下单后需要先完成外部绑定,客服只需要把链接发给客户,并在完成后点一次发货按钮。</p>
|
<div>
|
||||||
<div class="flow-meta-list">
|
<h4>角色信息</h4>
|
||||||
<div><span>绑定链接</span><strong>{{ kuaishouCloudFlow.binding.bindUrl || '-' }}</strong></div>
|
<p class="flow-copy">角色信息来自 cloudtentacles 的 `vn/bind_info`,客服可在客户绑定后手动刷新。</p>
|
||||||
<div><span>准备时间</span><strong>{{ formatAdminDateTime(kuaishouCloudFlow.binding.bindPreparedAt) }}</strong></div>
|
</div>
|
||||||
<div><span>虚拟号 ID</span><strong>{{ kuaishouCloudFlow.binding.vnId || '-' }}</strong></div>
|
<div class="spotlight-actions">
|
||||||
<div><span>虚拟号码</span><strong>{{ kuaishouCloudFlow.binding.vnPhone || '-' }}</strong></div>
|
<AdminStatusTag :status="kuaishouCloudRole?.status || 'pending'" />
|
||||||
|
<el-button
|
||||||
|
v-if="detail.operations.canRefreshKuaishouCloudRoleInfo"
|
||||||
|
:loading="actionLoading"
|
||||||
|
round
|
||||||
|
size="small"
|
||||||
|
@click="submitKuaishouCloudRefreshRoleInfo"
|
||||||
|
>
|
||||||
|
刷新
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="role-hero">
|
||||||
|
<div>
|
||||||
|
<span>角色名</span>
|
||||||
|
<strong>{{ kuaishouCloudRole?.name || detail.task.roleName || '等待识别' }}</strong>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span>角色 ID</span>
|
||||||
|
<strong>{{ kuaishouCloudRole?.rid || detail.task.roleId || '等待识别' }}</strong>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span>最近刷新</span>
|
||||||
|
<strong>{{ formatAdminDateTime(kuaishouCloudRole?.refreshedAt) }}</strong>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p v-if="kuaishouCloudRole?.errorMessage" class="role-warning">{{ kuaishouCloudRole.errorMessage }}</p>
|
||||||
|
<div v-if="kuaishouRoleInfoEntries.length > 0" class="flow-meta-list flow-meta-list--dense">
|
||||||
|
<div v-for="entry in kuaishouRoleInfoEntries" :key="entry.key">
|
||||||
|
<span>{{ entry.label }}</span>
|
||||||
|
<strong>{{ entry.value }}</strong>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</article>
|
</article>
|
||||||
|
|
||||||
<article class="flow-block">
|
<article class="flow-block flow-block--checklist">
|
||||||
<h4>cloud 资源</h4>
|
<h4>流程检查</h4>
|
||||||
<div class="flow-meta-list">
|
<p class="flow-copy">客服可以先看这里,快速判断卡在哪一步,再决定是刷新角色、准备资源、发货还是退号。</p>
|
||||||
|
<div class="checklist-grid">
|
||||||
|
<div v-for="item in kuaishouCloudChecklist" :key="item.key" class="checklist-item">
|
||||||
|
<div class="checklist-item-head">
|
||||||
|
<span>{{ item.label }}</span>
|
||||||
|
<AdminStatusTag :status="item.status" />
|
||||||
|
</div>
|
||||||
|
<strong>{{ item.detail }}</strong>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-collapse v-model="kuaishouCloudCollapsePanels" class="flow-collapse">
|
||||||
|
<el-collapse-item name="claim" title="客户领取与绑定">
|
||||||
|
<div class="flow-meta-list flow-meta-list--two-column">
|
||||||
|
<div><span>客户链接</span><strong>{{ claimUrl || '-' }}</strong></div>
|
||||||
|
<div><span>绑定链接</span><strong>{{ kuaishouCloudFlow.binding.bindUrl || '-' }}</strong></div>
|
||||||
|
<div><span>Token 状态</span><strong>{{ detail.claimToken?.status || '-' }}</strong></div>
|
||||||
|
<div><span>准备时间</span><strong>{{ formatAdminDateTime(kuaishouCloudFlow.binding.bindPreparedAt) }}</strong></div>
|
||||||
|
<div><span>首次提交</span><strong>{{ formatAdminDateTime(kuaishouCloudFlow.ticket.capturedAt) }}</strong></div>
|
||||||
|
<div><span>校验时间</span><strong>{{ formatAdminDateTime(kuaishouCloudFlow.ticket.verifiedAt) }}</strong></div>
|
||||||
|
<div><span>虚拟号 ID</span><strong>{{ kuaishouCloudFlow.binding.vnId || '-' }}</strong></div>
|
||||||
|
<div><span>虚拟号码</span><strong>{{ kuaishouCloudFlow.binding.vnPhone || '-' }}</strong></div>
|
||||||
|
</div>
|
||||||
|
<p class="manual-hint">客户会先在领取页查看图文指引并提交核销码,只有校验通过后才会自动跳转绑定链接。</p>
|
||||||
|
</el-collapse-item>
|
||||||
|
|
||||||
|
<el-collapse-item name="ticket" title="核销与商品信息">
|
||||||
|
<div class="flow-meta-list flow-meta-list--two-column">
|
||||||
|
<div><span>核销码</span><strong>{{ kuaishouCloudFlow.ticket.code || '-' }}</strong></div>
|
||||||
|
<div><span>校验状态</span><strong>{{ kuaishouCloudFlow.ticket.status || '-' }}</strong></div>
|
||||||
|
<div><span>商品标题</span><strong>{{ kuaishouCloudFlow.ticket.goodsTitle || '-' }}</strong></div>
|
||||||
|
<div><span>剩余次数</span><strong>{{ kuaishouCloudFlow.ticket.leftCount || 0 }}</strong></div>
|
||||||
|
</div>
|
||||||
|
<p class="manual-hint">“确认绑定完成并发货”已经合并为一个按钮,只有核销码通过校验后才能继续。</p>
|
||||||
|
</el-collapse-item>
|
||||||
|
|
||||||
|
<el-collapse-item name="cloud" title="Cloud 资源与发货">
|
||||||
|
<div class="flow-meta-list flow-meta-list--two-column">
|
||||||
<div><span>cloudSourceKey</span><strong>{{ kuaishouCloudFlow.binding.cloudSourceKey || '-' }}</strong></div>
|
<div><span>cloudSourceKey</span><strong>{{ kuaishouCloudFlow.binding.cloudSourceKey || '-' }}</strong></div>
|
||||||
<div><span>cloud SKU</span><strong>{{ kuaishouCloudFlow.binding.skuId ? `${kuaishouCloudFlow.binding.skuId} / ${kuaishouCloudFlow.binding.skuName || '-'}` : '-' }}</strong></div>
|
<div><span>cloud SKU</span><strong>{{ kuaishouCloudFlow.binding.skuId ? `${kuaishouCloudFlow.binding.skuId} / ${kuaishouCloudFlow.binding.skuName || '-'}` : '-' }}</strong></div>
|
||||||
<div><span>VN Key</span><strong>{{ kuaishouCloudFlow.binding.vnKey || '-' }}</strong></div>
|
<div><span>VN Key</span><strong>{{ kuaishouCloudFlow.binding.vnKey || '-' }}</strong></div>
|
||||||
<div><span>自动购买</span><strong>{{ kuaishouCloudFlow.purchase.autoBuyEnabled ? '开启' : '关闭' }}</strong></div>
|
<div><span>自动购买</span><strong>{{ kuaishouCloudFlow.purchase.autoBuyEnabled ? '开启' : '关闭' }}</strong></div>
|
||||||
<div><span>背包命中</span><strong>{{ kuaishouCloudFlow.purchase.usedKnapsack ? '是' : '否' }}</strong></div>
|
<div><span>背包命中</span><strong>{{ kuaishouCloudFlow.purchase.usedKnapsack ? '是' : '否' }}</strong></div>
|
||||||
<div><span>触发购买</span><strong>{{ kuaishouCloudFlow.purchase.purchaseTriggered ? '是' : '否' }}</strong></div>
|
<div><span>触发购买</span><strong>{{ kuaishouCloudFlow.purchase.purchaseTriggered ? '是' : '否' }}</strong></div>
|
||||||
</div>
|
<div><span>购买时间</span><strong>{{ formatAdminDateTime(kuaishouCloudFlow.purchase.purchaseAt) }}</strong></div>
|
||||||
</article>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flow-grid">
|
|
||||||
<article class="flow-block">
|
|
||||||
<h4>客户领取页</h4>
|
|
||||||
<div class="flow-meta-list">
|
|
||||||
<div><span>客户链接</span><strong>{{ claimUrl || '-' }}</strong></div>
|
|
||||||
<div><span>Token 状态</span><strong>{{ detail.claimToken?.status || '-' }}</strong></div>
|
|
||||||
<div><span>首次提交</span><strong>{{ formatAdminDateTime(kuaishouCloudFlow.ticket.capturedAt) }}</strong></div>
|
|
||||||
<div><span>校验时间</span><strong>{{ formatAdminDateTime(kuaishouCloudFlow.ticket.verifiedAt) }}</strong></div>
|
|
||||||
</div>
|
|
||||||
<p class="manual-hint">客户会先在这个页面查看图文指引并提交核销码,校验通过后才会自动跳转绑定链接。</p>
|
|
||||||
</article>
|
|
||||||
|
|
||||||
<article class="flow-block">
|
|
||||||
<h4>核销码状态</h4>
|
|
||||||
<div class="flow-meta-list">
|
|
||||||
<div><span>核销码</span><strong>{{ kuaishouCloudFlow.ticket.code || '-' }}</strong></div>
|
|
||||||
<div><span>校验状态</span><strong>{{ kuaishouCloudFlow.ticket.status || '-' }}</strong></div>
|
|
||||||
<div><span>商品标题</span><strong>{{ kuaishouCloudFlow.ticket.goodsTitle || '-' }}</strong></div>
|
|
||||||
<div><span>剩余次数</span><strong>{{ kuaishouCloudFlow.ticket.leftCount || 0 }}</strong></div>
|
|
||||||
</div>
|
|
||||||
<p class="manual-hint">“确认绑定完成并发货”已经合并为一个按钮,只有客户提交并校验过核销码后才能继续。</p>
|
|
||||||
</article>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flow-grid">
|
|
||||||
<article class="flow-block">
|
|
||||||
<h4>发货结果</h4>
|
|
||||||
<div class="flow-meta-list">
|
|
||||||
<div><span>最近卡券号</span><strong>{{ kuaishouCloudFlow.ticket.code || '-' }}</strong></div>
|
|
||||||
<div><span>记录时间</span><strong>{{ formatAdminDateTime(kuaishouCloudFlow.ticket.capturedAt) }}</strong></div>
|
|
||||||
<div><span>发货时间</span><strong>{{ formatAdminDateTime(kuaishouCloudFlow.dispatch.dispatchAt) }}</strong></div>
|
<div><span>发货时间</span><strong>{{ formatAdminDateTime(kuaishouCloudFlow.dispatch.dispatchAt) }}</strong></div>
|
||||||
<div><span>发送类型</span><strong>{{ kuaishouCloudFlow.dispatch.sendType || '-' }}</strong></div>
|
<div><span>发送类型</span><strong>{{ kuaishouCloudFlow.dispatch.sendType || '-' }}</strong></div>
|
||||||
<div><span>结果说明</span><strong>{{ kuaishouCloudFlow.dispatch.note || detail.task.resultMessage || '-' }}</strong></div>
|
<div><span>结果说明</span><strong>{{ kuaishouCloudFlow.dispatch.note || detail.task.resultMessage || '-' }}</strong></div>
|
||||||
</div>
|
</div>
|
||||||
</article>
|
</el-collapse-item>
|
||||||
|
|
||||||
<article class="flow-block">
|
<el-collapse-item name="closure" title="流程收口">
|
||||||
<h4>流程收口</h4>
|
<div class="flow-meta-list flow-meta-list--two-column">
|
||||||
<div class="flow-meta-list">
|
|
||||||
<div><span>退号时间</span><strong>{{ formatAdminDateTime(kuaishouCloudFlow.returnNumber.returnedAt) }}</strong></div>
|
<div><span>退号时间</span><strong>{{ formatAdminDateTime(kuaishouCloudFlow.returnNumber.returnedAt) }}</strong></div>
|
||||||
<div><span>自动退号</span><strong>{{ kuaishouCloudFlow.returnNumber.autoReturnEnabled ? '开启' : '关闭' }}</strong></div>
|
<div><span>自动退号</span><strong>{{ kuaishouCloudFlow.returnNumber.autoReturnEnabled ? '开启' : '关闭' }}</strong></div>
|
||||||
<div><span>自动核销</span><strong>{{ kuaishouCloudFlow.consume.autoConsumeEnabled ? '开启' : '关闭' }}</strong></div>
|
<div><span>自动核销</span><strong>{{ kuaishouCloudFlow.consume.autoConsumeEnabled ? '开启' : '关闭' }}</strong></div>
|
||||||
<div><span>核销店铺</span><strong>{{ kuaishouCloudFlow.consume.shopId || '-' }}</strong></div>
|
<div><span>核销店铺</span><strong>{{ kuaishouCloudFlow.consume.shopId || '-' }}</strong></div>
|
||||||
|
<div><span>核销店铺名</span><strong>{{ kuaishouCloudFlow.consume.shopName || '-' }}</strong></div>
|
||||||
<div><span>核销时间</span><strong>{{ formatAdminDateTime(kuaishouCloudFlow.consume.consumedAt) }}</strong></div>
|
<div><span>核销时间</span><strong>{{ formatAdminDateTime(kuaishouCloudFlow.consume.consumedAt) }}</strong></div>
|
||||||
<div><span>核销异常</span><strong>{{ kuaishouCloudFlow.consume.errorMessage || '-' }}</strong></div>
|
<div><span>核销异常</span><strong>{{ kuaishouCloudFlow.consume.errorMessage || '-' }}</strong></div>
|
||||||
<div><span>备注</span><strong>{{ kuaishouCloudFlow.notes || '-' }}</strong></div>
|
<div><span>备注</span><strong>{{ kuaishouCloudFlow.notes || '-' }}</strong></div>
|
||||||
</div>
|
</div>
|
||||||
</article>
|
</el-collapse-item>
|
||||||
</div>
|
|
||||||
|
<el-collapse-item v-if="kuaishouRoleInfoEntries.length > 0" name="raw" title="角色原始字段">
|
||||||
|
<div class="flow-meta-list flow-meta-list--two-column">
|
||||||
|
<div v-for="entry in kuaishouRoleInfoEntries" :key="`raw-${entry.key}`">
|
||||||
|
<span>{{ entry.label }}</span>
|
||||||
|
<strong>{{ entry.value }}</strong>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-collapse-item>
|
||||||
|
</el-collapse>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section
|
<section
|
||||||
@@ -746,7 +963,7 @@ onBeforeUnmount(clearScreenshotPreview)
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="table-card">
|
<section v-if="!isKuaishouCloudTask" class="table-card">
|
||||||
<h3>库存绑定</h3>
|
<h3>库存绑定</h3>
|
||||||
<p class="section-copy">这里展示任务当前和历史绑定过的库存项,便于核对多库存任务的真实履约轨迹。</p>
|
<p class="section-copy">这里展示任务当前和历史绑定过的库存项,便于核对多库存任务的真实履约轨迹。</p>
|
||||||
<table class="data-table">
|
<table class="data-table">
|
||||||
@@ -889,12 +1106,81 @@ onBeforeUnmount(clearScreenshotPreview)
|
|||||||
.panel-header h1 { margin: 0; color: #1d3555; }
|
.panel-header h1 { margin: 0; color: #1d3555; }
|
||||||
.panel-header p { margin: 8px 0 0; color: #64748b; }
|
.panel-header p { margin: 8px 0 0; color: #64748b; }
|
||||||
.claim-link { color: #175cd3; text-decoration: none; word-break: break-all; }
|
.claim-link { color: #175cd3; text-decoration: none; word-break: break-all; }
|
||||||
.info-card,.table-card,.empty-block,.error-copy {
|
.info-card,.hero-card,.table-card,.empty-block,.error-copy {
|
||||||
padding: 18px; border-radius: 20px; background: rgba(255,255,255,.94); border: 1px solid rgba(86,108,138,.1);
|
padding: 18px; border-radius: 20px; background: rgba(255,255,255,.94); border: 1px solid rgba(86,108,138,.1);
|
||||||
}
|
}
|
||||||
|
.info-card p {
|
||||||
|
margin: 10px 0 0;
|
||||||
|
color: #334155;
|
||||||
|
}
|
||||||
|
.hero-card {
|
||||||
|
display: grid;
|
||||||
|
gap: 16px;
|
||||||
|
background:
|
||||||
|
radial-gradient(circle at top right, rgba(14, 165, 233, 0.16), transparent 26%),
|
||||||
|
linear-gradient(135deg, rgba(255,255,255,.98), rgba(239,246,255,.96));
|
||||||
|
}
|
||||||
|
.hero-copy {
|
||||||
|
display: grid;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
.hero-copy h2 {
|
||||||
|
margin: 0;
|
||||||
|
color: #0f172a;
|
||||||
|
font-size: 30px;
|
||||||
|
line-height: 1.1;
|
||||||
|
}
|
||||||
|
.hero-copy p,
|
||||||
|
.hero-inline span,
|
||||||
|
.hero-link-row span {
|
||||||
|
color: #475467;
|
||||||
|
}
|
||||||
|
.hero-eyebrow {
|
||||||
|
color: #175cd3;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: .08em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
.hero-meta-grid,
|
||||||
|
.overview-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
.hero-metric {
|
||||||
|
padding: 14px 16px;
|
||||||
|
border-radius: 18px;
|
||||||
|
border: 1px solid rgba(86,108,138,.1);
|
||||||
|
background: rgba(255,255,255,.82);
|
||||||
|
}
|
||||||
|
.hero-metric span {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
color: #64748b;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
.hero-metric strong {
|
||||||
|
color: #0f172a;
|
||||||
|
font-size: 18px;
|
||||||
|
line-height: 1.3;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
.hero-metric--blue { box-shadow: inset 0 0 0 1px rgba(59,130,246,.08); }
|
||||||
|
.hero-metric--green { box-shadow: inset 0 0 0 1px rgba(16,185,129,.08); }
|
||||||
|
.hero-metric--amber { box-shadow: inset 0 0 0 1px rgba(245,158,11,.08); }
|
||||||
|
.hero-metric--slate { box-shadow: inset 0 0 0 1px rgba(100,116,139,.08); }
|
||||||
|
.hero-inline,
|
||||||
|
.hero-link-row {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 12px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
.error-copy { color: #b42318; }
|
.error-copy { color: #b42318; }
|
||||||
.action-row { display: flex; flex-wrap: wrap; gap: 12px; }
|
.action-row { display: flex; flex-wrap: wrap; gap: 12px; }
|
||||||
.table-card h3 { margin: 0 0 12px; color: #1d3555; }
|
.table-card h3 { margin: 0 0 12px; color: #1d3555; }
|
||||||
|
.compact-card { padding: 18px; }
|
||||||
.section-copy { margin: 0 0 12px; color: #64748b; }
|
.section-copy { margin: 0 0 12px; color: #64748b; }
|
||||||
.auto-delivery-tag-line { display: flex; align-items: center; }
|
.auto-delivery-tag-line { display: flex; align-items: center; }
|
||||||
.auto-delivery-note {
|
.auto-delivery-note {
|
||||||
@@ -988,6 +1274,7 @@ onBeforeUnmount(clearScreenshotPreview)
|
|||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
}
|
}
|
||||||
|
.kuaishou-focus-grid,
|
||||||
.flow-grid {
|
.flow-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
|
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
|
||||||
@@ -1000,6 +1287,11 @@ onBeforeUnmount(clearScreenshotPreview)
|
|||||||
border: 1px solid rgba(86,108,138,.1);
|
border: 1px solid rgba(86,108,138,.1);
|
||||||
background: rgba(248,250,252,.9);
|
background: rgba(248,250,252,.9);
|
||||||
}
|
}
|
||||||
|
.flow-block--spotlight {
|
||||||
|
background:
|
||||||
|
linear-gradient(160deg, rgba(255,255,255,.98), rgba(239,246,255,.95)),
|
||||||
|
rgba(248,250,252,.9);
|
||||||
|
}
|
||||||
.flow-block h4 {
|
.flow-block h4 {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
color: #1d3555;
|
color: #1d3555;
|
||||||
@@ -1026,6 +1318,89 @@ onBeforeUnmount(clearScreenshotPreview)
|
|||||||
color: #1d3555;
|
color: #1d3555;
|
||||||
word-break: break-all;
|
word-break: break-all;
|
||||||
}
|
}
|
||||||
|
.flow-meta-list--dense {
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
|
||||||
|
}
|
||||||
|
.flow-meta-list--two-column {
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
|
||||||
|
}
|
||||||
|
.spotlight-head {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
.spotlight-actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
.flow-copy {
|
||||||
|
margin: 8px 0 0;
|
||||||
|
color: #64748b;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
.flow-collapse {
|
||||||
|
display: grid;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
.checklist-grid {
|
||||||
|
display: grid;
|
||||||
|
gap: 12px;
|
||||||
|
margin-top: 16px;
|
||||||
|
}
|
||||||
|
.checklist-item {
|
||||||
|
padding: 14px 16px;
|
||||||
|
border-radius: 16px;
|
||||||
|
background: rgba(255,255,255,.84);
|
||||||
|
border: 1px solid rgba(86,108,138,.08);
|
||||||
|
}
|
||||||
|
.checklist-item-head {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
.checklist-item-head span {
|
||||||
|
color: #64748b;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
.checklist-item strong {
|
||||||
|
color: #0f172a;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
.role-hero {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
|
||||||
|
gap: 12px;
|
||||||
|
margin-top: 16px;
|
||||||
|
}
|
||||||
|
.role-hero div {
|
||||||
|
padding: 14px 16px;
|
||||||
|
border-radius: 16px;
|
||||||
|
background: rgba(255,255,255,.88);
|
||||||
|
border: 1px solid rgba(86,108,138,.08);
|
||||||
|
}
|
||||||
|
.role-hero span {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
color: #64748b;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
.role-hero strong {
|
||||||
|
color: #0f172a;
|
||||||
|
font-size: 18px;
|
||||||
|
line-height: 1.35;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
.role-warning {
|
||||||
|
margin: 14px 0 0;
|
||||||
|
padding: 12px 14px;
|
||||||
|
border-radius: 14px;
|
||||||
|
background: rgba(255, 244, 229, 0.9);
|
||||||
|
color: #b54708;
|
||||||
|
}
|
||||||
.event-copy { color: #334155; }
|
.event-copy { color: #334155; }
|
||||||
.event-time { margin-top: 6px; color: #64748b; font-size: 12px; }
|
.event-time { margin-top: 6px; color: #64748b; font-size: 12px; }
|
||||||
.screenshot-link { display: inline-block; margin-bottom: 12px; color: #175cd3; text-decoration: none; }
|
.screenshot-link { display: inline-block; margin-bottom: 12px; color: #175cd3; text-decoration: none; }
|
||||||
@@ -1039,8 +1414,13 @@ onBeforeUnmount(clearScreenshotPreview)
|
|||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 720px) {
|
@media (max-width: 720px) {
|
||||||
|
.hero-inline,
|
||||||
|
.hero-link-row,
|
||||||
.flow-card-head,
|
.flow-card-head,
|
||||||
.flow-head-actions {
|
.flow-head-actions,
|
||||||
|
.spotlight-head,
|
||||||
|
.spotlight-actions,
|
||||||
|
.checklist-item-head {
|
||||||
display: grid;
|
display: grid;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user